@harness-engineering/core 0.21.1 → 0.21.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ARCHITECTURE_DESCRIPTOR: () => ARCHITECTURE_DESCRIPTOR,
36
36
  AdjustedForecastSchema: () => AdjustedForecastSchema,
37
37
  AgentActionEmitter: () => AgentActionEmitter,
38
+ AnthropicCacheAdapter: () => AnthropicCacheAdapter,
38
39
  ArchBaselineManager: () => ArchBaselineManager,
39
40
  ArchBaselineSchema: () => ArchBaselineSchema,
40
41
  ArchConfigSchema: () => ArchConfigSchema,
@@ -54,12 +55,12 @@ __export(index_exports, {
54
55
  CategorySnapshotSchema: () => CategorySnapshotSchema,
55
56
  ChecklistBuilder: () => ChecklistBuilder,
56
57
  CircularDepsCollector: () => CircularDepsCollector,
58
+ CompactionPipeline: () => CompactionPipeline,
57
59
  ComplexityCollector: () => ComplexityCollector,
58
60
  ConfidenceTierSchema: () => ConfidenceTierSchema,
59
61
  ConfirmationSchema: () => ConfirmationSchema,
60
62
  ConsoleSink: () => ConsoleSink,
61
63
  ConstraintRuleSchema: () => ConstraintRuleSchema,
62
- ContentPipeline: () => ContentPipeline,
63
64
  ContributingFeatureSchema: () => ContributingFeatureSchema,
64
65
  ContributionsSchema: () => ContributionsSchema,
65
66
  CouplingCollector: () => CouplingCollector,
@@ -69,6 +70,7 @@ __export(index_exports, {
69
70
  DEFAULT_STABILITY_THRESHOLDS: () => DEFAULT_STABILITY_THRESHOLDS,
70
71
  DEFAULT_STATE: () => DEFAULT_STATE,
71
72
  DEFAULT_STREAM_INDEX: () => DEFAULT_STREAM_INDEX,
73
+ DEFAULT_TOKEN_BUDGET: () => DEFAULT_TOKEN_BUDGET,
72
74
  DESTRUCTIVE_BASH: () => DESTRUCTIVE_BASH,
73
75
  DepDepthCollector: () => DepDepthCollector,
74
76
  DirectionSchema: () => DirectionSchema,
@@ -82,6 +84,7 @@ __export(index_exports, {
82
84
  ForbiddenImportCollector: () => ForbiddenImportCollector,
83
85
  GateConfigSchema: () => GateConfigSchema,
84
86
  GateResultSchema: () => GateResultSchema,
87
+ GeminiCacheAdapter: () => GeminiCacheAdapter,
85
88
  GitHubIssuesSyncAdapter: () => GitHubIssuesSyncAdapter,
86
89
  HandoffSchema: () => HandoffSchema,
87
90
  HarnessStateSchema: () => HarnessStateSchema,
@@ -96,6 +99,7 @@ __export(index_exports, {
96
99
  NoOpExecutor: () => NoOpExecutor,
97
100
  NoOpSink: () => NoOpSink,
98
101
  NoOpTelemetryAdapter: () => NoOpTelemetryAdapter,
102
+ OpenAICacheAdapter: () => OpenAICacheAdapter,
99
103
  PatternConfigSchema: () => PatternConfigSchema,
100
104
  PredictionEngine: () => PredictionEngine,
101
105
  PredictionOptionsSchema: () => PredictionOptionsSchema,
@@ -123,6 +127,7 @@ __export(index_exports, {
123
127
  StabilityForecastSchema: () => StabilityForecastSchema,
124
128
  StreamIndexSchema: () => StreamIndexSchema,
125
129
  StreamInfoSchema: () => StreamInfoSchema,
130
+ StructuralStrategy: () => StructuralStrategy,
126
131
  ThresholdConfigSchema: () => ThresholdConfigSchema,
127
132
  TimelineFileSchema: () => TimelineFileSchema,
128
133
  TimelineManager: () => TimelineManager,
@@ -130,13 +135,16 @@ __export(index_exports, {
130
135
  TransitionSchema: () => TransitionSchema,
131
136
  TrendLineSchema: () => TrendLineSchema,
132
137
  TrendResultSchema: () => TrendResultSchema,
138
+ TruncationStrategy: () => TruncationStrategy,
133
139
  TypeScriptParser: () => TypeScriptParser,
134
140
  VERSION: () => VERSION,
135
141
  ViolationSchema: () => ViolationSchema,
136
142
  addProvenance: () => addProvenance,
137
143
  agentConfigRules: () => agentConfigRules,
144
+ aggregateAdoptionByDay: () => aggregateByDay2,
138
145
  aggregateByDay: () => aggregateByDay,
139
146
  aggregateBySession: () => aggregateBySession,
147
+ aggregateBySkill: () => aggregateBySkill,
140
148
  analyzeDiff: () => analyzeDiff,
141
149
  analyzeLearningPatterns: () => analyzeLearningPatterns,
142
150
  appendFailure: () => appendFailure,
@@ -168,6 +176,7 @@ __export(index_exports, {
168
176
  clearFailuresCache: () => clearFailuresCache,
169
177
  clearLearningsCache: () => clearLearningsCache,
170
178
  clearTaint: () => clearTaint,
179
+ collectEvents: () => collectEvents,
171
180
  computeContentHash: () => computeContentHash,
172
181
  computeOverallSeverity: () => computeOverallSeverity,
173
182
  computeScanExitCode: () => computeScanExitCode,
@@ -207,6 +216,7 @@ __export(index_exports, {
207
216
  determineAssessment: () => determineAssessment,
208
217
  diff: () => diff,
209
218
  emitEvent: () => emitEvent,
219
+ estimateTokens: () => estimateTokens,
210
220
  executeWorkflow: () => executeWorkflow,
211
221
  expressRules: () => expressRules,
212
222
  extractBundle: () => extractBundle,
@@ -228,6 +238,7 @@ __export(index_exports, {
228
238
  getFeedbackConfig: () => getFeedbackConfig,
229
239
  getInjectionPatterns: () => getInjectionPatterns,
230
240
  getModelPrice: () => getModelPrice,
241
+ getOrCreateInstallId: () => getOrCreateInstallId,
231
242
  getOutline: () => getOutline,
232
243
  getParser: () => getParser,
233
244
  getPhaseCategories: () => getPhaseCategories,
@@ -280,8 +291,10 @@ __export(index_exports, {
280
291
  promoteSessionLearnings: () => promoteSessionLearnings,
281
292
  pruneLearnings: () => pruneLearnings,
282
293
  reactRules: () => reactRules,
294
+ readAdoptionRecords: () => readAdoptionRecords,
283
295
  readCheckState: () => readCheckState,
284
296
  readCostRecords: () => readCostRecords,
297
+ readIdentity: () => readIdentity,
285
298
  readLockfile: () => readLockfile,
286
299
  readSessionSection: () => readSessionSection,
287
300
  readSessionSections: () => readSessionSections,
@@ -292,13 +305,15 @@ __export(index_exports, {
292
305
  requestPeerReview: () => requestPeerReview,
293
306
  resetFeedbackConfig: () => resetFeedbackConfig,
294
307
  resetParserCache: () => resetParserCache,
308
+ resolveConsent: () => resolveConsent,
295
309
  resolveFileToLayer: () => resolveFileToLayer,
296
310
  resolveModelTier: () => resolveModelTier,
297
311
  resolveReverseStatus: () => resolveReverseStatus,
298
312
  resolveRuleSeverity: () => resolveRuleSeverity,
299
313
  resolveSessionDir: () => resolveSessionDir,
314
+ resolveStability: () => resolveStability,
300
315
  resolveStreamPath: () => resolveStreamPath,
301
- resolveThresholds: () => resolveThresholds2,
316
+ resolveThresholds: () => resolveThresholds3,
302
317
  runAll: () => runAll,
303
318
  runArchitectureAgent: () => runArchitectureAgent,
304
319
  runBugDetectionAgent: () => runBugDetectionAgent,
@@ -318,6 +333,8 @@ __export(index_exports, {
318
333
  scoreRoadmapCandidates: () => scoreRoadmapCandidates,
319
334
  searchSymbols: () => searchSymbols,
320
335
  secretRules: () => secretRules,
336
+ send: () => send,
337
+ serializeEnvelope: () => serializeEnvelope,
321
338
  serializeRoadmap: () => serializeRoadmap,
322
339
  setActiveStream: () => setActiveStream,
323
340
  sharpEdgesRules: () => sharpEdgesRules,
@@ -328,6 +345,7 @@ __export(index_exports, {
328
345
  syncRoadmap: () => syncRoadmap,
329
346
  syncToExternal: () => syncToExternal,
330
347
  tagUncitedFindings: () => tagUncitedFindings,
348
+ topSkills: () => topSkills,
331
349
  touchStream: () => touchStream,
332
350
  trackAction: () => trackAction,
333
351
  unfoldRange: () => unfoldRange,
@@ -373,17 +391,17 @@ var import_node_path = require("path");
373
391
  var import_glob = require("glob");
374
392
  var accessAsync = (0, import_util.promisify)(import_fs.access);
375
393
  var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
376
- async function fileExists(path31) {
394
+ async function fileExists(path34) {
377
395
  try {
378
- await accessAsync(path31, import_fs.constants.F_OK);
396
+ await accessAsync(path34, import_fs.constants.F_OK);
379
397
  return true;
380
398
  } catch {
381
399
  return false;
382
400
  }
383
401
  }
384
- async function readFileContent(path31) {
402
+ async function readFileContent(path34) {
385
403
  try {
386
- const content = await readFileAsync(path31, "utf-8");
404
+ const content = await readFileAsync(path34, "utf-8");
387
405
  return (0, import_types.Ok)(content);
388
406
  } catch (error) {
389
407
  return (0, import_types.Err)(error);
@@ -434,15 +452,15 @@ function validateConfig(data, schema) {
434
452
  let message = "Configuration validation failed";
435
453
  const suggestions = [];
436
454
  if (firstError) {
437
- const path31 = firstError.path.join(".");
438
- const pathDisplay = path31 ? ` at "${path31}"` : "";
455
+ const path34 = firstError.path.join(".");
456
+ const pathDisplay = path34 ? ` at "${path34}"` : "";
439
457
  if (firstError.code === "invalid_type") {
440
458
  const received = firstError.received;
441
459
  const expected = firstError.expected;
442
460
  if (received === "undefined") {
443
461
  code = "MISSING_FIELD";
444
462
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
445
- suggestions.push(`Field "${path31}" is required and must be of type "${expected}"`);
463
+ suggestions.push(`Field "${path34}" is required and must be of type "${expected}"`);
446
464
  } else {
447
465
  code = "INVALID_TYPE";
448
466
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -499,21 +517,11 @@ function validateCommitMessage(message, format = "conventional") {
499
517
  issues: []
500
518
  });
501
519
  }
502
- function validateConventionalCommit(message) {
503
- const lines = message.split("\n");
504
- const headerLine = lines[0];
505
- if (!headerLine) {
506
- const error = createError(
507
- "VALIDATION_FAILED",
508
- "Commit message header cannot be empty",
509
- { message },
510
- ["Provide a commit message with at least a header line"]
511
- );
512
- return (0, import_types.Err)(error);
513
- }
520
+ function parseConventionalHeader(message, headerLine) {
514
521
  const match = headerLine.match(CONVENTIONAL_PATTERN);
515
- if (!match) {
516
- const error = createError(
522
+ if (match) return (0, import_types.Ok)(match);
523
+ return (0, import_types.Err)(
524
+ createError(
517
525
  "VALIDATION_FAILED",
518
526
  "Commit message does not follow conventional format",
519
527
  { message, header: headerLine },
@@ -522,13 +530,10 @@ function validateConventionalCommit(message) {
522
530
  "Valid types: " + VALID_TYPES.join(", "),
523
531
  "Example: feat(core): add new feature"
524
532
  ]
525
- );
526
- return (0, import_types.Err)(error);
527
- }
528
- const type = match[1];
529
- const scope = match[3];
530
- const breaking = match[4] === "!";
531
- const description = match[5];
533
+ )
534
+ );
535
+ }
536
+ function collectCommitIssues(type, description) {
532
537
  const issues = [];
533
538
  if (!VALID_TYPES.includes(type)) {
534
539
  issues.push(`Invalid commit type "${type}". Valid types: ${VALID_TYPES.join(", ")}`);
@@ -536,34 +541,50 @@ function validateConventionalCommit(message) {
536
541
  if (!description || description.trim() === "") {
537
542
  issues.push("Commit description cannot be empty");
538
543
  }
539
- let hasBreakingChange = breaking;
540
- if (lines.length > 1) {
541
- const body = lines.slice(1).join("\n");
542
- if (body.includes("BREAKING CHANGE:")) {
543
- hasBreakingChange = true;
544
- }
544
+ return issues;
545
+ }
546
+ function hasBreakingChangeInBody(lines) {
547
+ if (lines.length <= 1) return false;
548
+ return lines.slice(1).join("\n").includes("BREAKING CHANGE:");
549
+ }
550
+ function validateConventionalCommit(message) {
551
+ const lines = message.split("\n");
552
+ const headerLine = lines[0];
553
+ if (!headerLine) {
554
+ return (0, import_types.Err)(
555
+ createError(
556
+ "VALIDATION_FAILED",
557
+ "Commit message header cannot be empty",
558
+ { message },
559
+ ["Provide a commit message with at least a header line"]
560
+ )
561
+ );
545
562
  }
563
+ const matchResult = parseConventionalHeader(message, headerLine);
564
+ if (!matchResult.ok) return matchResult;
565
+ const match = matchResult.value;
566
+ const type = match[1];
567
+ const scope = match[3];
568
+ const breaking = match[4] === "!";
569
+ const description = match[5];
570
+ const issues = collectCommitIssues(type, description);
546
571
  if (issues.length > 0) {
547
- let errorMessage = `Commit message validation failed: ${issues.join("; ")}`;
548
- if (issues.some((issue) => issue.includes("description cannot be empty"))) {
549
- errorMessage = `Commit message validation failed: ${issues.join("; ")}`;
550
- }
551
- const error = createError(
552
- "VALIDATION_FAILED",
553
- errorMessage,
554
- { message, issues, type, scope },
555
- ["Review and fix the validation issues above"]
572
+ return (0, import_types.Err)(
573
+ createError(
574
+ "VALIDATION_FAILED",
575
+ `Commit message validation failed: ${issues.join("; ")}`,
576
+ { message, issues, type, scope },
577
+ ["Review and fix the validation issues above"]
578
+ )
556
579
  );
557
- return (0, import_types.Err)(error);
558
580
  }
559
- const result = {
581
+ return (0, import_types.Ok)({
560
582
  valid: true,
561
583
  type,
562
584
  ...scope && { scope },
563
- breaking: hasBreakingChange,
585
+ breaking: breaking || hasBreakingChangeInBody(lines),
564
586
  issues: []
565
- };
566
- return (0, import_types.Ok)(result);
587
+ });
567
588
  }
568
589
 
569
590
  // src/context/types.ts
@@ -658,27 +679,27 @@ function extractSections(content) {
658
679
  }
659
680
  return sections.map((section) => buildAgentMapSection(section, lines));
660
681
  }
661
- function isExternalLink(path31) {
662
- return path31.startsWith("http://") || path31.startsWith("https://") || path31.startsWith("#") || path31.startsWith("mailto:");
682
+ function isExternalLink(path34) {
683
+ return path34.startsWith("http://") || path34.startsWith("https://") || path34.startsWith("#") || path34.startsWith("mailto:");
663
684
  }
664
685
  function resolveLinkPath(linkPath, baseDir) {
665
686
  return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
666
687
  }
667
- async function validateAgentsMap(path31 = "./AGENTS.md") {
668
- const contentResult = await readFileContent(path31);
688
+ async function validateAgentsMap(path34 = "./AGENTS.md") {
689
+ const contentResult = await readFileContent(path34);
669
690
  if (!contentResult.ok) {
670
691
  return (0, import_types.Err)(
671
692
  createError(
672
693
  "PARSE_ERROR",
673
694
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
674
- { path: path31 },
695
+ { path: path34 },
675
696
  ["Ensure the file exists", "Check file permissions"]
676
697
  )
677
698
  );
678
699
  }
679
700
  const content = contentResult.value;
680
701
  const sections = extractSections(content);
681
- const baseDir = (0, import_path.dirname)(path31);
702
+ const baseDir = (0, import_path.dirname)(path34);
682
703
  const sectionTitles = sections.map((s) => s.title);
683
704
  const missingSections = REQUIRED_SECTIONS.filter(
684
705
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -819,8 +840,8 @@ async function checkDocCoverage(domain, options = {}) {
819
840
 
820
841
  // src/context/knowledge-map.ts
821
842
  var import_path3 = require("path");
822
- function suggestFix(path31, existingFiles) {
823
- const targetName = (0, import_path3.basename)(path31).toLowerCase();
843
+ function suggestFix(path34, existingFiles) {
844
+ const targetName = (0, import_path3.basename)(path34).toLowerCase();
824
845
  const similar = existingFiles.find((file) => {
825
846
  const fileName = (0, import_path3.basename)(file).toLowerCase();
826
847
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -828,7 +849,7 @@ function suggestFix(path31, existingFiles) {
828
849
  if (similar) {
829
850
  return `Did you mean "${similar}"?`;
830
851
  }
831
- return `Create the file "${path31}" or remove the link`;
852
+ return `Create the file "${path34}" or remove the link`;
832
853
  }
833
854
  async function validateKnowledgeMap(rootDir = process.cwd()) {
834
855
  const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
@@ -1021,6 +1042,47 @@ var NODE_TYPE_TO_CATEGORY = {
1021
1042
  prompt: "systemPrompt",
1022
1043
  system: "systemPrompt"
1023
1044
  };
1045
+ function makeZeroWeights() {
1046
+ return {
1047
+ systemPrompt: 0,
1048
+ projectManifest: 0,
1049
+ taskSpec: 0,
1050
+ activeCode: 0,
1051
+ interfaces: 0,
1052
+ reserve: 0
1053
+ };
1054
+ }
1055
+ function normalizeRatios(ratios) {
1056
+ const sum = Object.values(ratios).reduce((s, r) => s + r, 0);
1057
+ if (sum === 0) return;
1058
+ for (const key of Object.keys(ratios)) {
1059
+ ratios[key] = ratios[key] / sum;
1060
+ }
1061
+ }
1062
+ function enforceMinimumRatios(ratios, min) {
1063
+ for (const key of Object.keys(ratios)) {
1064
+ if (ratios[key] < min) ratios[key] = min;
1065
+ }
1066
+ }
1067
+ function applyGraphDensity(ratios, graphDensity) {
1068
+ const weights = makeZeroWeights();
1069
+ for (const [nodeType, count] of Object.entries(graphDensity)) {
1070
+ const category = NODE_TYPE_TO_CATEGORY[nodeType];
1071
+ if (category) weights[category] += count;
1072
+ }
1073
+ const totalWeight = Object.values(weights).reduce((s, w) => s + w, 0);
1074
+ if (totalWeight === 0) return;
1075
+ const MIN = 0.01;
1076
+ for (const key of Object.keys(ratios)) {
1077
+ ratios[key] = weights[key] > 0 ? weights[key] / totalWeight : MIN;
1078
+ }
1079
+ if (ratios.reserve < DEFAULT_RATIOS.reserve) ratios.reserve = DEFAULT_RATIOS.reserve;
1080
+ if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt)
1081
+ ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
1082
+ normalizeRatios(ratios);
1083
+ enforceMinimumRatios(ratios, MIN);
1084
+ normalizeRatios(ratios);
1085
+ }
1024
1086
  function contextBudget(totalTokens, overrides, graphDensity) {
1025
1087
  const ratios = {
1026
1088
  systemPrompt: DEFAULT_RATIOS.systemPrompt,
@@ -1031,50 +1093,7 @@ function contextBudget(totalTokens, overrides, graphDensity) {
1031
1093
  reserve: DEFAULT_RATIOS.reserve
1032
1094
  };
1033
1095
  if (graphDensity) {
1034
- const categoryWeights = {
1035
- systemPrompt: 0,
1036
- projectManifest: 0,
1037
- taskSpec: 0,
1038
- activeCode: 0,
1039
- interfaces: 0,
1040
- reserve: 0
1041
- };
1042
- for (const [nodeType, count] of Object.entries(graphDensity)) {
1043
- const category = NODE_TYPE_TO_CATEGORY[nodeType];
1044
- if (category) {
1045
- categoryWeights[category] += count;
1046
- }
1047
- }
1048
- const totalWeight = Object.values(categoryWeights).reduce((sum, w) => sum + w, 0);
1049
- if (totalWeight > 0) {
1050
- const MIN_ALLOCATION = 0.01;
1051
- for (const key of Object.keys(ratios)) {
1052
- if (categoryWeights[key] > 0) {
1053
- ratios[key] = categoryWeights[key] / totalWeight;
1054
- } else {
1055
- ratios[key] = MIN_ALLOCATION;
1056
- }
1057
- }
1058
- if (ratios.reserve < DEFAULT_RATIOS.reserve) {
1059
- ratios.reserve = DEFAULT_RATIOS.reserve;
1060
- }
1061
- if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt) {
1062
- ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
1063
- }
1064
- const ratioSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
1065
- for (const key of Object.keys(ratios)) {
1066
- ratios[key] = ratios[key] / ratioSum;
1067
- }
1068
- for (const key of Object.keys(ratios)) {
1069
- if (ratios[key] < MIN_ALLOCATION) {
1070
- ratios[key] = MIN_ALLOCATION;
1071
- }
1072
- }
1073
- const finalSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
1074
- for (const key of Object.keys(ratios)) {
1075
- ratios[key] = ratios[key] / finalSum;
1076
- }
1077
- }
1096
+ applyGraphDensity(ratios, graphDensity);
1078
1097
  }
1079
1098
  if (overrides) {
1080
1099
  let overrideSum = 0;
@@ -1434,8 +1453,8 @@ function createBoundaryValidator(schema, name) {
1434
1453
  return (0, import_types.Ok)(result.data);
1435
1454
  }
1436
1455
  const suggestions = result.error.issues.map((issue) => {
1437
- const path31 = issue.path.join(".");
1438
- return path31 ? `${path31}: ${issue.message}` : issue.message;
1456
+ const path34 = issue.path.join(".");
1457
+ return path34 ? `${path34}: ${issue.message}` : issue.message;
1439
1458
  });
1440
1459
  return (0, import_types.Err)(
1441
1460
  createError(
@@ -1635,21 +1654,23 @@ function extractBundle(manifest, config) {
1635
1654
  }
1636
1655
 
1637
1656
  // src/constraints/sharing/merge.ts
1657
+ function arraysEqual(a, b) {
1658
+ if (a.length !== b.length) return false;
1659
+ return a.every((val, i) => deepEqual(val, b[i]));
1660
+ }
1661
+ function objectsEqual(a, b) {
1662
+ const keysA = Object.keys(a);
1663
+ const keysB = Object.keys(b);
1664
+ if (keysA.length !== keysB.length) return false;
1665
+ return keysA.every((key) => deepEqual(a[key], b[key]));
1666
+ }
1638
1667
  function deepEqual(a, b) {
1639
1668
  if (a === b) return true;
1640
1669
  if (typeof a !== typeof b) return false;
1641
1670
  if (typeof a !== "object" || a === null || b === null) return false;
1642
- if (Array.isArray(a) && Array.isArray(b)) {
1643
- if (a.length !== b.length) return false;
1644
- return a.every((val, i) => deepEqual(val, b[i]));
1645
- }
1671
+ if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
1646
1672
  if (Array.isArray(a) !== Array.isArray(b)) return false;
1647
- const keysA = Object.keys(a);
1648
- const keysB = Object.keys(b);
1649
- if (keysA.length !== keysB.length) return false;
1650
- return keysA.every(
1651
- (key) => deepEqual(a[key], b[key])
1652
- );
1673
+ return objectsEqual(a, b);
1653
1674
  }
1654
1675
  function stringArraysEqual(a, b) {
1655
1676
  if (a.length !== b.length) return false;
@@ -2067,11 +2088,11 @@ function processExportListSpecifiers(exportDecl, exports2) {
2067
2088
  var TypeScriptParser = class {
2068
2089
  name = "typescript";
2069
2090
  extensions = [".ts", ".tsx", ".mts", ".cts"];
2070
- async parseFile(path31) {
2071
- const contentResult = await readFileContent(path31);
2091
+ async parseFile(path34) {
2092
+ const contentResult = await readFileContent(path34);
2072
2093
  if (!contentResult.ok) {
2073
2094
  return (0, import_types.Err)(
2074
- createParseError("NOT_FOUND", `File not found: ${path31}`, { path: path31 }, [
2095
+ createParseError("NOT_FOUND", `File not found: ${path34}`, { path: path34 }, [
2075
2096
  "Check that the file exists",
2076
2097
  "Verify the path is correct"
2077
2098
  ])
@@ -2081,7 +2102,7 @@ var TypeScriptParser = class {
2081
2102
  const ast = (0, import_typescript_estree.parse)(contentResult.value, {
2082
2103
  loc: true,
2083
2104
  range: true,
2084
- jsx: path31.endsWith(".tsx"),
2105
+ jsx: path34.endsWith(".tsx"),
2085
2106
  errorOnUnknownASTType: false
2086
2107
  });
2087
2108
  return (0, import_types.Ok)({
@@ -2092,7 +2113,7 @@ var TypeScriptParser = class {
2092
2113
  } catch (e) {
2093
2114
  const error = e;
2094
2115
  return (0, import_types.Err)(
2095
- createParseError("SYNTAX_ERROR", `Failed to parse ${path31}: ${error.message}`, { path: path31 }, [
2116
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path34}: ${error.message}`, { path: path34 }, [
2096
2117
  "Check for syntax errors in the file",
2097
2118
  "Ensure valid TypeScript syntax"
2098
2119
  ])
@@ -2277,22 +2298,22 @@ function extractInlineRefs(content) {
2277
2298
  }
2278
2299
  return refs;
2279
2300
  }
2280
- async function parseDocumentationFile(path31) {
2281
- const contentResult = await readFileContent(path31);
2301
+ async function parseDocumentationFile(path34) {
2302
+ const contentResult = await readFileContent(path34);
2282
2303
  if (!contentResult.ok) {
2283
2304
  return (0, import_types.Err)(
2284
2305
  createEntropyError(
2285
2306
  "PARSE_ERROR",
2286
- `Failed to read documentation file: ${path31}`,
2287
- { file: path31 },
2307
+ `Failed to read documentation file: ${path34}`,
2308
+ { file: path34 },
2288
2309
  ["Check that the file exists"]
2289
2310
  )
2290
2311
  );
2291
2312
  }
2292
2313
  const content = contentResult.value;
2293
- const type = path31.endsWith(".md") ? "markdown" : "text";
2314
+ const type = path34.endsWith(".md") ? "markdown" : "text";
2294
2315
  return (0, import_types.Ok)({
2295
- path: path31,
2316
+ path: path34,
2296
2317
  type,
2297
2318
  content,
2298
2319
  codeBlocks: extractCodeBlocks(content),
@@ -2302,17 +2323,22 @@ async function parseDocumentationFile(path31) {
2302
2323
  function makeInternalSymbol(name, type, line) {
2303
2324
  return { name, type, line, references: 0, calledBy: [] };
2304
2325
  }
2326
+ function extractFunctionSymbol(node, line) {
2327
+ if (node.id?.name) return [makeInternalSymbol(node.id.name, "function", line)];
2328
+ return [];
2329
+ }
2330
+ function extractVariableSymbols(node, line) {
2331
+ return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
2332
+ }
2333
+ function extractClassSymbol(node, line) {
2334
+ if (node.id?.name) return [makeInternalSymbol(node.id.name, "class", line)];
2335
+ return [];
2336
+ }
2305
2337
  function extractSymbolsFromNode(node) {
2306
2338
  const line = node.loc?.start?.line || 0;
2307
- if (node.type === "FunctionDeclaration" && node.id?.name) {
2308
- return [makeInternalSymbol(node.id.name, "function", line)];
2309
- }
2310
- if (node.type === "VariableDeclaration") {
2311
- return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
2312
- }
2313
- if (node.type === "ClassDeclaration" && node.id?.name) {
2314
- return [makeInternalSymbol(node.id.name, "class", line)];
2315
- }
2339
+ if (node.type === "FunctionDeclaration") return extractFunctionSymbol(node, line);
2340
+ if (node.type === "VariableDeclaration") return extractVariableSymbols(node, line);
2341
+ if (node.type === "ClassDeclaration") return extractClassSymbol(node, line);
2316
2342
  return [];
2317
2343
  }
2318
2344
  function extractInternalSymbols(ast) {
@@ -2321,21 +2347,17 @@ function extractInternalSymbols(ast) {
2321
2347
  const nodes = body.body;
2322
2348
  return nodes.flatMap(extractSymbolsFromNode);
2323
2349
  }
2350
+ function toJSDocComment(comment) {
2351
+ if (comment.type !== "Block" || !comment.value?.startsWith("*")) return null;
2352
+ return { content: comment.value, line: comment.loc?.start?.line || 0 };
2353
+ }
2324
2354
  function extractJSDocComments(ast) {
2325
- const comments = [];
2326
2355
  const body = ast.body;
2327
- if (body?.comments) {
2328
- for (const comment of body.comments) {
2329
- if (comment.type === "Block" && comment.value?.startsWith("*")) {
2330
- const jsDocComment = {
2331
- content: comment.value,
2332
- line: comment.loc?.start?.line || 0
2333
- };
2334
- comments.push(jsDocComment);
2335
- }
2336
- }
2337
- }
2338
- return comments;
2356
+ if (!body?.comments) return [];
2357
+ return body.comments.flatMap((c) => {
2358
+ const doc = toJSDocComment(c);
2359
+ return doc ? [doc] : [];
2360
+ });
2339
2361
  }
2340
2362
  function buildExportMap(files) {
2341
2363
  const byFile = /* @__PURE__ */ new Map();
@@ -2350,41 +2372,42 @@ function buildExportMap(files) {
2350
2372
  }
2351
2373
  return { byFile, byName };
2352
2374
  }
2353
- function extractAllCodeReferences(docs) {
2375
+ var CODE_BLOCK_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "ts", "javascript", "js"]);
2376
+ function refsFromInlineRefs(doc) {
2377
+ return doc.inlineRefs.map((inlineRef) => ({
2378
+ docFile: doc.path,
2379
+ line: inlineRef.line,
2380
+ column: inlineRef.column,
2381
+ reference: inlineRef.reference,
2382
+ context: "inline"
2383
+ }));
2384
+ }
2385
+ function refsFromCodeBlock(docPath, block) {
2386
+ if (!CODE_BLOCK_LANGUAGES.has(block.language)) return [];
2354
2387
  const refs = [];
2355
- for (const doc of docs) {
2356
- for (const inlineRef of doc.inlineRefs) {
2388
+ const importRegex = /import\s+\{([^}]+)\}\s+from/g;
2389
+ let match;
2390
+ while ((match = importRegex.exec(block.content)) !== null) {
2391
+ const group = match[1];
2392
+ if (group === void 0) continue;
2393
+ for (const name of group.split(",").map((n) => n.trim())) {
2357
2394
  refs.push({
2358
- docFile: doc.path,
2359
- line: inlineRef.line,
2360
- column: inlineRef.column,
2361
- reference: inlineRef.reference,
2362
- context: "inline"
2395
+ docFile: docPath,
2396
+ line: block.line,
2397
+ column: 0,
2398
+ reference: name,
2399
+ context: "code-block"
2363
2400
  });
2364
2401
  }
2365
- for (const block of doc.codeBlocks) {
2366
- if (block.language === "typescript" || block.language === "ts" || block.language === "javascript" || block.language === "js") {
2367
- const importRegex = /import\s+\{([^}]+)\}\s+from/g;
2368
- let match;
2369
- while ((match = importRegex.exec(block.content)) !== null) {
2370
- const matchedGroup = match[1];
2371
- if (matchedGroup === void 0) continue;
2372
- const names = matchedGroup.split(",").map((n) => n.trim());
2373
- for (const name of names) {
2374
- refs.push({
2375
- docFile: doc.path,
2376
- line: block.line,
2377
- column: 0,
2378
- reference: name,
2379
- context: "code-block"
2380
- });
2381
- }
2382
- }
2383
- }
2384
- }
2385
2402
  }
2386
2403
  return refs;
2387
2404
  }
2405
+ function refsFromCodeBlocks(doc) {
2406
+ return doc.codeBlocks.flatMap((block) => refsFromCodeBlock(doc.path, block));
2407
+ }
2408
+ function extractAllCodeReferences(docs) {
2409
+ return docs.flatMap((doc) => [...refsFromInlineRefs(doc), ...refsFromCodeBlocks(doc)]);
2410
+ }
2388
2411
  async function buildSnapshot(config) {
2389
2412
  const startTime = Date.now();
2390
2413
  const parser = config.parser || new TypeScriptParser();
@@ -2590,44 +2613,52 @@ async function checkStructureDrift(snapshot, _config) {
2590
2613
  }
2591
2614
  return drifts;
2592
2615
  }
2616
+ function computeDriftSeverity(driftCount) {
2617
+ if (driftCount === 0) return "none";
2618
+ if (driftCount <= 3) return "low";
2619
+ if (driftCount <= 10) return "medium";
2620
+ return "high";
2621
+ }
2622
+ function buildGraphDriftReport(graphDriftData) {
2623
+ const drifts = [];
2624
+ for (const target of graphDriftData.missingTargets) {
2625
+ drifts.push({
2626
+ type: "api-signature",
2627
+ docFile: target,
2628
+ line: 0,
2629
+ reference: target,
2630
+ context: "graph-missing-target",
2631
+ issue: "NOT_FOUND",
2632
+ details: `Graph node "${target}" has no matching code target`,
2633
+ confidence: "high"
2634
+ });
2635
+ }
2636
+ for (const edge of graphDriftData.staleEdges) {
2637
+ drifts.push({
2638
+ type: "api-signature",
2639
+ docFile: edge.docNodeId,
2640
+ line: 0,
2641
+ reference: edge.codeNodeId,
2642
+ context: `graph-stale-edge:${edge.edgeType}`,
2643
+ issue: "NOT_FOUND",
2644
+ details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
2645
+ confidence: "medium"
2646
+ });
2647
+ }
2648
+ return (0, import_types.Ok)({
2649
+ drifts,
2650
+ stats: {
2651
+ docsScanned: graphDriftData.staleEdges.length,
2652
+ referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
2653
+ driftsFound: drifts.length,
2654
+ byType: { api: drifts.length, example: 0, structure: 0 }
2655
+ },
2656
+ severity: computeDriftSeverity(drifts.length)
2657
+ });
2658
+ }
2593
2659
  async function detectDocDrift(snapshot, config, graphDriftData) {
2594
2660
  if (graphDriftData) {
2595
- const drifts2 = [];
2596
- for (const target of graphDriftData.missingTargets) {
2597
- drifts2.push({
2598
- type: "api-signature",
2599
- docFile: target,
2600
- line: 0,
2601
- reference: target,
2602
- context: "graph-missing-target",
2603
- issue: "NOT_FOUND",
2604
- details: `Graph node "${target}" has no matching code target`,
2605
- confidence: "high"
2606
- });
2607
- }
2608
- for (const edge of graphDriftData.staleEdges) {
2609
- drifts2.push({
2610
- type: "api-signature",
2611
- docFile: edge.docNodeId,
2612
- line: 0,
2613
- reference: edge.codeNodeId,
2614
- context: `graph-stale-edge:${edge.edgeType}`,
2615
- issue: "NOT_FOUND",
2616
- details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
2617
- confidence: "medium"
2618
- });
2619
- }
2620
- const severity2 = drifts2.length === 0 ? "none" : drifts2.length <= 3 ? "low" : drifts2.length <= 10 ? "medium" : "high";
2621
- return (0, import_types.Ok)({
2622
- drifts: drifts2,
2623
- stats: {
2624
- docsScanned: graphDriftData.staleEdges.length,
2625
- referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
2626
- driftsFound: drifts2.length,
2627
- byType: { api: drifts2.length, example: 0, structure: 0 }
2628
- },
2629
- severity: severity2
2630
- });
2661
+ return buildGraphDriftReport(graphDriftData);
2631
2662
  }
2632
2663
  const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
2633
2664
  const drifts = [];
@@ -2676,6 +2707,23 @@ function resolveImportToFile(importSource, fromFile, snapshot) {
2676
2707
  }
2677
2708
  return null;
2678
2709
  }
2710
+ function enqueueResolved(sources, current, snapshot, visited, queue) {
2711
+ for (const item of sources) {
2712
+ if (!item.source) continue;
2713
+ const resolved = resolveImportToFile(item.source, current, snapshot);
2714
+ if (resolved && !visited.has(resolved)) {
2715
+ queue.push(resolved);
2716
+ }
2717
+ }
2718
+ }
2719
+ function processReachabilityNode(current, snapshot, reachability, visited, queue) {
2720
+ reachability.set(current, true);
2721
+ const sourceFile = snapshot.files.find((f) => f.path === current);
2722
+ if (!sourceFile) return;
2723
+ enqueueResolved(sourceFile.imports, current, snapshot, visited, queue);
2724
+ const reExports = sourceFile.exports.filter((e) => e.isReExport);
2725
+ enqueueResolved(reExports, current, snapshot, visited, queue);
2726
+ }
2679
2727
  function buildReachabilityMap(snapshot) {
2680
2728
  const reachability = /* @__PURE__ */ new Map();
2681
2729
  for (const file of snapshot.files) {
@@ -2687,23 +2735,7 @@ function buildReachabilityMap(snapshot) {
2687
2735
  const current = queue.shift();
2688
2736
  if (visited.has(current)) continue;
2689
2737
  visited.add(current);
2690
- reachability.set(current, true);
2691
- const sourceFile = snapshot.files.find((f) => f.path === current);
2692
- if (!sourceFile) continue;
2693
- for (const imp of sourceFile.imports) {
2694
- const resolved = resolveImportToFile(imp.source, current, snapshot);
2695
- if (resolved && !visited.has(resolved)) {
2696
- queue.push(resolved);
2697
- }
2698
- }
2699
- for (const exp of sourceFile.exports) {
2700
- if (exp.isReExport && exp.source) {
2701
- const resolved = resolveImportToFile(exp.source, current, snapshot);
2702
- if (resolved && !visited.has(resolved)) {
2703
- queue.push(resolved);
2704
- }
2705
- }
2706
- }
2738
+ processReachabilityNode(current, snapshot, reachability, visited, queue);
2707
2739
  }
2708
2740
  return reachability;
2709
2741
  }
@@ -2773,21 +2805,27 @@ function findDeadExports(snapshot, usageMap, reachability) {
2773
2805
  }
2774
2806
  return deadExports;
2775
2807
  }
2808
+ function maxLineOfValue(value) {
2809
+ if (Array.isArray(value)) {
2810
+ return value.reduce((m, item) => Math.max(m, findMaxLineInNode(item)), 0);
2811
+ }
2812
+ if (value && typeof value === "object") {
2813
+ return findMaxLineInNode(value);
2814
+ }
2815
+ return 0;
2816
+ }
2817
+ function maxLineOfNodeKeys(node) {
2818
+ let max = 0;
2819
+ for (const key of Object.keys(node)) {
2820
+ max = Math.max(max, maxLineOfValue(node[key]));
2821
+ }
2822
+ return max;
2823
+ }
2776
2824
  function findMaxLineInNode(node) {
2777
2825
  if (!node || typeof node !== "object") return 0;
2778
2826
  const n = node;
2779
- let maxLine = n.loc?.end?.line ?? 0;
2780
- for (const key of Object.keys(node)) {
2781
- const value = node[key];
2782
- if (Array.isArray(value)) {
2783
- for (const item of value) {
2784
- maxLine = Math.max(maxLine, findMaxLineInNode(item));
2785
- }
2786
- } else if (value && typeof value === "object") {
2787
- maxLine = Math.max(maxLine, findMaxLineInNode(value));
2788
- }
2789
- }
2790
- return maxLine;
2827
+ const locLine = n.loc?.end?.line ?? 0;
2828
+ return Math.max(locLine, maxLineOfNodeKeys(node));
2791
2829
  }
2792
2830
  function countLinesFromAST(ast) {
2793
2831
  if (!ast.body || !Array.isArray(ast.body)) return 1;
@@ -2861,54 +2899,59 @@ function findDeadInternals(snapshot, _reachability) {
2861
2899
  }
2862
2900
  return deadInternals;
2863
2901
  }
2864
- async function detectDeadCode(snapshot, graphDeadCodeData) {
2865
- if (graphDeadCodeData) {
2866
- const deadFiles2 = [];
2867
- const deadExports2 = [];
2868
- const fileTypes = /* @__PURE__ */ new Set(["file", "module"]);
2869
- const exportTypes = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
2870
- for (const node of graphDeadCodeData.unreachableNodes) {
2871
- if (fileTypes.has(node.type)) {
2872
- deadFiles2.push({
2873
- path: node.path || node.id,
2874
- reason: "NO_IMPORTERS",
2875
- exportCount: 0,
2876
- lineCount: 0
2877
- });
2878
- } else if (exportTypes.has(node.type)) {
2879
- const exportType = node.type === "method" ? "function" : node.type;
2880
- deadExports2.push({
2881
- file: node.path || node.id,
2882
- name: node.name,
2883
- line: 0,
2884
- type: exportType,
2885
- isDefault: false,
2886
- reason: "NO_IMPORTERS"
2887
- });
2888
- }
2889
- }
2890
- const reachableCount = graphDeadCodeData.reachableNodeIds instanceof Set ? graphDeadCodeData.reachableNodeIds.size : graphDeadCodeData.reachableNodeIds.length;
2891
- const fileNodes = graphDeadCodeData.unreachableNodes.filter((n) => fileTypes.has(n.type));
2892
- const exportNodes = graphDeadCodeData.unreachableNodes.filter((n) => exportTypes.has(n.type));
2893
- const totalFiles = reachableCount + fileNodes.length;
2894
- const totalExports2 = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
2895
- const report2 = {
2896
- deadExports: deadExports2,
2897
- deadFiles: deadFiles2,
2898
- deadInternals: [],
2899
- unusedImports: [],
2900
- stats: {
2901
- filesAnalyzed: totalFiles,
2902
- entryPointsUsed: [],
2903
- totalExports: totalExports2,
2904
- deadExportCount: deadExports2.length,
2905
- totalFiles,
2906
- deadFileCount: deadFiles2.length,
2907
- estimatedDeadLines: 0
2908
- }
2909
- };
2910
- return (0, import_types.Ok)(report2);
2902
+ var FILE_TYPES = /* @__PURE__ */ new Set(["file", "module"]);
2903
+ var EXPORT_TYPES = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
2904
+ function classifyUnreachableNode(node, deadFiles, deadExports) {
2905
+ if (FILE_TYPES.has(node.type)) {
2906
+ deadFiles.push({
2907
+ path: node.path || node.id,
2908
+ reason: "NO_IMPORTERS",
2909
+ exportCount: 0,
2910
+ lineCount: 0
2911
+ });
2912
+ } else if (EXPORT_TYPES.has(node.type)) {
2913
+ const exportType = node.type === "method" ? "function" : node.type;
2914
+ deadExports.push({
2915
+ file: node.path || node.id,
2916
+ name: node.name,
2917
+ line: 0,
2918
+ type: exportType,
2919
+ isDefault: false,
2920
+ reason: "NO_IMPORTERS"
2921
+ });
2911
2922
  }
2923
+ }
2924
+ function computeGraphReportStats(data, deadFiles, deadExports) {
2925
+ const reachableCount = data.reachableNodeIds instanceof Set ? data.reachableNodeIds.size : data.reachableNodeIds.length;
2926
+ const fileNodes = data.unreachableNodes.filter((n) => FILE_TYPES.has(n.type));
2927
+ const exportNodes = data.unreachableNodes.filter((n) => EXPORT_TYPES.has(n.type));
2928
+ const totalFiles = reachableCount + fileNodes.length;
2929
+ const totalExports = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
2930
+ return {
2931
+ filesAnalyzed: totalFiles,
2932
+ entryPointsUsed: [],
2933
+ totalExports,
2934
+ deadExportCount: deadExports.length,
2935
+ totalFiles,
2936
+ deadFileCount: deadFiles.length,
2937
+ estimatedDeadLines: 0
2938
+ };
2939
+ }
2940
+ function buildReportFromGraph(data) {
2941
+ const deadFiles = [];
2942
+ const deadExports = [];
2943
+ for (const node of data.unreachableNodes) {
2944
+ classifyUnreachableNode(node, deadFiles, deadExports);
2945
+ }
2946
+ return {
2947
+ deadExports,
2948
+ deadFiles,
2949
+ deadInternals: [],
2950
+ unusedImports: [],
2951
+ stats: computeGraphReportStats(data, deadFiles, deadExports)
2952
+ };
2953
+ }
2954
+ function buildReportFromSnapshot(snapshot) {
2912
2955
  const reachability = buildReachabilityMap(snapshot);
2913
2956
  const usageMap = buildExportUsageMap(snapshot);
2914
2957
  const deadExports = findDeadExports(snapshot, usageMap, reachability);
@@ -2920,7 +2963,7 @@ async function detectDeadCode(snapshot, graphDeadCodeData) {
2920
2963
  0
2921
2964
  );
2922
2965
  const estimatedDeadLines = deadFiles.reduce((acc, file) => acc + file.lineCount, 0);
2923
- const report = {
2966
+ return {
2924
2967
  deadExports,
2925
2968
  deadFiles,
2926
2969
  deadInternals,
@@ -2935,6 +2978,9 @@ async function detectDeadCode(snapshot, graphDeadCodeData) {
2935
2978
  estimatedDeadLines
2936
2979
  }
2937
2980
  };
2981
+ }
2982
+ async function detectDeadCode(snapshot, graphDeadCodeData) {
2983
+ const report = graphDeadCodeData ? buildReportFromGraph(graphDeadCodeData) : buildReportFromSnapshot(snapshot);
2938
2984
  return (0, import_types.Ok)(report);
2939
2985
  }
2940
2986
 
@@ -3205,26 +3251,28 @@ function findFunctionEnd(lines, startIdx) {
3205
3251
  }
3206
3252
  return lines.length - 1;
3207
3253
  }
3208
- function computeCyclomaticComplexity(body) {
3209
- let complexity = 1;
3210
- const decisionPatterns = [
3211
- /\bif\s*\(/g,
3212
- /\belse\s+if\s*\(/g,
3213
- /\bwhile\s*\(/g,
3214
- /\bfor\s*\(/g,
3215
- /\bcase\s+/g,
3216
- /&&/g,
3217
- /\|\|/g,
3218
- /\?(?!=)/g,
3219
- // Ternary ? but not ?. or ??
3220
- /\bcatch\s*\(/g
3221
- ];
3222
- for (const pattern of decisionPatterns) {
3254
+ var DECISION_PATTERNS = [
3255
+ /\bif\s*\(/g,
3256
+ /\belse\s+if\s*\(/g,
3257
+ /\bwhile\s*\(/g,
3258
+ /\bfor\s*\(/g,
3259
+ /\bcase\s+/g,
3260
+ /&&/g,
3261
+ /\|\|/g,
3262
+ /\?(?!=)/g,
3263
+ // Ternary ? but not ?. or ??
3264
+ /\bcatch\s*\(/g
3265
+ ];
3266
+ function countDecisionPoints(body) {
3267
+ let count = 0;
3268
+ for (const pattern of DECISION_PATTERNS) {
3223
3269
  const matches = body.match(pattern);
3224
- if (matches) {
3225
- complexity += matches.length;
3226
- }
3270
+ if (matches) count += matches.length;
3227
3271
  }
3272
+ return count;
3273
+ }
3274
+ function computeCyclomaticComplexity(body) {
3275
+ let complexity = 1 + countDecisionPoints(body);
3228
3276
  const elseIfMatches = body.match(/\belse\s+if\s*\(/g);
3229
3277
  if (elseIfMatches) {
3230
3278
  complexity -= elseIfMatches.length;
@@ -3502,8 +3550,8 @@ function resolveImportSource(source, fromFile, snapshot) {
3502
3550
  }
3503
3551
  return void 0;
3504
3552
  }
3505
- function checkViolations(metrics, config) {
3506
- const thresholds = {
3553
+ function resolveThresholds2(config) {
3554
+ return {
3507
3555
  fanOut: { ...DEFAULT_THRESHOLDS2.fanOut, ...config?.thresholds?.fanOut },
3508
3556
  fanIn: { ...DEFAULT_THRESHOLDS2.fanIn, ...config?.thresholds?.fanIn },
3509
3557
  couplingRatio: { ...DEFAULT_THRESHOLDS2.couplingRatio, ...config?.thresholds?.couplingRatio },
@@ -3512,53 +3560,70 @@ function checkViolations(metrics, config) {
3512
3560
  ...config?.thresholds?.transitiveDependencyDepth
3513
3561
  }
3514
3562
  };
3563
+ }
3564
+ function checkFanOut(m, threshold) {
3565
+ if (m.fanOut <= threshold) return null;
3566
+ return {
3567
+ file: m.file,
3568
+ metric: "fanOut",
3569
+ value: m.fanOut,
3570
+ threshold,
3571
+ tier: 2,
3572
+ severity: "warning",
3573
+ message: `File has ${m.fanOut} imports (threshold: ${threshold})`
3574
+ };
3575
+ }
3576
+ function checkFanIn(m, threshold) {
3577
+ if (m.fanIn <= threshold) return null;
3578
+ return {
3579
+ file: m.file,
3580
+ metric: "fanIn",
3581
+ value: m.fanIn,
3582
+ threshold,
3583
+ tier: 3,
3584
+ severity: "info",
3585
+ message: `File is imported by ${m.fanIn} files (threshold: ${threshold})`
3586
+ };
3587
+ }
3588
+ function checkCouplingRatio(m, threshold) {
3589
+ const totalConnections = m.fanIn + m.fanOut;
3590
+ if (totalConnections <= 5 || m.couplingRatio <= threshold) return null;
3591
+ return {
3592
+ file: m.file,
3593
+ metric: "couplingRatio",
3594
+ value: m.couplingRatio,
3595
+ threshold,
3596
+ tier: 2,
3597
+ severity: "warning",
3598
+ message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${threshold})`
3599
+ };
3600
+ }
3601
+ function checkTransitiveDepth(m, threshold) {
3602
+ if (m.transitiveDepth <= threshold) return null;
3603
+ return {
3604
+ file: m.file,
3605
+ metric: "transitiveDependencyDepth",
3606
+ value: m.transitiveDepth,
3607
+ threshold,
3608
+ tier: 3,
3609
+ severity: "info",
3610
+ message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${threshold})`
3611
+ };
3612
+ }
3613
+ function checkMetricViolations(m, thresholds) {
3614
+ const candidates = [
3615
+ checkFanOut(m, thresholds.fanOut.warn),
3616
+ checkFanIn(m, thresholds.fanIn.info),
3617
+ checkCouplingRatio(m, thresholds.couplingRatio.warn),
3618
+ checkTransitiveDepth(m, thresholds.transitiveDependencyDepth.info)
3619
+ ];
3620
+ return candidates.filter((v) => v !== null);
3621
+ }
3622
+ function checkViolations(metrics, config) {
3623
+ const thresholds = resolveThresholds2(config);
3515
3624
  const violations = [];
3516
3625
  for (const m of metrics) {
3517
- if (thresholds.fanOut.warn !== void 0 && m.fanOut > thresholds.fanOut.warn) {
3518
- violations.push({
3519
- file: m.file,
3520
- metric: "fanOut",
3521
- value: m.fanOut,
3522
- threshold: thresholds.fanOut.warn,
3523
- tier: 2,
3524
- severity: "warning",
3525
- message: `File has ${m.fanOut} imports (threshold: ${thresholds.fanOut.warn})`
3526
- });
3527
- }
3528
- if (thresholds.fanIn.info !== void 0 && m.fanIn > thresholds.fanIn.info) {
3529
- violations.push({
3530
- file: m.file,
3531
- metric: "fanIn",
3532
- value: m.fanIn,
3533
- threshold: thresholds.fanIn.info,
3534
- tier: 3,
3535
- severity: "info",
3536
- message: `File is imported by ${m.fanIn} files (threshold: ${thresholds.fanIn.info})`
3537
- });
3538
- }
3539
- const totalConnections = m.fanIn + m.fanOut;
3540
- if (totalConnections > 5 && thresholds.couplingRatio.warn !== void 0 && m.couplingRatio > thresholds.couplingRatio.warn) {
3541
- violations.push({
3542
- file: m.file,
3543
- metric: "couplingRatio",
3544
- value: m.couplingRatio,
3545
- threshold: thresholds.couplingRatio.warn,
3546
- tier: 2,
3547
- severity: "warning",
3548
- message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${thresholds.couplingRatio.warn})`
3549
- });
3550
- }
3551
- if (thresholds.transitiveDependencyDepth.info !== void 0 && m.transitiveDepth > thresholds.transitiveDependencyDepth.info) {
3552
- violations.push({
3553
- file: m.file,
3554
- metric: "transitiveDependencyDepth",
3555
- value: m.transitiveDepth,
3556
- threshold: thresholds.transitiveDependencyDepth.info,
3557
- tier: 3,
3558
- severity: "info",
3559
- message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${thresholds.transitiveDependencyDepth.info})`
3560
- });
3561
- }
3626
+ violations.push(...checkMetricViolations(m, thresholds));
3562
3627
  }
3563
3628
  return violations;
3564
3629
  }
@@ -3668,48 +3733,52 @@ async function detectSizeBudgetViolations(rootDir, config) {
3668
3733
  }
3669
3734
 
3670
3735
  // src/entropy/fixers/suggestions.ts
3736
+ function deadFileSuggestion(file) {
3737
+ return {
3738
+ type: "delete",
3739
+ priority: "high",
3740
+ source: "dead-code",
3741
+ relatedIssues: [`dead-file:${file.path}`],
3742
+ title: `Remove dead file: ${file.path.split("/").pop()}`,
3743
+ description: `This file is not imported by any other file and can be safely removed.`,
3744
+ files: [file.path],
3745
+ steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
3746
+ whyManual: "File deletion requires verification that no dynamic imports exist"
3747
+ };
3748
+ }
3749
+ function deadExportSuggestion(exp) {
3750
+ return {
3751
+ type: "refactor",
3752
+ priority: "medium",
3753
+ source: "dead-code",
3754
+ relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
3755
+ title: `Remove unused export: ${exp.name}`,
3756
+ description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
3757
+ files: [exp.file],
3758
+ steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
3759
+ whyManual: "Export removal may affect external consumers not in scope"
3760
+ };
3761
+ }
3762
+ function unusedImportSuggestion(imp) {
3763
+ const plural = imp.specifiers.length > 1;
3764
+ return {
3765
+ type: "delete",
3766
+ priority: "medium",
3767
+ source: "dead-code",
3768
+ relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
3769
+ title: `Remove unused import${plural ? "s" : ""}: ${imp.specifiers.join(", ")}`,
3770
+ description: `The import${plural ? "s" : ""} from "${imp.source}" ${plural ? "are" : "is"} not used.`,
3771
+ files: [imp.file],
3772
+ steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
3773
+ whyManual: "Import removal can be auto-fixed"
3774
+ };
3775
+ }
3671
3776
  function generateDeadCodeSuggestions(report) {
3672
- const suggestions = [];
3673
- for (const file of report.deadFiles) {
3674
- suggestions.push({
3675
- type: "delete",
3676
- priority: "high",
3677
- source: "dead-code",
3678
- relatedIssues: [`dead-file:${file.path}`],
3679
- title: `Remove dead file: ${file.path.split("/").pop()}`,
3680
- description: `This file is not imported by any other file and can be safely removed.`,
3681
- files: [file.path],
3682
- steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
3683
- whyManual: "File deletion requires verification that no dynamic imports exist"
3684
- });
3685
- }
3686
- for (const exp of report.deadExports) {
3687
- suggestions.push({
3688
- type: "refactor",
3689
- priority: "medium",
3690
- source: "dead-code",
3691
- relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
3692
- title: `Remove unused export: ${exp.name}`,
3693
- description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
3694
- files: [exp.file],
3695
- steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
3696
- whyManual: "Export removal may affect external consumers not in scope"
3697
- });
3698
- }
3699
- for (const imp of report.unusedImports) {
3700
- suggestions.push({
3701
- type: "delete",
3702
- priority: "medium",
3703
- source: "dead-code",
3704
- relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
3705
- title: `Remove unused import${imp.specifiers.length > 1 ? "s" : ""}: ${imp.specifiers.join(", ")}`,
3706
- description: `The import${imp.specifiers.length > 1 ? "s" : ""} from "${imp.source}" ${imp.specifiers.length > 1 ? "are" : "is"} not used.`,
3707
- files: [imp.file],
3708
- steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
3709
- whyManual: "Import removal can be auto-fixed"
3710
- });
3711
- }
3712
- return suggestions;
3777
+ return [
3778
+ ...report.deadFiles.map(deadFileSuggestion),
3779
+ ...report.deadExports.map(deadExportSuggestion),
3780
+ ...report.unusedImports.map(unusedImportSuggestion)
3781
+ ];
3713
3782
  }
3714
3783
  function generateDriftSuggestions(report) {
3715
3784
  const suggestions = [];
@@ -4152,43 +4221,55 @@ async function createBackup(filePath, backupDir) {
4152
4221
  );
4153
4222
  }
4154
4223
  }
4155
- async function applySingleFix(fix, config) {
4224
+ async function applyDeleteFile(fix, config) {
4225
+ if (config.createBackup && config.backupDir) {
4226
+ const backupResult = await createBackup(fix.file, config.backupDir);
4227
+ if (!backupResult.ok) return (0, import_types.Err)({ fix, error: backupResult.error.message });
4228
+ }
4229
+ await unlink2(fix.file);
4230
+ return (0, import_types.Ok)(void 0);
4231
+ }
4232
+ async function applyDeleteLines(fix) {
4233
+ if (fix.line !== void 0) {
4234
+ const content = await readFile5(fix.file, "utf-8");
4235
+ const lines = content.split("\n");
4236
+ lines.splice(fix.line - 1, 1);
4237
+ await writeFile3(fix.file, lines.join("\n"));
4238
+ }
4239
+ }
4240
+ async function applyReplace(fix) {
4241
+ if (fix.oldContent && fix.newContent !== void 0) {
4242
+ const content = await readFile5(fix.file, "utf-8");
4243
+ await writeFile3(fix.file, content.replace(fix.oldContent, fix.newContent));
4244
+ }
4245
+ }
4246
+ async function applyInsert(fix) {
4247
+ if (fix.line !== void 0 && fix.newContent) {
4248
+ const content = await readFile5(fix.file, "utf-8");
4249
+ const lines = content.split("\n");
4250
+ lines.splice(fix.line - 1, 0, fix.newContent);
4251
+ await writeFile3(fix.file, lines.join("\n"));
4252
+ }
4253
+ }
4254
+ async function applySingleFix(fix, config) {
4156
4255
  if (config.dryRun) {
4157
4256
  return (0, import_types.Ok)(fix);
4158
4257
  }
4159
4258
  try {
4160
4259
  switch (fix.action) {
4161
- case "delete-file":
4162
- if (config.createBackup && config.backupDir) {
4163
- const backupResult = await createBackup(fix.file, config.backupDir);
4164
- if (!backupResult.ok) {
4165
- return (0, import_types.Err)({ fix, error: backupResult.error.message });
4166
- }
4167
- }
4168
- await unlink2(fix.file);
4260
+ case "delete-file": {
4261
+ const result = await applyDeleteFile(fix, config);
4262
+ if (!result.ok) return result;
4169
4263
  break;
4264
+ }
4170
4265
  case "delete-lines":
4171
- if (fix.line !== void 0) {
4172
- const content = await readFile5(fix.file, "utf-8");
4173
- const lines = content.split("\n");
4174
- lines.splice(fix.line - 1, 1);
4175
- await writeFile3(fix.file, lines.join("\n"));
4176
- }
4266
+ await applyDeleteLines(fix);
4177
4267
  break;
4178
4268
  case "replace":
4179
- if (fix.oldContent && fix.newContent !== void 0) {
4180
- const content = await readFile5(fix.file, "utf-8");
4181
- const newContent = content.replace(fix.oldContent, fix.newContent);
4182
- await writeFile3(fix.file, newContent);
4183
- }
4269
+ await applyReplace(fix);
4184
4270
  break;
4185
4271
  case "insert":
4186
- if (fix.line !== void 0 && fix.newContent) {
4187
- const content = await readFile5(fix.file, "utf-8");
4188
- const lines = content.split("\n");
4189
- lines.splice(fix.line - 1, 0, fix.newContent);
4190
- await writeFile3(fix.file, lines.join("\n"));
4191
- }
4272
+ await applyInsert(fix);
4192
4273
  break;
4193
4274
  }
4194
4275
  return (0, import_types.Ok)(fix);
@@ -4361,6 +4442,21 @@ function applyHotspotDowngrade(finding, hotspot) {
4361
4442
  }
4362
4443
  return finding;
4363
4444
  }
4445
+ function mergeGroup(group) {
4446
+ if (group.length === 1) return [group[0]];
4447
+ const deadCode = group.find((f) => f.concern === "dead-code");
4448
+ const arch = group.find((f) => f.concern === "architecture");
4449
+ if (deadCode && arch) {
4450
+ return [
4451
+ {
4452
+ ...deadCode,
4453
+ description: `${deadCode.description} (also violates architecture: ${arch.type})`,
4454
+ suggestion: deadCode.fixAction ? `${deadCode.fixAction} (resolves both dead code and architecture violation)` : deadCode.suggestion
4455
+ }
4456
+ ];
4457
+ }
4458
+ return group;
4459
+ }
4364
4460
  function deduplicateCleanupFindings(findings) {
4365
4461
  const byFileAndLine = /* @__PURE__ */ new Map();
4366
4462
  for (const f of findings) {
@@ -4371,21 +4467,7 @@ function deduplicateCleanupFindings(findings) {
4371
4467
  }
4372
4468
  const result = [];
4373
4469
  for (const group of byFileAndLine.values()) {
4374
- if (group.length === 1) {
4375
- result.push(group[0]);
4376
- continue;
4377
- }
4378
- const deadCode = group.find((f) => f.concern === "dead-code");
4379
- const arch = group.find((f) => f.concern === "architecture");
4380
- if (deadCode && arch) {
4381
- result.push({
4382
- ...deadCode,
4383
- description: `${deadCode.description} (also violates architecture: ${arch.type})`,
4384
- suggestion: deadCode.fixAction ? `${deadCode.fixAction} (resolves both dead code and architecture violation)` : deadCode.suggestion
4385
- });
4386
- } else {
4387
- result.push(...group);
4388
- }
4470
+ result.push(...mergeGroup(group));
4389
4471
  }
4390
4472
  return result;
4391
4473
  }
@@ -4758,6 +4840,32 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
4758
4840
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
4759
4841
  var FUNCTION_DECL_RE = /(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
4760
4842
  var CONST_DECL_RE = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=/;
4843
+ function mergeGraphInferred(highFanInFunctions, seen) {
4844
+ let added = 0;
4845
+ for (const item of highFanInFunctions) {
4846
+ const key = `${item.file}::${item.function}`;
4847
+ if (!seen.has(key)) {
4848
+ seen.set(key, {
4849
+ file: item.file,
4850
+ function: item.function,
4851
+ source: "graph-inferred",
4852
+ fanIn: item.fanIn
4853
+ });
4854
+ added++;
4855
+ }
4856
+ }
4857
+ return added;
4858
+ }
4859
+ function isCommentOrBlank(line) {
4860
+ return line === "" || line === "*/" || line === "*" || line.startsWith("*") || line.startsWith("//");
4861
+ }
4862
+ function matchDeclarationName(line) {
4863
+ const funcMatch = line.match(FUNCTION_DECL_RE);
4864
+ if (funcMatch?.[1]) return funcMatch[1];
4865
+ const constMatch = line.match(CONST_DECL_RE);
4866
+ if (constMatch?.[1]) return constMatch[1];
4867
+ return null;
4868
+ }
4761
4869
  var CriticalPathResolver = class {
4762
4870
  projectRoot;
4763
4871
  constructor(projectRoot) {
@@ -4770,27 +4878,12 @@ var CriticalPathResolver = class {
4770
4878
  const key = `${entry.file}::${entry.function}`;
4771
4879
  seen.set(key, entry);
4772
4880
  }
4773
- let graphInferred = 0;
4774
- if (graphData) {
4775
- for (const item of graphData.highFanInFunctions) {
4776
- const key = `${item.file}::${item.function}`;
4777
- if (!seen.has(key)) {
4778
- seen.set(key, {
4779
- file: item.file,
4780
- function: item.function,
4781
- source: "graph-inferred",
4782
- fanIn: item.fanIn
4783
- });
4784
- graphInferred++;
4785
- }
4786
- }
4787
- }
4881
+ const graphInferred = graphData ? mergeGraphInferred(graphData.highFanInFunctions, seen) : 0;
4788
4882
  const entries = Array.from(seen.values());
4789
- const annotatedCount = annotated.length;
4790
4883
  return {
4791
4884
  entries,
4792
4885
  stats: {
4793
- annotated: annotatedCount,
4886
+ annotated: annotated.length,
4794
4887
  graphInferred,
4795
4888
  total: entries.length
4796
4889
  }
@@ -4817,6 +4910,14 @@ var CriticalPathResolver = class {
4817
4910
  }
4818
4911
  }
4819
4912
  }
4913
+ resolveFunctionName(lines, fromIndex) {
4914
+ for (let j = fromIndex; j < lines.length; j++) {
4915
+ const nextLine = lines[j].trim();
4916
+ if (isCommentOrBlank(nextLine)) continue;
4917
+ return matchDeclarationName(nextLine);
4918
+ }
4919
+ return null;
4920
+ }
4820
4921
  scanFile(filePath, entries) {
4821
4922
  let content;
4822
4923
  try {
@@ -4827,30 +4928,10 @@ var CriticalPathResolver = class {
4827
4928
  const lines = content.split("\n");
4828
4929
  const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, "/");
4829
4930
  for (let i = 0; i < lines.length; i++) {
4830
- const line = lines[i];
4831
- if (!line.includes("@perf-critical")) continue;
4832
- for (let j = i + 1; j < lines.length; j++) {
4833
- const nextLine = lines[j].trim();
4834
- if (nextLine === "" || nextLine === "*/" || nextLine === "*") continue;
4835
- if (nextLine.startsWith("*") || nextLine.startsWith("//")) continue;
4836
- const funcMatch = nextLine.match(FUNCTION_DECL_RE);
4837
- if (funcMatch && funcMatch[1]) {
4838
- entries.push({
4839
- file: relativePath,
4840
- function: funcMatch[1],
4841
- source: "annotation"
4842
- });
4843
- } else {
4844
- const constMatch = nextLine.match(CONST_DECL_RE);
4845
- if (constMatch && constMatch[1]) {
4846
- entries.push({
4847
- file: relativePath,
4848
- function: constMatch[1],
4849
- source: "annotation"
4850
- });
4851
- }
4852
- }
4853
- break;
4931
+ if (!lines[i].includes("@perf-critical")) continue;
4932
+ const fnName = this.resolveFunctionName(lines, i + 1);
4933
+ if (fnName) {
4934
+ entries.push({ file: relativePath, function: fnName, source: "annotation" });
4854
4935
  }
4855
4936
  }
4856
4937
  }
@@ -5005,14 +5086,19 @@ function detectFileStatus(part) {
5005
5086
  if (part.includes("rename from")) return "renamed";
5006
5087
  return "modified";
5007
5088
  }
5008
- function parseDiffPart(part) {
5089
+ function parseDiffHeader(part) {
5009
5090
  if (!part.trim()) return null;
5010
5091
  const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
5011
5092
  if (!headerMatch || !headerMatch[2]) return null;
5093
+ return headerMatch[2];
5094
+ }
5095
+ function parseDiffPart(part) {
5096
+ const path34 = parseDiffHeader(part);
5097
+ if (!path34) return null;
5012
5098
  const additionRegex = /^\+(?!\+\+)/gm;
5013
5099
  const deletionRegex = /^-(?!--)/gm;
5014
5100
  return {
5015
- path: headerMatch[2],
5101
+ path: path34,
5016
5102
  status: detectFileStatus(part),
5017
5103
  additions: (part.match(additionRegex) || []).length,
5018
5104
  deletions: (part.match(deletionRegex) || []).length
@@ -5034,100 +5120,136 @@ function parseDiff(diff2) {
5034
5120
  });
5035
5121
  }
5036
5122
  }
5037
- async function analyzeDiff(changes, options, graphImpactData) {
5038
- if (!options?.enabled) {
5039
- return (0, import_types.Ok)([]);
5123
+ function checkForbiddenPatterns(diff2, forbiddenPatterns, nextId) {
5124
+ const items = [];
5125
+ if (!forbiddenPatterns) return items;
5126
+ for (const forbidden of forbiddenPatterns) {
5127
+ const pattern = typeof forbidden.pattern === "string" ? new RegExp(forbidden.pattern, "g") : forbidden.pattern;
5128
+ if (!pattern.test(diff2)) continue;
5129
+ items.push({
5130
+ id: nextId(),
5131
+ category: "diff",
5132
+ check: `Forbidden pattern: ${forbidden.pattern}`,
5133
+ passed: false,
5134
+ severity: forbidden.severity,
5135
+ details: forbidden.message,
5136
+ suggestion: `Remove occurrences of ${forbidden.pattern}`
5137
+ });
5040
5138
  }
5139
+ return items;
5140
+ }
5141
+ function checkMaxChangedFiles(files, maxChangedFiles, nextId) {
5142
+ if (!maxChangedFiles || files.length <= maxChangedFiles) return [];
5143
+ return [
5144
+ {
5145
+ id: nextId(),
5146
+ category: "diff",
5147
+ check: `PR size: ${files.length} files changed`,
5148
+ passed: false,
5149
+ severity: "warning",
5150
+ details: `This PR changes ${files.length} files, which exceeds the recommended maximum of ${maxChangedFiles}`,
5151
+ suggestion: "Consider breaking this into smaller PRs"
5152
+ }
5153
+ ];
5154
+ }
5155
+ function checkFileSizes(files, maxFileSize, nextId) {
5041
5156
  const items = [];
5042
- let itemId = 0;
5043
- if (options.forbiddenPatterns) {
5044
- for (const forbidden of options.forbiddenPatterns) {
5045
- const pattern = typeof forbidden.pattern === "string" ? new RegExp(forbidden.pattern, "g") : forbidden.pattern;
5046
- if (pattern.test(changes.diff)) {
5047
- items.push({
5048
- id: `diff-${++itemId}`,
5049
- category: "diff",
5050
- check: `Forbidden pattern: ${forbidden.pattern}`,
5051
- passed: false,
5052
- severity: forbidden.severity,
5053
- details: forbidden.message,
5054
- suggestion: `Remove occurrences of ${forbidden.pattern}`
5055
- });
5056
- }
5157
+ if (!maxFileSize) return items;
5158
+ for (const file of files) {
5159
+ const totalLines = file.additions + file.deletions;
5160
+ if (totalLines <= maxFileSize) continue;
5161
+ items.push({
5162
+ id: nextId(),
5163
+ category: "diff",
5164
+ check: `File size: ${file.path}`,
5165
+ passed: false,
5166
+ severity: "warning",
5167
+ details: `File has ${totalLines} lines changed, exceeding limit of ${maxFileSize}`,
5168
+ file: file.path,
5169
+ suggestion: "Consider splitting this file into smaller modules"
5170
+ });
5171
+ }
5172
+ return items;
5173
+ }
5174
+ function checkTestCoverageGraph(files, graphImpactData) {
5175
+ const items = [];
5176
+ for (const file of files) {
5177
+ if (file.status !== "added" || !file.path.endsWith(".ts") || file.path.includes(".test.")) {
5178
+ continue;
5057
5179
  }
5180
+ const hasGraphTest = graphImpactData.affectedTests.some((t) => t.coversFile === file.path);
5181
+ if (hasGraphTest) continue;
5182
+ items.push({
5183
+ id: `test-coverage-${file.path}`,
5184
+ category: "diff",
5185
+ check: "Test coverage (graph)",
5186
+ passed: false,
5187
+ severity: "warning",
5188
+ details: `New file ${file.path} has no test file linked in the graph`,
5189
+ file: file.path
5190
+ });
5058
5191
  }
5059
- if (options.maxChangedFiles && changes.files.length > options.maxChangedFiles) {
5192
+ return items;
5193
+ }
5194
+ function checkTestCoverageFilename(files, nextId) {
5195
+ const items = [];
5196
+ const addedSourceFiles = files.filter(
5197
+ (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
5198
+ );
5199
+ const testFiles = files.filter((f) => f.path.includes(".test."));
5200
+ for (const sourceFile of addedSourceFiles) {
5201
+ const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
5202
+ const hasTest = testFiles.some(
5203
+ (t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
5204
+ );
5205
+ if (hasTest) continue;
5060
5206
  items.push({
5061
- id: `diff-${++itemId}`,
5207
+ id: nextId(),
5062
5208
  category: "diff",
5063
- check: `PR size: ${changes.files.length} files changed`,
5209
+ check: `Test coverage: ${sourceFile.path}`,
5064
5210
  passed: false,
5065
5211
  severity: "warning",
5066
- details: `This PR changes ${changes.files.length} files, which exceeds the recommended maximum of ${options.maxChangedFiles}`,
5067
- suggestion: "Consider breaking this into smaller PRs"
5212
+ details: "New source file added without corresponding test file",
5213
+ file: sourceFile.path,
5214
+ suggestion: `Add tests in ${expectedTestPath}`
5068
5215
  });
5069
5216
  }
5070
- if (options.maxFileSize) {
5071
- for (const file of changes.files) {
5072
- const totalLines = file.additions + file.deletions;
5073
- if (totalLines > options.maxFileSize) {
5074
- items.push({
5075
- id: `diff-${++itemId}`,
5076
- category: "diff",
5077
- check: `File size: ${file.path}`,
5078
- passed: false,
5079
- severity: "warning",
5080
- details: `File has ${totalLines} lines changed, exceeding limit of ${options.maxFileSize}`,
5081
- file: file.path,
5082
- suggestion: "Consider splitting this file into smaller modules"
5083
- });
5084
- }
5217
+ return items;
5218
+ }
5219
+ function checkDocCoverage2(files, graphImpactData) {
5220
+ const items = [];
5221
+ for (const file of files) {
5222
+ if (file.status !== "modified" || !file.path.endsWith(".ts") || file.path.includes(".test.")) {
5223
+ continue;
5085
5224
  }
5225
+ const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
5226
+ if (hasDoc) continue;
5227
+ items.push({
5228
+ id: `doc-coverage-${file.path}`,
5229
+ category: "diff",
5230
+ check: "Documentation coverage (graph)",
5231
+ passed: true,
5232
+ severity: "info",
5233
+ details: `Modified file ${file.path} has no documentation linked in the graph`,
5234
+ file: file.path
5235
+ });
5236
+ }
5237
+ return items;
5238
+ }
5239
+ async function analyzeDiff(changes, options, graphImpactData) {
5240
+ if (!options?.enabled) {
5241
+ return (0, import_types.Ok)([]);
5086
5242
  }
5243
+ let itemId = 0;
5244
+ const nextId = () => `diff-${++itemId}`;
5245
+ const items = [
5246
+ ...checkForbiddenPatterns(changes.diff, options.forbiddenPatterns, nextId),
5247
+ ...checkMaxChangedFiles(changes.files, options.maxChangedFiles, nextId),
5248
+ ...checkFileSizes(changes.files, options.maxFileSize, nextId)
5249
+ ];
5087
5250
  if (options.checkTestCoverage) {
5088
- if (graphImpactData) {
5089
- for (const file of changes.files) {
5090
- if (file.status === "added" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
5091
- const hasGraphTest = graphImpactData.affectedTests.some(
5092
- (t) => t.coversFile === file.path
5093
- );
5094
- if (!hasGraphTest) {
5095
- items.push({
5096
- id: `test-coverage-${file.path}`,
5097
- category: "diff",
5098
- check: "Test coverage (graph)",
5099
- passed: false,
5100
- severity: "warning",
5101
- details: `New file ${file.path} has no test file linked in the graph`,
5102
- file: file.path
5103
- });
5104
- }
5105
- }
5106
- }
5107
- } else {
5108
- const addedSourceFiles = changes.files.filter(
5109
- (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
5110
- );
5111
- const testFiles = changes.files.filter((f) => f.path.includes(".test."));
5112
- for (const sourceFile of addedSourceFiles) {
5113
- const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
5114
- const hasTest = testFiles.some(
5115
- (t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
5116
- );
5117
- if (!hasTest) {
5118
- items.push({
5119
- id: `diff-${++itemId}`,
5120
- category: "diff",
5121
- check: `Test coverage: ${sourceFile.path}`,
5122
- passed: false,
5123
- severity: "warning",
5124
- details: "New source file added without corresponding test file",
5125
- file: sourceFile.path,
5126
- suggestion: `Add tests in ${expectedTestPath}`
5127
- });
5128
- }
5129
- }
5130
- }
5251
+ const coverageItems = graphImpactData ? checkTestCoverageGraph(changes.files, graphImpactData) : checkTestCoverageFilename(changes.files, nextId);
5252
+ items.push(...coverageItems);
5131
5253
  }
5132
5254
  if (graphImpactData && graphImpactData.impactScope > 20) {
5133
5255
  items.push({
@@ -5140,22 +5262,7 @@ async function analyzeDiff(changes, options, graphImpactData) {
5140
5262
  });
5141
5263
  }
5142
5264
  if (graphImpactData) {
5143
- for (const file of changes.files) {
5144
- if (file.status === "modified" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
5145
- const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
5146
- if (!hasDoc) {
5147
- items.push({
5148
- id: `doc-coverage-${file.path}`,
5149
- category: "diff",
5150
- check: "Documentation coverage (graph)",
5151
- passed: true,
5152
- severity: "info",
5153
- details: `Modified file ${file.path} has no documentation linked in the graph`,
5154
- file: file.path
5155
- });
5156
- }
5157
- }
5158
- }
5265
+ items.push(...checkDocCoverage2(changes.files, graphImpactData));
5159
5266
  }
5160
5267
  return (0, import_types.Ok)(items);
5161
5268
  }
@@ -5739,6 +5846,28 @@ function constraintRuleId(category, scope, description) {
5739
5846
  }
5740
5847
 
5741
5848
  // src/architecture/collectors/circular-deps.ts
5849
+ function makeStubParser() {
5850
+ return {
5851
+ name: "typescript",
5852
+ extensions: [".ts", ".tsx"],
5853
+ parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
5854
+ extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
5855
+ extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
5856
+ health: async () => ({ ok: true, value: { available: true } })
5857
+ };
5858
+ }
5859
+ function mapCycleViolations(cycles, rootDir, category) {
5860
+ return cycles.map((cycle) => {
5861
+ const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
5862
+ const firstFile = relativePosix(rootDir, cycle.cycle[0]);
5863
+ return {
5864
+ id: violationId(firstFile, category, cyclePath),
5865
+ file: firstFile,
5866
+ detail: `Circular dependency: ${cyclePath}`,
5867
+ severity: cycle.severity
5868
+ };
5869
+ });
5870
+ }
5742
5871
  var CircularDepsCollector = class {
5743
5872
  category = "circular-deps";
5744
5873
  getRules(_config, _rootDir) {
@@ -5754,21 +5883,7 @@ var CircularDepsCollector = class {
5754
5883
  }
5755
5884
  async collect(_config, rootDir) {
5756
5885
  const files = await findFiles("**/*.ts", rootDir);
5757
- const stubParser = {
5758
- name: "typescript",
5759
- extensions: [".ts", ".tsx"],
5760
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
5761
- extractImports: () => ({
5762
- ok: false,
5763
- error: { code: "EXTRACT_ERROR", message: "not needed" }
5764
- }),
5765
- extractExports: () => ({
5766
- ok: false,
5767
- error: { code: "EXTRACT_ERROR", message: "not needed" }
5768
- }),
5769
- health: async () => ({ ok: true, value: { available: true } })
5770
- };
5771
- const graphResult = await buildDependencyGraph(files, stubParser);
5886
+ const graphResult = await buildDependencyGraph(files, makeStubParser());
5772
5887
  if (!graphResult.ok) {
5773
5888
  return [
5774
5889
  {
@@ -5793,16 +5908,7 @@ var CircularDepsCollector = class {
5793
5908
  ];
5794
5909
  }
5795
5910
  const { cycles, largestCycle } = result.value;
5796
- const violations = cycles.map((cycle) => {
5797
- const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
5798
- const firstFile = relativePosix(rootDir, cycle.cycle[0]);
5799
- return {
5800
- id: violationId(firstFile, this.category, cyclePath),
5801
- file: firstFile,
5802
- detail: `Circular dependency: ${cyclePath}`,
5803
- severity: cycle.severity
5804
- };
5805
- });
5911
+ const violations = mapCycleViolations(cycles, rootDir, this.category);
5806
5912
  return [
5807
5913
  {
5808
5914
  category: this.category,
@@ -5816,6 +5922,30 @@ var CircularDepsCollector = class {
5816
5922
  };
5817
5923
 
5818
5924
  // src/architecture/collectors/layer-violations.ts
5925
+ function makeLayerStubParser() {
5926
+ return {
5927
+ name: "typescript",
5928
+ extensions: [".ts", ".tsx"],
5929
+ parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
5930
+ extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
5931
+ extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
5932
+ health: async () => ({ ok: true, value: { available: true } })
5933
+ };
5934
+ }
5935
+ function mapLayerViolations(layerViolations, rootDir, category) {
5936
+ return layerViolations.map((v) => {
5937
+ const relFile = relativePosix(rootDir, v.file);
5938
+ const relImport = relativePosix(rootDir, v.imports);
5939
+ const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
5940
+ return {
5941
+ id: violationId(relFile, category ?? "", detail),
5942
+ file: relFile,
5943
+ category,
5944
+ detail,
5945
+ severity: "error"
5946
+ };
5947
+ });
5948
+ }
5819
5949
  var LayerViolationCollector = class {
5820
5950
  category = "layer-violations";
5821
5951
  getRules(_config, _rootDir) {
@@ -5830,18 +5960,10 @@ var LayerViolationCollector = class {
5830
5960
  ];
5831
5961
  }
5832
5962
  async collect(_config, rootDir) {
5833
- const stubParser = {
5834
- name: "typescript",
5835
- extensions: [".ts", ".tsx"],
5836
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
5837
- extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
5838
- extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
5839
- health: async () => ({ ok: true, value: { available: true } })
5840
- };
5841
5963
  const result = await validateDependencies({
5842
5964
  layers: [],
5843
5965
  rootDir,
5844
- parser: stubParser,
5966
+ parser: makeLayerStubParser(),
5845
5967
  fallbackBehavior: "skip"
5846
5968
  });
5847
5969
  if (!result.ok) {
@@ -5855,33 +5977,52 @@ var LayerViolationCollector = class {
5855
5977
  }
5856
5978
  ];
5857
5979
  }
5858
- const layerViolations = result.value.violations.filter(
5859
- (v) => v.reason === "WRONG_LAYER"
5980
+ const violations = mapLayerViolations(
5981
+ result.value.violations.filter((v) => v.reason === "WRONG_LAYER"),
5982
+ rootDir,
5983
+ this.category
5860
5984
  );
5861
- const violations = layerViolations.map((v) => {
5862
- const relFile = relativePosix(rootDir, v.file);
5863
- const relImport = relativePosix(rootDir, v.imports);
5864
- const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
5865
- return {
5866
- id: violationId(relFile, this.category, detail),
5867
- file: relFile,
5868
- category: this.category,
5869
- detail,
5870
- severity: "error"
5871
- };
5872
- });
5873
- return [
5874
- {
5875
- category: this.category,
5876
- scope: "project",
5877
- value: violations.length,
5878
- violations
5879
- }
5880
- ];
5985
+ return [{ category: this.category, scope: "project", value: violations.length, violations }];
5881
5986
  }
5882
5987
  };
5883
5988
 
5884
5989
  // src/architecture/collectors/complexity.ts
5990
+ function buildSnapshot2(files, rootDir) {
5991
+ return {
5992
+ files: files.map((f) => ({
5993
+ path: f,
5994
+ ast: { type: "Program", body: null, language: "typescript" },
5995
+ imports: [],
5996
+ exports: [],
5997
+ internalSymbols: [],
5998
+ jsDocComments: []
5999
+ })),
6000
+ dependencyGraph: { nodes: [], edges: [] },
6001
+ exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
6002
+ docs: [],
6003
+ codeReferences: [],
6004
+ entryPoints: [],
6005
+ rootDir,
6006
+ config: { rootDir, analyze: {} },
6007
+ buildTime: 0
6008
+ };
6009
+ }
6010
+ function resolveMaxComplexity(config) {
6011
+ const threshold = config.thresholds.complexity;
6012
+ return typeof threshold === "number" ? threshold : threshold?.max ?? 15;
6013
+ }
6014
+ function mapComplexityViolations(complexityViolations, rootDir, category) {
6015
+ return complexityViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
6016
+ const relFile = relativePosix(rootDir, v.file);
6017
+ return {
6018
+ id: violationId(relFile, category ?? "", `${v.metric}:${v.function}`),
6019
+ file: relFile,
6020
+ category,
6021
+ detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
6022
+ severity: v.severity
6023
+ };
6024
+ });
6025
+ }
5885
6026
  var ComplexityCollector = class {
5886
6027
  category = "complexity";
5887
6028
  getRules(_config, _rootDir) {
@@ -5897,32 +6038,11 @@ var ComplexityCollector = class {
5897
6038
  }
5898
6039
  async collect(_config, rootDir) {
5899
6040
  const files = await findFiles("**/*.ts", rootDir);
5900
- const snapshot = {
5901
- files: files.map((f) => ({
5902
- path: f,
5903
- ast: { type: "Program", body: null, language: "typescript" },
5904
- imports: [],
5905
- exports: [],
5906
- internalSymbols: [],
5907
- jsDocComments: []
5908
- })),
5909
- dependencyGraph: { nodes: [], edges: [] },
5910
- exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
5911
- docs: [],
5912
- codeReferences: [],
5913
- entryPoints: [],
5914
- rootDir,
5915
- config: { rootDir, analyze: {} },
5916
- buildTime: 0
5917
- };
5918
- const complexityThreshold = _config.thresholds.complexity;
5919
- const maxComplexity = typeof complexityThreshold === "number" ? complexityThreshold : complexityThreshold?.max ?? 15;
6041
+ const snapshot = buildSnapshot2(files, rootDir);
6042
+ const maxComplexity = resolveMaxComplexity(_config);
5920
6043
  const complexityConfig = {
5921
6044
  thresholds: {
5922
- cyclomaticComplexity: {
5923
- error: maxComplexity,
5924
- warn: Math.floor(maxComplexity * 0.7)
5925
- }
6045
+ cyclomaticComplexity: { error: maxComplexity, warn: Math.floor(maxComplexity * 0.7) }
5926
6046
  }
5927
6047
  };
5928
6048
  const result = await detectComplexityViolations(snapshot, complexityConfig);
@@ -5938,20 +6058,7 @@ var ComplexityCollector = class {
5938
6058
  ];
5939
6059
  }
5940
6060
  const { violations: complexityViolations, stats } = result.value;
5941
- const filtered = complexityViolations.filter(
5942
- (v) => v.severity === "error" || v.severity === "warning"
5943
- );
5944
- const violations = filtered.map((v) => {
5945
- const relFile = relativePosix(rootDir, v.file);
5946
- const idDetail = `${v.metric}:${v.function}`;
5947
- return {
5948
- id: violationId(relFile, this.category, idDetail),
5949
- file: relFile,
5950
- category: this.category,
5951
- detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
5952
- severity: v.severity
5953
- };
5954
- });
6061
+ const violations = mapComplexityViolations(complexityViolations, rootDir, this.category);
5955
6062
  return [
5956
6063
  {
5957
6064
  category: this.category,
@@ -5968,6 +6075,38 @@ var ComplexityCollector = class {
5968
6075
  };
5969
6076
 
5970
6077
  // src/architecture/collectors/coupling.ts
6078
+ function buildCouplingSnapshot(files, rootDir) {
6079
+ return {
6080
+ files: files.map((f) => ({
6081
+ path: f,
6082
+ ast: { type: "Program", body: null, language: "typescript" },
6083
+ imports: [],
6084
+ exports: [],
6085
+ internalSymbols: [],
6086
+ jsDocComments: []
6087
+ })),
6088
+ dependencyGraph: { nodes: [], edges: [] },
6089
+ exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
6090
+ docs: [],
6091
+ codeReferences: [],
6092
+ entryPoints: [],
6093
+ rootDir,
6094
+ config: { rootDir, analyze: {} },
6095
+ buildTime: 0
6096
+ };
6097
+ }
6098
+ function mapCouplingViolations(couplingViolations, rootDir, category) {
6099
+ return couplingViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
6100
+ const relFile = relativePosix(rootDir, v.file);
6101
+ return {
6102
+ id: violationId(relFile, category ?? "", v.metric),
6103
+ file: relFile,
6104
+ category,
6105
+ detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
6106
+ severity: v.severity
6107
+ };
6108
+ });
6109
+ }
5971
6110
  var CouplingCollector = class {
5972
6111
  category = "coupling";
5973
6112
  getRules(_config, _rootDir) {
@@ -5983,24 +6122,7 @@ var CouplingCollector = class {
5983
6122
  }
5984
6123
  async collect(_config, rootDir) {
5985
6124
  const files = await findFiles("**/*.ts", rootDir);
5986
- const snapshot = {
5987
- files: files.map((f) => ({
5988
- path: f,
5989
- ast: { type: "Program", body: null, language: "typescript" },
5990
- imports: [],
5991
- exports: [],
5992
- internalSymbols: [],
5993
- jsDocComments: []
5994
- })),
5995
- dependencyGraph: { nodes: [], edges: [] },
5996
- exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
5997
- docs: [],
5998
- codeReferences: [],
5999
- entryPoints: [],
6000
- rootDir,
6001
- config: { rootDir, analyze: {} },
6002
- buildTime: 0
6003
- };
6125
+ const snapshot = buildCouplingSnapshot(files, rootDir);
6004
6126
  const result = await detectCouplingViolations(snapshot);
6005
6127
  if (!result.ok) {
6006
6128
  return [
@@ -6014,20 +6136,7 @@ var CouplingCollector = class {
6014
6136
  ];
6015
6137
  }
6016
6138
  const { violations: couplingViolations, stats } = result.value;
6017
- const filtered = couplingViolations.filter(
6018
- (v) => v.severity === "error" || v.severity === "warning"
6019
- );
6020
- const violations = filtered.map((v) => {
6021
- const relFile = relativePosix(rootDir, v.file);
6022
- const idDetail = `${v.metric}`;
6023
- return {
6024
- id: violationId(relFile, this.category, idDetail),
6025
- file: relFile,
6026
- category: this.category,
6027
- detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
6028
- severity: v.severity
6029
- };
6030
- });
6139
+ const violations = mapCouplingViolations(couplingViolations, rootDir, this.category);
6031
6140
  return [
6032
6141
  {
6033
6142
  category: this.category,
@@ -6041,6 +6150,30 @@ var CouplingCollector = class {
6041
6150
  };
6042
6151
 
6043
6152
  // src/architecture/collectors/forbidden-imports.ts
6153
+ function makeForbiddenStubParser() {
6154
+ return {
6155
+ name: "typescript",
6156
+ extensions: [".ts", ".tsx"],
6157
+ parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
6158
+ extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
6159
+ extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
6160
+ health: async () => ({ ok: true, value: { available: true } })
6161
+ };
6162
+ }
6163
+ function mapForbiddenImportViolations(forbidden, rootDir, category) {
6164
+ return forbidden.map((v) => {
6165
+ const relFile = relativePosix(rootDir, v.file);
6166
+ const relImport = relativePosix(rootDir, v.imports);
6167
+ const detail = `forbidden import: ${relFile} -> ${relImport}`;
6168
+ return {
6169
+ id: violationId(relFile, category ?? "", detail),
6170
+ file: relFile,
6171
+ category,
6172
+ detail,
6173
+ severity: "error"
6174
+ };
6175
+ });
6176
+ }
6044
6177
  var ForbiddenImportCollector = class {
6045
6178
  category = "forbidden-imports";
6046
6179
  getRules(_config, _rootDir) {
@@ -6055,18 +6188,10 @@ var ForbiddenImportCollector = class {
6055
6188
  ];
6056
6189
  }
6057
6190
  async collect(_config, rootDir) {
6058
- const stubParser = {
6059
- name: "typescript",
6060
- extensions: [".ts", ".tsx"],
6061
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
6062
- extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
6063
- extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
6064
- health: async () => ({ ok: true, value: { available: true } })
6065
- };
6066
6191
  const result = await validateDependencies({
6067
6192
  layers: [],
6068
6193
  rootDir,
6069
- parser: stubParser,
6194
+ parser: makeForbiddenStubParser(),
6070
6195
  fallbackBehavior: "skip"
6071
6196
  });
6072
6197
  if (!result.ok) {
@@ -6080,91 +6205,94 @@ var ForbiddenImportCollector = class {
6080
6205
  }
6081
6206
  ];
6082
6207
  }
6083
- const forbidden = result.value.violations.filter(
6084
- (v) => v.reason === "FORBIDDEN_IMPORT"
6208
+ const violations = mapForbiddenImportViolations(
6209
+ result.value.violations.filter((v) => v.reason === "FORBIDDEN_IMPORT"),
6210
+ rootDir,
6211
+ this.category
6085
6212
  );
6086
- const violations = forbidden.map((v) => {
6087
- const relFile = relativePosix(rootDir, v.file);
6088
- const relImport = relativePosix(rootDir, v.imports);
6089
- const detail = `forbidden import: ${relFile} -> ${relImport}`;
6090
- return {
6091
- id: violationId(relFile, this.category, detail),
6092
- file: relFile,
6093
- category: this.category,
6094
- detail,
6095
- severity: "error"
6096
- };
6097
- });
6098
- return [
6099
- {
6100
- category: this.category,
6101
- scope: "project",
6102
- value: violations.length,
6103
- violations
6104
- }
6105
- ];
6213
+ return [{ category: this.category, scope: "project", value: violations.length, violations }];
6106
6214
  }
6107
6215
  };
6108
6216
 
6109
6217
  // src/architecture/collectors/module-size.ts
6110
6218
  var import_promises2 = require("fs/promises");
6111
6219
  var import_node_path4 = require("path");
6112
- async function discoverModules(rootDir) {
6113
- const modules = [];
6114
- async function scanDir(dir) {
6115
- let entries;
6116
- try {
6117
- entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
6118
- } catch {
6119
- return;
6120
- }
6121
- const tsFiles = [];
6122
- const subdirs = [];
6123
- for (const entry of entries) {
6124
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") {
6125
- continue;
6126
- }
6127
- const fullPath = (0, import_node_path4.join)(dir, entry.name);
6128
- if (entry.isDirectory()) {
6129
- subdirs.push(fullPath);
6130
- } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".test.tsx") && !entry.name.endsWith(".spec.ts")) {
6131
- tsFiles.push(fullPath);
6132
- }
6133
- }
6134
- if (tsFiles.length > 0) {
6135
- let totalLoc = 0;
6136
- for (const f of tsFiles) {
6137
- try {
6138
- const content = await (0, import_promises2.readFile)(f, "utf-8");
6139
- totalLoc += content.split("\n").filter((line) => line.trim().length > 0).length;
6140
- } catch {
6141
- }
6142
- }
6143
- modules.push({
6144
- modulePath: relativePosix(rootDir, dir),
6145
- fileCount: tsFiles.length,
6146
- totalLoc,
6147
- files: tsFiles.map((f) => relativePosix(rootDir, f))
6148
- });
6220
+ function isSkippedEntry(name) {
6221
+ return name.startsWith(".") || name === "node_modules" || name === "dist";
6222
+ }
6223
+ function isTsSourceFile(name) {
6224
+ if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
6225
+ if (name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".spec.ts"))
6226
+ return false;
6227
+ return true;
6228
+ }
6229
+ async function countLoc(filePath) {
6230
+ try {
6231
+ const content = await (0, import_promises2.readFile)(filePath, "utf-8");
6232
+ return content.split("\n").filter((line) => line.trim().length > 0).length;
6233
+ } catch {
6234
+ return 0;
6235
+ }
6236
+ }
6237
+ async function buildModuleStats(rootDir, dir, tsFiles) {
6238
+ let totalLoc = 0;
6239
+ for (const f of tsFiles) {
6240
+ totalLoc += await countLoc(f);
6241
+ }
6242
+ return {
6243
+ modulePath: relativePosix(rootDir, dir),
6244
+ fileCount: tsFiles.length,
6245
+ totalLoc,
6246
+ files: tsFiles.map((f) => relativePosix(rootDir, f))
6247
+ };
6248
+ }
6249
+ async function scanDir(rootDir, dir, modules) {
6250
+ let entries;
6251
+ try {
6252
+ entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
6253
+ } catch {
6254
+ return;
6255
+ }
6256
+ const tsFiles = [];
6257
+ const subdirs = [];
6258
+ for (const entry of entries) {
6259
+ if (isSkippedEntry(entry.name)) continue;
6260
+ const fullPath = (0, import_node_path4.join)(dir, entry.name);
6261
+ if (entry.isDirectory()) {
6262
+ subdirs.push(fullPath);
6263
+ continue;
6149
6264
  }
6150
- for (const sub of subdirs) {
6151
- await scanDir(sub);
6265
+ if (entry.isFile() && isTsSourceFile(entry.name)) {
6266
+ tsFiles.push(fullPath);
6152
6267
  }
6153
6268
  }
6154
- await scanDir(rootDir);
6269
+ if (tsFiles.length > 0) {
6270
+ modules.push(await buildModuleStats(rootDir, dir, tsFiles));
6271
+ }
6272
+ for (const sub of subdirs) {
6273
+ await scanDir(rootDir, sub, modules);
6274
+ }
6275
+ }
6276
+ async function discoverModules(rootDir) {
6277
+ const modules = [];
6278
+ await scanDir(rootDir, rootDir, modules);
6155
6279
  return modules;
6156
6280
  }
6281
+ function extractThresholds(config) {
6282
+ const thresholds = config.thresholds["module-size"];
6283
+ let maxLoc = Infinity;
6284
+ let maxFiles = Infinity;
6285
+ if (typeof thresholds === "object" && thresholds !== null) {
6286
+ const t = thresholds;
6287
+ if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
6288
+ if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
6289
+ }
6290
+ return { maxLoc, maxFiles };
6291
+ }
6157
6292
  var ModuleSizeCollector = class {
6158
6293
  category = "module-size";
6159
6294
  getRules(config, _rootDir) {
6160
- const thresholds = config.thresholds["module-size"];
6161
- let maxLoc = Infinity;
6162
- let maxFiles = Infinity;
6163
- if (typeof thresholds === "object" && thresholds !== null) {
6164
- const t = thresholds;
6165
- if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
6166
- if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
6167
- }
6295
+ const { maxLoc, maxFiles } = extractThresholds(config);
6168
6296
  const rules = [];
6169
6297
  if (maxLoc < Infinity) {
6170
6298
  const desc = `Module LOC must not exceed ${maxLoc}`;
@@ -6197,14 +6325,7 @@ var ModuleSizeCollector = class {
6197
6325
  }
6198
6326
  async collect(config, rootDir) {
6199
6327
  const modules = await discoverModules(rootDir);
6200
- const thresholds = config.thresholds["module-size"];
6201
- let maxLoc = Infinity;
6202
- let maxFiles = Infinity;
6203
- if (typeof thresholds === "object" && thresholds !== null) {
6204
- const t = thresholds;
6205
- if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
6206
- if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
6207
- }
6328
+ const { maxLoc, maxFiles } = extractThresholds(config);
6208
6329
  return modules.map((mod) => {
6209
6330
  const violations = [];
6210
6331
  if (mod.totalLoc > maxLoc) {
@@ -6254,27 +6375,33 @@ function extractImportSources(content, filePath) {
6254
6375
  }
6255
6376
  return sources;
6256
6377
  }
6257
- async function collectTsFiles(dir) {
6258
- const results = [];
6259
- async function scan(d) {
6260
- let entries;
6261
- try {
6262
- entries = await (0, import_promises3.readdir)(d, { withFileTypes: true });
6263
- } catch {
6264
- return;
6265
- }
6266
- for (const entry of entries) {
6267
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
6268
- continue;
6269
- const fullPath = (0, import_node_path5.join)(d, entry.name);
6270
- if (entry.isDirectory()) {
6271
- await scan(fullPath);
6272
- } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".test.tsx") && !entry.name.endsWith(".spec.ts")) {
6273
- results.push(fullPath);
6274
- }
6378
+ function isSkippedEntry2(name) {
6379
+ return name.startsWith(".") || name === "node_modules" || name === "dist";
6380
+ }
6381
+ function isTsSourceFile2(name) {
6382
+ if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
6383
+ return !name.endsWith(".test.ts") && !name.endsWith(".test.tsx") && !name.endsWith(".spec.ts");
6384
+ }
6385
+ async function scanDir2(d, results) {
6386
+ let entries;
6387
+ try {
6388
+ entries = await (0, import_promises3.readdir)(d, { withFileTypes: true });
6389
+ } catch {
6390
+ return;
6391
+ }
6392
+ for (const entry of entries) {
6393
+ if (isSkippedEntry2(entry.name)) continue;
6394
+ const fullPath = (0, import_node_path5.join)(d, entry.name);
6395
+ if (entry.isDirectory()) {
6396
+ await scanDir2(fullPath, results);
6397
+ } else if (entry.isFile() && isTsSourceFile2(entry.name)) {
6398
+ results.push(fullPath);
6275
6399
  }
6276
6400
  }
6277
- await scan(dir);
6401
+ }
6402
+ async function collectTsFiles(dir) {
6403
+ const results = [];
6404
+ await scanDir2(dir, results);
6278
6405
  return results;
6279
6406
  }
6280
6407
  function computeLongestChain(file, graph, visited, memo) {
@@ -6305,34 +6432,42 @@ var DepDepthCollector = class {
6305
6432
  }
6306
6433
  ];
6307
6434
  }
6308
- async collect(config, rootDir) {
6309
- const allFiles = await collectTsFiles(rootDir);
6435
+ async buildImportGraph(allFiles) {
6310
6436
  const graph = /* @__PURE__ */ new Map();
6311
6437
  const fileSet = new Set(allFiles);
6312
6438
  for (const file of allFiles) {
6313
6439
  try {
6314
6440
  const content = await (0, import_promises3.readFile)(file, "utf-8");
6315
- const imports = extractImportSources(content, file).filter((imp) => fileSet.has(imp));
6316
- graph.set(file, imports);
6441
+ graph.set(
6442
+ file,
6443
+ extractImportSources(content, file).filter((imp) => fileSet.has(imp))
6444
+ );
6317
6445
  } catch {
6318
6446
  graph.set(file, []);
6319
6447
  }
6320
6448
  }
6449
+ return graph;
6450
+ }
6451
+ buildModuleMap(allFiles, rootDir) {
6321
6452
  const moduleMap = /* @__PURE__ */ new Map();
6322
6453
  for (const file of allFiles) {
6323
6454
  const relDir = relativePosix(rootDir, (0, import_node_path5.dirname)(file));
6324
6455
  if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
6325
6456
  moduleMap.get(relDir).push(file);
6326
6457
  }
6458
+ return moduleMap;
6459
+ }
6460
+ async collect(config, rootDir) {
6461
+ const allFiles = await collectTsFiles(rootDir);
6462
+ const graph = await this.buildImportGraph(allFiles);
6463
+ const moduleMap = this.buildModuleMap(allFiles, rootDir);
6327
6464
  const memo = /* @__PURE__ */ new Map();
6328
6465
  const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : Infinity;
6329
6466
  const results = [];
6330
6467
  for (const [modulePath, files] of moduleMap) {
6331
- let longestChain = 0;
6332
- for (const file of files) {
6333
- const depth = computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo);
6334
- if (depth > longestChain) longestChain = depth;
6335
- }
6468
+ const longestChain = files.reduce((max, file) => {
6469
+ return Math.max(max, computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo));
6470
+ }, 0);
6336
6471
  const violations = [];
6337
6472
  if (longestChain > threshold) {
6338
6473
  violations.push({
@@ -6429,10 +6564,26 @@ function hasMatchingViolation(rule, violationsByCategory) {
6429
6564
  }
6430
6565
 
6431
6566
  // src/architecture/detect-stale.ts
6567
+ function evaluateStaleNode(node, now, cutoff) {
6568
+ const lastViolatedAt = node.lastViolatedAt ?? null;
6569
+ const createdAt = node.createdAt;
6570
+ const comparisonTimestamp = lastViolatedAt ?? createdAt;
6571
+ if (!comparisonTimestamp) return null;
6572
+ const timestampMs = new Date(comparisonTimestamp).getTime();
6573
+ if (timestampMs >= cutoff) return null;
6574
+ const daysSince = Math.floor((now - timestampMs) / (24 * 60 * 60 * 1e3));
6575
+ return {
6576
+ id: node.id,
6577
+ category: node.category,
6578
+ description: node.name ?? "",
6579
+ scope: node.scope ?? "project",
6580
+ lastViolatedAt,
6581
+ daysSinceLastViolation: daysSince
6582
+ };
6583
+ }
6432
6584
  function detectStaleConstraints(store, windowDays = 30, category) {
6433
6585
  const now = Date.now();
6434
- const windowMs = windowDays * 24 * 60 * 60 * 1e3;
6435
- const cutoff = now - windowMs;
6586
+ const cutoff = now - windowDays * 24 * 60 * 60 * 1e3;
6436
6587
  let constraints = store.findNodes({ type: "constraint" });
6437
6588
  if (category) {
6438
6589
  constraints = constraints.filter((n) => n.category === category);
@@ -6440,22 +6591,8 @@ function detectStaleConstraints(store, windowDays = 30, category) {
6440
6591
  const totalConstraints = constraints.length;
6441
6592
  const staleConstraints = [];
6442
6593
  for (const node of constraints) {
6443
- const lastViolatedAt = node.lastViolatedAt ?? null;
6444
- const createdAt = node.createdAt;
6445
- const comparisonTimestamp = lastViolatedAt ?? createdAt;
6446
- if (!comparisonTimestamp) continue;
6447
- const timestampMs = new Date(comparisonTimestamp).getTime();
6448
- if (timestampMs < cutoff) {
6449
- const daysSince = Math.floor((now - timestampMs) / (24 * 60 * 60 * 1e3));
6450
- staleConstraints.push({
6451
- id: node.id,
6452
- category: node.category,
6453
- description: node.name ?? "",
6454
- scope: node.scope ?? "project",
6455
- lastViolatedAt,
6456
- daysSinceLastViolation: daysSince
6457
- });
6458
- }
6594
+ const entry = evaluateStaleNode(node, now, cutoff);
6595
+ if (entry) staleConstraints.push(entry);
6459
6596
  }
6460
6597
  staleConstraints.sort((a, b) => b.daysSinceLastViolation - a.daysSinceLastViolation);
6461
6598
  return { staleConstraints, totalConstraints, windowDays };
@@ -6581,44 +6718,57 @@ function collectOrphanedBaselineViolations(baseline, visitedCategories) {
6581
6718
  }
6582
6719
  return resolved;
6583
6720
  }
6721
+ function diffCategory(category, agg, baselineCategory, acc) {
6722
+ const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
6723
+ const baselineValue = baselineCategory?.value ?? 0;
6724
+ const classified = classifyViolations(agg.violations, baselineViolationIds);
6725
+ acc.newViolations.push(...classified.newViolations);
6726
+ acc.preExisting.push(...classified.preExisting);
6727
+ const currentViolationIds = new Set(agg.violations.map((v) => v.id));
6728
+ acc.resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
6729
+ if (baselineCategory && agg.value > baselineValue) {
6730
+ acc.regressions.push({
6731
+ category,
6732
+ baselineValue,
6733
+ currentValue: agg.value,
6734
+ delta: agg.value - baselineValue
6735
+ });
6736
+ }
6737
+ }
6584
6738
  function diff(current, baseline) {
6585
6739
  const aggregated = aggregateByCategory(current);
6586
- const newViolations = [];
6587
- const resolvedViolations = [];
6588
- const preExisting = [];
6589
- const regressions = [];
6740
+ const acc = {
6741
+ newViolations: [],
6742
+ resolvedViolations: [],
6743
+ preExisting: [],
6744
+ regressions: []
6745
+ };
6590
6746
  const visitedCategories = /* @__PURE__ */ new Set();
6591
6747
  for (const [category, agg] of aggregated) {
6592
6748
  visitedCategories.add(category);
6593
- const baselineCategory = baseline.metrics[category];
6594
- const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
6595
- const baselineValue = baselineCategory?.value ?? 0;
6596
- const classified = classifyViolations(agg.violations, baselineViolationIds);
6597
- newViolations.push(...classified.newViolations);
6598
- preExisting.push(...classified.preExisting);
6599
- const currentViolationIds = new Set(agg.violations.map((v) => v.id));
6600
- resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
6601
- if (baselineCategory && agg.value > baselineValue) {
6602
- regressions.push({
6603
- category,
6604
- baselineValue,
6605
- currentValue: agg.value,
6606
- delta: agg.value - baselineValue
6607
- });
6608
- }
6749
+ diffCategory(category, agg, baseline.metrics[category], acc);
6609
6750
  }
6610
- resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
6751
+ acc.resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
6611
6752
  return {
6612
- passed: newViolations.length === 0 && regressions.length === 0,
6613
- newViolations,
6614
- resolvedViolations,
6615
- preExisting,
6616
- regressions
6753
+ passed: acc.newViolations.length === 0 && acc.regressions.length === 0,
6754
+ newViolations: acc.newViolations,
6755
+ resolvedViolations: acc.resolvedViolations,
6756
+ preExisting: acc.preExisting,
6757
+ regressions: acc.regressions
6617
6758
  };
6618
6759
  }
6619
-
6620
- // src/architecture/config.ts
6621
- function resolveThresholds2(scope, config) {
6760
+
6761
+ // src/architecture/config.ts
6762
+ function mergeThresholdCategory(projectValue2, moduleValue) {
6763
+ if (projectValue2 !== void 0 && typeof projectValue2 === "object" && !Array.isArray(projectValue2) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
6764
+ return {
6765
+ ...projectValue2,
6766
+ ...moduleValue
6767
+ };
6768
+ }
6769
+ return moduleValue;
6770
+ }
6771
+ function resolveThresholds3(scope, config) {
6622
6772
  const projectThresholds = {};
6623
6773
  for (const [key, val] of Object.entries(config.thresholds)) {
6624
6774
  projectThresholds[key] = typeof val === "object" && val !== null && !Array.isArray(val) ? { ...val } : val;
@@ -6633,14 +6783,7 @@ function resolveThresholds2(scope, config) {
6633
6783
  const merged = { ...projectThresholds };
6634
6784
  for (const [category, moduleValue] of Object.entries(moduleOverrides)) {
6635
6785
  const projectValue2 = projectThresholds[category];
6636
- if (projectValue2 !== void 0 && typeof projectValue2 === "object" && !Array.isArray(projectValue2) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
6637
- merged[category] = {
6638
- ...projectValue2,
6639
- ...moduleValue
6640
- };
6641
- } else {
6642
- merged[category] = moduleValue;
6643
- }
6786
+ merged[category] = mergeThresholdCategory(projectValue2, moduleValue);
6644
6787
  }
6645
6788
  return merged;
6646
6789
  }
@@ -7450,18 +7593,10 @@ var PredictionEngine = class {
7450
7593
  */
7451
7594
  predict(options) {
7452
7595
  const opts = this.resolveOptions(options);
7453
- const timeline = this.timelineManager.load();
7454
- const snapshots = timeline.snapshots;
7455
- if (snapshots.length < 3) {
7456
- throw new Error(
7457
- `PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
7458
- );
7459
- }
7596
+ const snapshots = this.loadValidatedSnapshots();
7460
7597
  const thresholds = this.resolveThresholds(opts);
7461
7598
  const categoriesToProcess = opts.categories ?? [...ALL_CATEGORIES2];
7462
- const firstDate = new Date(snapshots[0].capturedAt).getTime();
7463
- const lastSnapshot = snapshots[snapshots.length - 1];
7464
- const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
7599
+ const { firstDate, lastSnapshot, currentT } = this.computeTimeOffsets(snapshots);
7465
7600
  const baselines = this.computeBaselines(
7466
7601
  categoriesToProcess,
7467
7602
  thresholds,
@@ -7472,27 +7607,32 @@ var PredictionEngine = class {
7472
7607
  );
7473
7608
  const specImpacts = this.computeSpecImpacts(opts);
7474
7609
  const categories = this.computeAdjustedForecasts(baselines, thresholds, specImpacts, currentT);
7475
- const warnings = this.generateWarnings(
7476
- categories,
7477
- opts.horizon
7478
- );
7479
- const stabilityForecast = this.computeStabilityForecast(
7480
- categories,
7481
- thresholds,
7482
- snapshots
7483
- );
7610
+ const adjustedCategories = categories;
7484
7611
  return {
7485
7612
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7486
7613
  snapshotsUsed: snapshots.length,
7487
- timelineRange: {
7488
- from: snapshots[0].capturedAt,
7489
- to: lastSnapshot.capturedAt
7490
- },
7491
- stabilityForecast,
7492
- categories,
7493
- warnings
7614
+ timelineRange: { from: snapshots[0].capturedAt, to: lastSnapshot.capturedAt },
7615
+ stabilityForecast: this.computeStabilityForecast(adjustedCategories, thresholds, snapshots),
7616
+ categories: adjustedCategories,
7617
+ warnings: this.generateWarnings(adjustedCategories, opts.horizon)
7494
7618
  };
7495
7619
  }
7620
+ loadValidatedSnapshots() {
7621
+ const timeline = this.timelineManager.load();
7622
+ const snapshots = timeline.snapshots;
7623
+ if (snapshots.length < 3) {
7624
+ throw new Error(
7625
+ `PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
7626
+ );
7627
+ }
7628
+ return snapshots;
7629
+ }
7630
+ computeTimeOffsets(snapshots) {
7631
+ const firstDate = new Date(snapshots[0].capturedAt).getTime();
7632
+ const lastSnapshot = snapshots[snapshots.length - 1];
7633
+ const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
7634
+ return { firstDate, lastSnapshot, currentT };
7635
+ }
7496
7636
  // --- Private helpers ---
7497
7637
  resolveOptions(options) {
7498
7638
  return {
@@ -7659,31 +7799,40 @@ var PredictionEngine = class {
7659
7799
  for (const category of ALL_CATEGORIES2) {
7660
7800
  const af = categories[category];
7661
7801
  if (!af) continue;
7662
- const forecast = af.adjusted;
7663
- const crossing = forecast.thresholdCrossingWeeks;
7664
- if (crossing === null || crossing <= 0) continue;
7665
- let severity = null;
7666
- if (crossing <= criticalWindow && (forecast.confidence === "high" || forecast.confidence === "medium")) {
7667
- severity = "critical";
7668
- } else if (crossing <= warningWindow && (forecast.confidence === "high" || forecast.confidence === "medium")) {
7669
- severity = "warning";
7670
- } else if (crossing <= horizon) {
7671
- severity = "info";
7672
- }
7673
- if (severity) {
7674
- const contributingNames = af.contributingFeatures.map((f) => f.name);
7675
- warnings.push({
7676
- severity,
7677
- category,
7678
- message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
7679
- weeksUntil: crossing,
7680
- confidence: forecast.confidence,
7681
- contributingFeatures: contributingNames
7682
- });
7683
- }
7802
+ const warning = this.buildCategoryWarning(
7803
+ category,
7804
+ af,
7805
+ criticalWindow,
7806
+ warningWindow,
7807
+ horizon
7808
+ );
7809
+ if (warning) warnings.push(warning);
7684
7810
  }
7685
7811
  return warnings;
7686
7812
  }
7813
+ buildCategoryWarning(category, af, criticalWindow, warningWindow, horizon) {
7814
+ const forecast = af.adjusted;
7815
+ const crossing = forecast.thresholdCrossingWeeks;
7816
+ if (crossing === null || crossing <= 0) return null;
7817
+ const isHighConfidence = forecast.confidence === "high" || forecast.confidence === "medium";
7818
+ let severity = null;
7819
+ if (crossing <= criticalWindow && isHighConfidence) {
7820
+ severity = "critical";
7821
+ } else if (crossing <= warningWindow && isHighConfidence) {
7822
+ severity = "warning";
7823
+ } else if (crossing <= horizon) {
7824
+ severity = "info";
7825
+ }
7826
+ if (!severity) return null;
7827
+ return {
7828
+ severity,
7829
+ category,
7830
+ message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
7831
+ weeksUntil: crossing,
7832
+ confidence: forecast.confidence,
7833
+ contributingFeatures: af.contributingFeatures.map((f) => f.name)
7834
+ };
7835
+ }
7687
7836
  /**
7688
7837
  * Compute composite stability forecast by projecting per-category values
7689
7838
  * forward and computing stability scores at each horizon.
@@ -7752,14 +7901,9 @@ var PredictionEngine = class {
7752
7901
  const raw = fs5.readFileSync(roadmapPath, "utf-8");
7753
7902
  const parseResult = parseRoadmap(raw);
7754
7903
  if (!parseResult.ok) return null;
7755
- const features = [];
7756
- for (const milestone of parseResult.value.milestones) {
7757
- for (const feature of milestone.features) {
7758
- if (feature.status === "planned" || feature.status === "in-progress") {
7759
- features.push({ name: feature.name, spec: feature.spec });
7760
- }
7761
- }
7762
- }
7904
+ const features = parseResult.value.milestones.flatMap(
7905
+ (m) => m.features.filter((f) => f.status === "planned" || f.status === "in-progress").map((f) => ({ name: f.name, spec: f.spec }))
7906
+ );
7763
7907
  if (features.length === 0) return null;
7764
7908
  return this.estimator.estimateAll(features);
7765
7909
  } catch {
@@ -8410,7 +8554,36 @@ async function saveState(projectPath, state, stream, session) {
8410
8554
  // src/state/learnings-content.ts
8411
8555
  var fs11 = __toESM(require("fs"));
8412
8556
  var path8 = __toESM(require("path"));
8413
- var crypto = __toESM(require("crypto"));
8557
+ var crypto2 = __toESM(require("crypto"));
8558
+
8559
+ // src/compaction/envelope.ts
8560
+ function estimateTokens(content) {
8561
+ return Math.ceil(content.length / 4);
8562
+ }
8563
+ function serializeEnvelope(envelope) {
8564
+ const { meta, sections } = envelope;
8565
+ const strategyLabel = meta.strategy.length > 0 ? meta.strategy.join("+") : "none";
8566
+ let cacheLabel = "";
8567
+ if (meta.cached) {
8568
+ if (meta.cacheReadTokens != null && meta.cacheInputTokens != null && meta.cacheInputTokens > 0) {
8569
+ const hitPct = Math.round(meta.cacheReadTokens / meta.cacheInputTokens * 100);
8570
+ const readFormatted = meta.cacheReadTokens >= 1e3 ? (meta.cacheReadTokens / 1e3).toFixed(1) + "K" : String(meta.cacheReadTokens);
8571
+ cacheLabel = ` [cached | cache: ${readFormatted} read, ${hitPct}% hit]`;
8572
+ } else {
8573
+ cacheLabel = " [cached]";
8574
+ }
8575
+ }
8576
+ const header = `<!-- packed: ${strategyLabel} | ${meta.originalTokenEstimate}\u2192${meta.compactedTokenEstimate} tokens (-${meta.reductionPct}%)${cacheLabel} -->`;
8577
+ if (sections.length === 0) {
8578
+ return header;
8579
+ }
8580
+ const body = sections.map((section) => `### [${section.source}]
8581
+ ${section.content}`).join("\n\n");
8582
+ return `${header}
8583
+ ${body}`;
8584
+ }
8585
+
8586
+ // src/state/learnings-content.ts
8414
8587
  function parseFrontmatter2(line) {
8415
8588
  const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
8416
8589
  if (!match) return null;
@@ -8438,7 +8611,7 @@ function extractIndexEntry(entry) {
8438
8611
  };
8439
8612
  }
8440
8613
  function computeEntryHash(text) {
8441
- return crypto.createHash("sha256").update(text).digest("hex").slice(0, 8);
8614
+ return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 8);
8442
8615
  }
8443
8616
  function normalizeLearningContent(text) {
8444
8617
  let normalized = text;
@@ -8453,7 +8626,7 @@ function normalizeLearningContent(text) {
8453
8626
  return normalized;
8454
8627
  }
8455
8628
  function computeContentHash(text) {
8456
- return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
8629
+ return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 16);
8457
8630
  }
8458
8631
  function loadContentHashes(stateDir) {
8459
8632
  const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
@@ -8514,9 +8687,6 @@ function analyzeLearningPatterns(entries) {
8514
8687
  }
8515
8688
  return patterns.sort((a, b) => b.count - a.count);
8516
8689
  }
8517
- function estimateTokens(text) {
8518
- return Math.ceil(text.length / 4);
8519
- }
8520
8690
  function scoreRelevance(entry, intent) {
8521
8691
  if (!intent || intent.trim() === "") return 0;
8522
8692
  const intentWords = intent.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
@@ -8593,7 +8763,7 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
8593
8763
  // src/state/learnings.ts
8594
8764
  var fs13 = __toESM(require("fs"));
8595
8765
  var path10 = __toESM(require("path"));
8596
- var crypto2 = __toESM(require("crypto"));
8766
+ var crypto3 = __toESM(require("crypto"));
8597
8767
  async function appendLearning(projectPath, learning, skillName, outcome, stream, session) {
8598
8768
  try {
8599
8769
  const dirResult = await getStateDir(projectPath, stream, session);
@@ -8630,7 +8800,7 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
8630
8800
  } else {
8631
8801
  bulletLine = `- **${timestamp}:** ${learning}`;
8632
8802
  }
8633
- const hash = crypto2.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
8803
+ const hash = crypto3.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
8634
8804
  const tagsStr = fmTags.length > 0 ? ` tags:${fmTags.join(",")}` : "";
8635
8805
  const frontmatter = `<!-- hash:${hash}${tagsStr} -->`;
8636
8806
  const entry = `
@@ -9341,7 +9511,7 @@ async function updateSessionEntryStatus(projectPath, sessionSlug, section, entry
9341
9511
  }
9342
9512
  function generateEntryId() {
9343
9513
  const timestamp = Date.now().toString(36);
9344
- const random = Math.random().toString(36).substring(2, 8);
9514
+ const random = Buffer.from(crypto.getRandomValues(new Uint8Array(4))).toString("hex");
9345
9515
  return `${timestamp}-${random}`;
9346
9516
  }
9347
9517
 
@@ -9486,15 +9656,25 @@ async function loadEvents(projectPath, options) {
9486
9656
  );
9487
9657
  }
9488
9658
  }
9659
+ function phaseTransitionFields(data) {
9660
+ return {
9661
+ from: data?.from ?? "?",
9662
+ to: data?.to ?? "?",
9663
+ suffix: data?.taskCount ? ` (${data.taskCount} tasks)` : ""
9664
+ };
9665
+ }
9489
9666
  function formatPhaseTransition(event) {
9490
9667
  const data = event.data;
9491
- const suffix = data?.taskCount ? ` (${data.taskCount} tasks)` : "";
9492
- return `phase: ${data?.from ?? "?"} -> ${data?.to ?? "?"}${suffix}`;
9668
+ const { from, to, suffix } = phaseTransitionFields(data);
9669
+ return `phase: ${from} -> ${to}${suffix}`;
9670
+ }
9671
+ function formatGateChecks(checks) {
9672
+ return checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
9493
9673
  }
9494
9674
  function formatGateResult(event) {
9495
9675
  const data = event.data;
9496
9676
  const status = data?.passed ? "passed" : "failed";
9497
- const checks = data?.checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
9677
+ const checks = formatGateChecks(data?.checks);
9498
9678
  return checks ? `gate: ${status} (${checks})` : `gate: ${status}`;
9499
9679
  }
9500
9680
  function formatHandoffDetail(event) {
@@ -9770,34 +9950,36 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
9770
9950
  // src/security/stack-detector.ts
9771
9951
  var fs22 = __toESM(require("fs"));
9772
9952
  var path19 = __toESM(require("path"));
9773
- function detectStack(projectRoot) {
9774
- const stacks = [];
9953
+ function nodeSubStacks(allDeps) {
9954
+ const found = [];
9955
+ if (allDeps.react || allDeps["react-dom"]) found.push("react");
9956
+ if (allDeps.express) found.push("express");
9957
+ if (allDeps.koa) found.push("koa");
9958
+ if (allDeps.fastify) found.push("fastify");
9959
+ if (allDeps.next) found.push("next");
9960
+ if (allDeps.vue) found.push("vue");
9961
+ if (allDeps.angular || allDeps["@angular/core"]) found.push("angular");
9962
+ return found;
9963
+ }
9964
+ function detectNodeStacks(projectRoot) {
9775
9965
  const pkgJsonPath = path19.join(projectRoot, "package.json");
9776
- if (fs22.existsSync(pkgJsonPath)) {
9777
- stacks.push("node");
9778
- try {
9779
- const pkgJson = JSON.parse(fs22.readFileSync(pkgJsonPath, "utf-8"));
9780
- const allDeps = {
9781
- ...pkgJson.dependencies,
9782
- ...pkgJson.devDependencies
9783
- };
9784
- if (allDeps.react || allDeps["react-dom"]) stacks.push("react");
9785
- if (allDeps.express) stacks.push("express");
9786
- if (allDeps.koa) stacks.push("koa");
9787
- if (allDeps.fastify) stacks.push("fastify");
9788
- if (allDeps.next) stacks.push("next");
9789
- if (allDeps.vue) stacks.push("vue");
9790
- if (allDeps.angular || allDeps["@angular/core"]) stacks.push("angular");
9791
- } catch {
9792
- }
9966
+ if (!fs22.existsSync(pkgJsonPath)) return [];
9967
+ const stacks = ["node"];
9968
+ try {
9969
+ const pkgJson = JSON.parse(fs22.readFileSync(pkgJsonPath, "utf-8"));
9970
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
9971
+ stacks.push(...nodeSubStacks(allDeps));
9972
+ } catch {
9793
9973
  }
9794
- const goModPath = path19.join(projectRoot, "go.mod");
9795
- if (fs22.existsSync(goModPath)) {
9974
+ return stacks;
9975
+ }
9976
+ function detectStack(projectRoot) {
9977
+ const stacks = [...detectNodeStacks(projectRoot)];
9978
+ if (fs22.existsSync(path19.join(projectRoot, "go.mod"))) {
9796
9979
  stacks.push("go");
9797
9980
  }
9798
- const requirementsPath = path19.join(projectRoot, "requirements.txt");
9799
- const pyprojectPath = path19.join(projectRoot, "pyproject.toml");
9800
- if (fs22.existsSync(requirementsPath) || fs22.existsSync(pyprojectPath)) {
9981
+ const hasPython = fs22.existsSync(path19.join(projectRoot, "requirements.txt")) || fs22.existsSync(path19.join(projectRoot, "pyproject.toml"));
9982
+ if (hasPython) {
9801
9983
  stacks.push("python");
9802
9984
  }
9803
9985
  return stacks;
@@ -10641,6 +10823,56 @@ var SecurityScanner = class {
10641
10823
  });
10642
10824
  return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
10643
10825
  }
10826
+ /** Build a finding for a suppression comment that is missing its justification. */
10827
+ buildSuppressionFinding(rule, filePath, lineNumber, line) {
10828
+ return {
10829
+ ruleId: rule.id,
10830
+ ruleName: rule.name,
10831
+ category: rule.category,
10832
+ severity: this.config.strict ? "error" : "warning",
10833
+ confidence: "high",
10834
+ file: filePath,
10835
+ line: lineNumber,
10836
+ match: line.trim(),
10837
+ context: line,
10838
+ message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
10839
+ remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
10840
+ };
10841
+ }
10842
+ /** Check one line against a rule's patterns; return a finding or null. */
10843
+ matchRuleLine(rule, resolved, filePath, lineNumber, line) {
10844
+ for (const pattern of rule.patterns) {
10845
+ pattern.lastIndex = 0;
10846
+ if (!pattern.test(line)) continue;
10847
+ return {
10848
+ ruleId: rule.id,
10849
+ ruleName: rule.name,
10850
+ category: rule.category,
10851
+ severity: resolved,
10852
+ confidence: rule.confidence,
10853
+ file: filePath,
10854
+ line: lineNumber,
10855
+ match: line.trim(),
10856
+ context: line,
10857
+ message: rule.message,
10858
+ remediation: rule.remediation,
10859
+ ...rule.references ? { references: rule.references } : {}
10860
+ };
10861
+ }
10862
+ return null;
10863
+ }
10864
+ /** Scan a single line against a resolved rule; push any findings into the array. */
10865
+ scanLineForRule(rule, resolved, line, lineNumber, filePath, findings) {
10866
+ const suppressionMatch = parseHarnessIgnore(line, rule.id);
10867
+ if (suppressionMatch) {
10868
+ if (!suppressionMatch.justification) {
10869
+ findings.push(this.buildSuppressionFinding(rule, filePath, lineNumber, line));
10870
+ }
10871
+ return;
10872
+ }
10873
+ const finding = this.matchRuleLine(rule, resolved, filePath, lineNumber, line);
10874
+ if (finding) findings.push(finding);
10875
+ }
10644
10876
  /**
10645
10877
  * Core scanning loop shared by scanContent and scanContentForFile.
10646
10878
  * Evaluates each rule against each line, handling suppression (FP gate)
@@ -10657,46 +10889,7 @@ var SecurityScanner = class {
10657
10889
  );
10658
10890
  if (resolved === "off") continue;
10659
10891
  for (let i = 0; i < lines.length; i++) {
10660
- const line = lines[i] ?? "";
10661
- const suppressionMatch = parseHarnessIgnore(line, rule.id);
10662
- if (suppressionMatch) {
10663
- if (!suppressionMatch.justification) {
10664
- findings.push({
10665
- ruleId: rule.id,
10666
- ruleName: rule.name,
10667
- category: rule.category,
10668
- severity: this.config.strict ? "error" : "warning",
10669
- confidence: "high",
10670
- file: filePath,
10671
- line: startLine + i,
10672
- match: line.trim(),
10673
- context: line,
10674
- message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
10675
- remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
10676
- });
10677
- }
10678
- continue;
10679
- }
10680
- for (const pattern of rule.patterns) {
10681
- pattern.lastIndex = 0;
10682
- if (pattern.test(line)) {
10683
- findings.push({
10684
- ruleId: rule.id,
10685
- ruleName: rule.name,
10686
- category: rule.category,
10687
- severity: resolved,
10688
- confidence: rule.confidence,
10689
- file: filePath,
10690
- line: startLine + i,
10691
- match: line.trim(),
10692
- context: line,
10693
- message: rule.message,
10694
- remediation: rule.remediation,
10695
- ...rule.references ? { references: rule.references } : {}
10696
- });
10697
- break;
10698
- }
10699
- }
10892
+ this.scanLineForRule(rule, resolved, lines[i] ?? "", startLine + i, filePath, findings);
10700
10893
  }
10701
10894
  }
10702
10895
  return findings;
@@ -12116,37 +12309,30 @@ function extractConventionRules(bundle) {
12116
12309
  }
12117
12310
  return rules;
12118
12311
  }
12119
- function findMissingJsDoc(bundle) {
12312
+ var EXPORT_RE = /export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+(\w+)/;
12313
+ function hasPrecedingJsDoc(lines, i) {
12314
+ for (let j = i - 1; j >= 0; j--) {
12315
+ const prev = lines[j].trim();
12316
+ if (prev === "") continue;
12317
+ return prev.endsWith("*/");
12318
+ }
12319
+ return false;
12320
+ }
12321
+ function scanFileForMissingJsDoc(filePath, lines) {
12120
12322
  const missing = [];
12121
- for (const cf of bundle.changedFiles) {
12122
- const lines = cf.content.split("\n");
12123
- for (let i = 0; i < lines.length; i++) {
12124
- const line = lines[i];
12125
- const exportMatch = line.match(
12126
- /export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+(\w+)/
12127
- );
12128
- if (exportMatch) {
12129
- let hasJsDoc = false;
12130
- for (let j = i - 1; j >= 0; j--) {
12131
- const prev = lines[j].trim();
12132
- if (prev === "") continue;
12133
- if (prev.endsWith("*/")) {
12134
- hasJsDoc = true;
12135
- }
12136
- break;
12137
- }
12138
- if (!hasJsDoc) {
12139
- missing.push({
12140
- file: cf.path,
12141
- line: i + 1,
12142
- exportName: exportMatch[1]
12143
- });
12144
- }
12145
- }
12323
+ for (let i = 0; i < lines.length; i++) {
12324
+ const exportMatch = lines[i].match(EXPORT_RE);
12325
+ if (exportMatch && !hasPrecedingJsDoc(lines, i)) {
12326
+ missing.push({ file: filePath, line: i + 1, exportName: exportMatch[1] });
12146
12327
  }
12147
12328
  }
12148
12329
  return missing;
12149
12330
  }
12331
+ function findMissingJsDoc(bundle) {
12332
+ return bundle.changedFiles.flatMap(
12333
+ (cf) => scanFileForMissingJsDoc(cf.path, cf.content.split("\n"))
12334
+ );
12335
+ }
12150
12336
  function checkMissingJsDoc(bundle, rules) {
12151
12337
  const jsDocRule = rules.find((r) => r.text.toLowerCase().includes("jsdoc"));
12152
12338
  if (!jsDocRule) return [];
@@ -12211,29 +12397,27 @@ function checkChangeTypeSpecific(bundle) {
12211
12397
  return [];
12212
12398
  }
12213
12399
  }
12400
+ function checkFileResultTypeConvention(cf, bundle, rule) {
12401
+ const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
12402
+ const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
12403
+ if (!hasTryCatch || usesResult) return null;
12404
+ return {
12405
+ id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
12406
+ file: cf.path,
12407
+ lineRange: [1, cf.lines],
12408
+ domain: "compliance",
12409
+ severity: "suggestion",
12410
+ title: "Fallible operation uses try/catch instead of Result type",
12411
+ rationale: `Convention requires using Result type for fallible operations (from ${rule.source}).`,
12412
+ suggestion: "Refactor error handling to use the Result type pattern.",
12413
+ evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${rule.text}"`],
12414
+ validatedBy: "heuristic"
12415
+ };
12416
+ }
12214
12417
  function checkResultTypeConvention(bundle, rules) {
12215
12418
  const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
12216
12419
  if (!resultTypeRule) return [];
12217
- const findings = [];
12218
- for (const cf of bundle.changedFiles) {
12219
- const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
12220
- const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
12221
- if (hasTryCatch && !usesResult) {
12222
- findings.push({
12223
- id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
12224
- file: cf.path,
12225
- lineRange: [1, cf.lines],
12226
- domain: "compliance",
12227
- severity: "suggestion",
12228
- title: "Fallible operation uses try/catch instead of Result type",
12229
- rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
12230
- suggestion: "Refactor error handling to use the Result type pattern.",
12231
- evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${resultTypeRule.text}"`],
12232
- validatedBy: "heuristic"
12233
- });
12234
- }
12235
- }
12236
- return findings;
12420
+ return bundle.changedFiles.map((cf) => checkFileResultTypeConvention(cf, bundle, resultTypeRule)).filter((f) => f !== null);
12237
12421
  }
12238
12422
  function runComplianceAgent(bundle) {
12239
12423
  const rules = extractConventionRules(bundle);
@@ -12259,53 +12443,58 @@ var BUG_DETECTION_DESCRIPTOR = {
12259
12443
  "Test coverage \u2014 tests for happy path, error paths, and edge cases"
12260
12444
  ]
12261
12445
  };
12446
+ function hasPrecedingZeroCheck(lines, i) {
12447
+ const preceding = lines.slice(Math.max(0, i - 3), i).join("\n");
12448
+ return preceding.includes("=== 0") || preceding.includes("!== 0") || preceding.includes("== 0") || preceding.includes("!= 0");
12449
+ }
12262
12450
  function detectDivisionByZero(bundle) {
12263
12451
  const findings = [];
12264
12452
  for (const cf of bundle.changedFiles) {
12265
12453
  const lines = cf.content.split("\n");
12266
12454
  for (let i = 0; i < lines.length; i++) {
12267
12455
  const line = lines[i];
12268
- if (line.match(/[^=!<>]\s*\/\s*[a-zA-Z_]\w*/) && !line.includes("//")) {
12269
- const preceding = lines.slice(Math.max(0, i - 3), i).join("\n");
12270
- if (!preceding.includes("=== 0") && !preceding.includes("!== 0") && !preceding.includes("== 0") && !preceding.includes("!= 0")) {
12271
- findings.push({
12272
- id: makeFindingId("bug", cf.path, i + 1, "division by zero"),
12273
- file: cf.path,
12274
- lineRange: [i + 1, i + 1],
12275
- domain: "bug",
12276
- severity: "important",
12277
- title: "Potential division by zero without guard",
12278
- rationale: "Division operation found without a preceding zero check on the divisor. This can cause Infinity or NaN at runtime.",
12279
- suggestion: "Add a check for zero before dividing, or use a safe division utility.",
12280
- evidence: [`Line ${i + 1}: ${line.trim()}`],
12281
- validatedBy: "heuristic"
12282
- });
12283
- }
12284
- }
12456
+ if (!line.match(/[^=!<>]\s*\/\s*[a-zA-Z_]\w*/) || line.includes("//")) continue;
12457
+ if (hasPrecedingZeroCheck(lines, i)) continue;
12458
+ findings.push({
12459
+ id: makeFindingId("bug", cf.path, i + 1, "division by zero"),
12460
+ file: cf.path,
12461
+ lineRange: [i + 1, i + 1],
12462
+ domain: "bug",
12463
+ severity: "important",
12464
+ title: "Potential division by zero without guard",
12465
+ rationale: "Division operation found without a preceding zero check on the divisor. This can cause Infinity or NaN at runtime.",
12466
+ suggestion: "Add a check for zero before dividing, or use a safe division utility.",
12467
+ evidence: [`Line ${i + 1}: ${line.trim()}`],
12468
+ validatedBy: "heuristic"
12469
+ });
12285
12470
  }
12286
12471
  }
12287
12472
  return findings;
12288
12473
  }
12474
+ function isEmptyCatch(lines, i) {
12475
+ const line = lines[i];
12476
+ if (line.match(/catch\s*\([^)]*\)\s*\{\s*\}/)) return true;
12477
+ return line.match(/catch\s*\([^)]*\)\s*\{/) !== null && i + 1 < lines.length && lines[i + 1].trim() === "}";
12478
+ }
12289
12479
  function detectEmptyCatch(bundle) {
12290
12480
  const findings = [];
12291
12481
  for (const cf of bundle.changedFiles) {
12292
12482
  const lines = cf.content.split("\n");
12293
12483
  for (let i = 0; i < lines.length; i++) {
12484
+ if (!isEmptyCatch(lines, i)) continue;
12294
12485
  const line = lines[i];
12295
- if (line.match(/catch\s*\([^)]*\)\s*\{\s*\}/) || line.match(/catch\s*\([^)]*\)\s*\{/) && i + 1 < lines.length && lines[i + 1].trim() === "}") {
12296
- findings.push({
12297
- id: makeFindingId("bug", cf.path, i + 1, "empty catch block"),
12298
- file: cf.path,
12299
- lineRange: [i + 1, i + 2],
12300
- domain: "bug",
12301
- severity: "important",
12302
- title: "Empty catch block silently swallows error",
12303
- rationale: "Catching an error without handling, logging, or re-throwing it hides failures and makes debugging difficult.",
12304
- suggestion: "Log the error, re-throw it, or handle it explicitly. If intentionally ignoring, add a comment explaining why.",
12305
- evidence: [`Line ${i + 1}: ${line.trim()}`],
12306
- validatedBy: "heuristic"
12307
- });
12308
- }
12486
+ findings.push({
12487
+ id: makeFindingId("bug", cf.path, i + 1, "empty catch block"),
12488
+ file: cf.path,
12489
+ lineRange: [i + 1, i + 2],
12490
+ domain: "bug",
12491
+ severity: "important",
12492
+ title: "Empty catch block silently swallows error",
12493
+ rationale: "Catching an error without handling, logging, or re-throwing it hides failures and makes debugging difficult.",
12494
+ suggestion: "Log the error, re-throw it, or handle it explicitly. If intentionally ignoring, add a comment explaining why.",
12495
+ evidence: [`Line ${i + 1}: ${line.trim()}`],
12496
+ validatedBy: "heuristic"
12497
+ });
12309
12498
  }
12310
12499
  }
12311
12500
  return findings;
@@ -12363,34 +12552,102 @@ var SECRET_PATTERNS = [
12363
12552
  ];
12364
12553
  var SQL_CONCAT_PATTERN = /(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\s+.*?\+\s*\w+|`[^`]*\$\{[^}]*\}[^`]*(?:SELECT|INSERT|UPDATE|DELETE|WHERE)/i;
12365
12554
  var SHELL_EXEC_PATTERN = /(?:exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/;
12555
+ function makeEvalFinding(file, lineNum, line) {
12556
+ return {
12557
+ id: makeFindingId("security", file, lineNum, "eval usage CWE-94"),
12558
+ file,
12559
+ lineRange: [lineNum, lineNum],
12560
+ domain: "security",
12561
+ severity: "critical",
12562
+ title: `Dangerous ${"eval"}() or new ${"Function"}() usage`,
12563
+ rationale: `${"eval"}() and new ${"Function"}() execute arbitrary code. If user input reaches these calls, it enables Remote Code Execution (CWE-94).`,
12564
+ suggestion: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
12565
+ evidence: [`Line ${lineNum}: ${line.trim()}`],
12566
+ validatedBy: "heuristic",
12567
+ cweId: "CWE-94",
12568
+ owaspCategory: "A03:2021 Injection",
12569
+ confidence: "high",
12570
+ remediation: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
12571
+ references: [
12572
+ "https://cwe.mitre.org/data/definitions/94.html",
12573
+ "https://owasp.org/Top10/A03_2021-Injection/"
12574
+ ]
12575
+ };
12576
+ }
12577
+ function makeSecretFinding(file, lineNum) {
12578
+ return {
12579
+ id: makeFindingId("security", file, lineNum, "hardcoded secret CWE-798"),
12580
+ file,
12581
+ lineRange: [lineNum, lineNum],
12582
+ domain: "security",
12583
+ severity: "critical",
12584
+ title: "Hardcoded secret or API key detected",
12585
+ rationale: "Hardcoded secrets in source code can be extracted from version history even after removal. Use environment variables or a secrets manager (CWE-798).",
12586
+ suggestion: "Move the secret to an environment variable and access it via process.env.",
12587
+ evidence: [`Line ${lineNum}: [secret detected \u2014 value redacted]`],
12588
+ validatedBy: "heuristic",
12589
+ cweId: "CWE-798",
12590
+ owaspCategory: "A07:2021 Identification and Authentication Failures",
12591
+ confidence: "high",
12592
+ remediation: "Move the secret to an environment variable and access it via process.env.",
12593
+ references: [
12594
+ "https://cwe.mitre.org/data/definitions/798.html",
12595
+ "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
12596
+ ]
12597
+ };
12598
+ }
12599
+ function makeSqlFinding(file, lineNum, line) {
12600
+ return {
12601
+ id: makeFindingId("security", file, lineNum, "SQL injection CWE-89"),
12602
+ file,
12603
+ lineRange: [lineNum, lineNum],
12604
+ domain: "security",
12605
+ severity: "critical",
12606
+ title: "Potential SQL injection via string concatenation",
12607
+ rationale: "Building SQL queries with string concatenation or template literals allows attackers to inject malicious SQL (CWE-89).",
12608
+ suggestion: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
12609
+ evidence: [`Line ${lineNum}: ${line.trim()}`],
12610
+ validatedBy: "heuristic",
12611
+ cweId: "CWE-89",
12612
+ owaspCategory: "A03:2021 Injection",
12613
+ confidence: "high",
12614
+ remediation: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
12615
+ references: [
12616
+ "https://cwe.mitre.org/data/definitions/89.html",
12617
+ "https://owasp.org/Top10/A03_2021-Injection/"
12618
+ ]
12619
+ };
12620
+ }
12621
+ function makeCommandFinding(file, lineNum, line) {
12622
+ return {
12623
+ id: makeFindingId("security", file, lineNum, "command injection CWE-78"),
12624
+ file,
12625
+ lineRange: [lineNum, lineNum],
12626
+ domain: "security",
12627
+ severity: "critical",
12628
+ title: "Potential command injection via shell exec with interpolation",
12629
+ rationale: "Using exec/spawn with template literal interpolation allows attackers to inject shell commands (CWE-78).",
12630
+ suggestion: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
12631
+ evidence: [`Line ${lineNum}: ${line.trim()}`],
12632
+ validatedBy: "heuristic",
12633
+ cweId: "CWE-78",
12634
+ owaspCategory: "A03:2021 Injection",
12635
+ confidence: "high",
12636
+ remediation: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
12637
+ references: [
12638
+ "https://cwe.mitre.org/data/definitions/78.html",
12639
+ "https://owasp.org/Top10/A03_2021-Injection/"
12640
+ ]
12641
+ };
12642
+ }
12366
12643
  function detectEvalUsage(bundle) {
12367
12644
  const findings = [];
12368
12645
  for (const cf of bundle.changedFiles) {
12369
12646
  const lines = cf.content.split("\n");
12370
12647
  for (let i = 0; i < lines.length; i++) {
12371
12648
  const line = lines[i];
12372
- if (EVAL_PATTERN.test(line)) {
12373
- findings.push({
12374
- id: makeFindingId("security", cf.path, i + 1, "eval usage CWE-94"),
12375
- file: cf.path,
12376
- lineRange: [i + 1, i + 1],
12377
- domain: "security",
12378
- severity: "critical",
12379
- title: `Dangerous ${"eval"}() or new ${"Function"}() usage`,
12380
- rationale: `${"eval"}() and new ${"Function"}() execute arbitrary code. If user input reaches these calls, it enables Remote Code Execution (CWE-94).`,
12381
- suggestion: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
12382
- evidence: [`Line ${i + 1}: ${line.trim()}`],
12383
- validatedBy: "heuristic",
12384
- cweId: "CWE-94",
12385
- owaspCategory: "A03:2021 Injection",
12386
- confidence: "high",
12387
- remediation: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
12388
- references: [
12389
- "https://cwe.mitre.org/data/definitions/94.html",
12390
- "https://owasp.org/Top10/A03_2021-Injection/"
12391
- ]
12392
- });
12393
- }
12649
+ if (!EVAL_PATTERN.test(line)) continue;
12650
+ findings.push(makeEvalFinding(cf.path, i + 1, line));
12394
12651
  }
12395
12652
  }
12396
12653
  return findings;
@@ -12402,31 +12659,9 @@ function detectHardcodedSecrets(bundle) {
12402
12659
  for (let i = 0; i < lines.length; i++) {
12403
12660
  const line = lines[i];
12404
12661
  const codePart = line.includes("//") ? line.slice(0, line.indexOf("//")) : line;
12405
- for (const pattern of SECRET_PATTERNS) {
12406
- if (pattern.test(codePart)) {
12407
- findings.push({
12408
- id: makeFindingId("security", cf.path, i + 1, "hardcoded secret CWE-798"),
12409
- file: cf.path,
12410
- lineRange: [i + 1, i + 1],
12411
- domain: "security",
12412
- severity: "critical",
12413
- title: "Hardcoded secret or API key detected",
12414
- rationale: "Hardcoded secrets in source code can be extracted from version history even after removal. Use environment variables or a secrets manager (CWE-798).",
12415
- suggestion: "Move the secret to an environment variable and access it via process.env.",
12416
- evidence: [`Line ${i + 1}: [secret detected \u2014 value redacted]`],
12417
- validatedBy: "heuristic",
12418
- cweId: "CWE-798",
12419
- owaspCategory: "A07:2021 Identification and Authentication Failures",
12420
- confidence: "high",
12421
- remediation: "Move the secret to an environment variable and access it via process.env.",
12422
- references: [
12423
- "https://cwe.mitre.org/data/definitions/798.html",
12424
- "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
12425
- ]
12426
- });
12427
- break;
12428
- }
12429
- }
12662
+ const matched = SECRET_PATTERNS.some((p) => p.test(codePart));
12663
+ if (!matched) continue;
12664
+ findings.push(makeSecretFinding(cf.path, i + 1));
12430
12665
  }
12431
12666
  }
12432
12667
  return findings;
@@ -12437,28 +12672,8 @@ function detectSqlInjection(bundle) {
12437
12672
  const lines = cf.content.split("\n");
12438
12673
  for (let i = 0; i < lines.length; i++) {
12439
12674
  const line = lines[i];
12440
- if (SQL_CONCAT_PATTERN.test(line)) {
12441
- findings.push({
12442
- id: makeFindingId("security", cf.path, i + 1, "SQL injection CWE-89"),
12443
- file: cf.path,
12444
- lineRange: [i + 1, i + 1],
12445
- domain: "security",
12446
- severity: "critical",
12447
- title: "Potential SQL injection via string concatenation",
12448
- rationale: "Building SQL queries with string concatenation or template literals allows attackers to inject malicious SQL (CWE-89).",
12449
- suggestion: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
12450
- evidence: [`Line ${i + 1}: ${line.trim()}`],
12451
- validatedBy: "heuristic",
12452
- cweId: "CWE-89",
12453
- owaspCategory: "A03:2021 Injection",
12454
- confidence: "high",
12455
- remediation: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
12456
- references: [
12457
- "https://cwe.mitre.org/data/definitions/89.html",
12458
- "https://owasp.org/Top10/A03_2021-Injection/"
12459
- ]
12460
- });
12461
- }
12675
+ if (!SQL_CONCAT_PATTERN.test(line)) continue;
12676
+ findings.push(makeSqlFinding(cf.path, i + 1, line));
12462
12677
  }
12463
12678
  }
12464
12679
  return findings;
@@ -12469,28 +12684,8 @@ function detectCommandInjection(bundle) {
12469
12684
  const lines = cf.content.split("\n");
12470
12685
  for (let i = 0; i < lines.length; i++) {
12471
12686
  const line = lines[i];
12472
- if (SHELL_EXEC_PATTERN.test(line)) {
12473
- findings.push({
12474
- id: makeFindingId("security", cf.path, i + 1, "command injection CWE-78"),
12475
- file: cf.path,
12476
- lineRange: [i + 1, i + 1],
12477
- domain: "security",
12478
- severity: "critical",
12479
- title: "Potential command injection via shell exec with interpolation",
12480
- rationale: "Using exec/spawn with template literal interpolation allows attackers to inject shell commands (CWE-78).",
12481
- suggestion: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
12482
- evidence: [`Line ${i + 1}: ${line.trim()}`],
12483
- validatedBy: "heuristic",
12484
- cweId: "CWE-78",
12485
- owaspCategory: "A03:2021 Injection",
12486
- confidence: "high",
12487
- remediation: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
12488
- references: [
12489
- "https://cwe.mitre.org/data/definitions/78.html",
12490
- "https://owasp.org/Top10/A03_2021-Injection/"
12491
- ]
12492
- });
12493
- }
12687
+ if (!SHELL_EXEC_PATTERN.test(line)) continue;
12688
+ findings.push(makeCommandFinding(cf.path, i + 1, line));
12494
12689
  }
12495
12690
  }
12496
12691
  return findings;
@@ -12523,10 +12718,15 @@ function isViolationLine(line) {
12523
12718
  const lower = line.toLowerCase();
12524
12719
  return lower.includes("violation") || lower.includes("layer");
12525
12720
  }
12526
- function createLayerViolationFinding(line, fallbackPath) {
12527
- const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
12721
+ var VIOLATION_FILE_RE = /(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/;
12722
+ function extractViolationLocation(line, fallbackPath) {
12723
+ const fileMatch = line.match(VIOLATION_FILE_RE);
12528
12724
  const file = fileMatch?.[1] ?? fallbackPath;
12529
12725
  const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
12726
+ return { file, lineNum };
12727
+ }
12728
+ function createLayerViolationFinding(line, fallbackPath) {
12729
+ const { file, lineNum } = extractViolationLocation(line, fallbackPath);
12530
12730
  return {
12531
12731
  id: makeFindingId("arch", file, lineNum, "layer violation"),
12532
12732
  file,
@@ -12699,6 +12899,26 @@ function normalizePath(filePath, projectRoot) {
12699
12899
  }
12700
12900
  return normalized;
12701
12901
  }
12902
+ function resolveImportPath3(currentFile, importPath) {
12903
+ const dir = path23.dirname(currentFile);
12904
+ let resolved = path23.join(dir, importPath).replace(/\\/g, "/");
12905
+ if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
12906
+ resolved += ".ts";
12907
+ }
12908
+ return path23.normalize(resolved).replace(/\\/g, "/");
12909
+ }
12910
+ function enqueueImports(content, current, visited, queue, maxDepth) {
12911
+ const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
12912
+ let match;
12913
+ while ((match = importRegex.exec(content)) !== null) {
12914
+ const importPath = match[1];
12915
+ if (!importPath.startsWith(".")) continue;
12916
+ const resolved = resolveImportPath3(current.file, importPath);
12917
+ if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
12918
+ queue.push({ file: resolved, depth: current.depth + 1 });
12919
+ }
12920
+ }
12921
+ }
12702
12922
  function followImportChain(fromFile, fileContents, maxDepth = 2) {
12703
12923
  const visited = /* @__PURE__ */ new Set();
12704
12924
  const queue = [{ file: fromFile, depth: 0 }];
@@ -12708,83 +12928,64 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
12708
12928
  visited.add(current.file);
12709
12929
  const content = fileContents.get(current.file);
12710
12930
  if (!content) continue;
12711
- const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
12712
- let match;
12713
- while ((match = importRegex.exec(content)) !== null) {
12714
- const importPath = match[1];
12715
- if (!importPath.startsWith(".")) continue;
12716
- const dir = path23.dirname(current.file);
12717
- let resolved = path23.join(dir, importPath).replace(/\\/g, "/");
12718
- if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
12719
- resolved += ".ts";
12720
- }
12721
- resolved = path23.normalize(resolved).replace(/\\/g, "/");
12722
- if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
12723
- queue.push({ file: resolved, depth: current.depth + 1 });
12724
- }
12725
- }
12931
+ enqueueImports(content, current, visited, queue, maxDepth);
12726
12932
  }
12727
12933
  visited.delete(fromFile);
12728
12934
  return visited;
12729
12935
  }
12730
- async function validateFindings(options) {
12731
- const { findings, exclusionSet, graph, projectRoot, fileContents } = options;
12732
- const validated = [];
12733
- for (const finding of findings) {
12734
- const normalizedFile = normalizePath(finding.file, projectRoot);
12735
- if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
12736
- continue;
12737
- }
12738
- const absoluteFile = path23.isAbsolute(finding.file) ? finding.file : path23.join(projectRoot, finding.file).replace(/\\/g, "/");
12739
- if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
12740
- continue;
12741
- }
12742
- const crossFileRefs = extractCrossFileRefs(finding);
12743
- if (crossFileRefs.length === 0) {
12744
- validated.push({ ...finding });
12745
- continue;
12746
- }
12747
- if (graph) {
12748
- try {
12749
- let allReachable = true;
12750
- for (const ref of crossFileRefs) {
12751
- const reachable = await graph.isReachable(ref.from, ref.to);
12752
- if (!reachable) {
12753
- allReachable = false;
12754
- break;
12755
- }
12756
- }
12757
- if (allReachable) {
12758
- validated.push({ ...finding, validatedBy: "graph" });
12759
- }
12760
- continue;
12761
- } catch {
12762
- }
12936
+ function isMechanicallyExcluded(finding, exclusionSet, projectRoot) {
12937
+ const normalizedFile = normalizePath(finding.file, projectRoot);
12938
+ if (exclusionSet.isExcluded(normalizedFile, finding.lineRange)) return true;
12939
+ if (exclusionSet.isExcluded(finding.file, finding.lineRange)) return true;
12940
+ const absoluteFile = path23.isAbsolute(finding.file) ? finding.file : path23.join(projectRoot, finding.file).replace(/\\/g, "/");
12941
+ return exclusionSet.isExcluded(absoluteFile, finding.lineRange);
12942
+ }
12943
+ async function validateWithGraph(crossFileRefs, graph) {
12944
+ try {
12945
+ for (const ref of crossFileRefs) {
12946
+ const reachable = await graph.isReachable(ref.from, ref.to);
12947
+ if (!reachable) return { result: "discard" };
12763
12948
  }
12764
- {
12765
- let chainValidated = false;
12766
- if (fileContents) {
12767
- for (const ref of crossFileRefs) {
12768
- const normalizedFrom = normalizePath(ref.from, projectRoot);
12769
- const reachable = followImportChain(normalizedFrom, fileContents, 2);
12770
- const normalizedTo = normalizePath(ref.to, projectRoot);
12771
- if (reachable.has(normalizedTo)) {
12772
- chainValidated = true;
12773
- break;
12774
- }
12775
- }
12776
- }
12777
- if (chainValidated) {
12778
- validated.push({ ...finding, validatedBy: "heuristic" });
12779
- } else {
12780
- validated.push({
12781
- ...finding,
12782
- severity: DOWNGRADE_MAP[finding.severity],
12783
- validatedBy: "heuristic"
12784
- });
12949
+ return { result: "keep" };
12950
+ } catch {
12951
+ return { result: "fallback" };
12952
+ }
12953
+ }
12954
+ function validateWithHeuristic(finding, crossFileRefs, fileContents, projectRoot) {
12955
+ if (fileContents) {
12956
+ for (const ref of crossFileRefs) {
12957
+ const normalizedFrom = normalizePath(ref.from, projectRoot);
12958
+ const reachable = followImportChain(normalizedFrom, fileContents, 2);
12959
+ const normalizedTo = normalizePath(ref.to, projectRoot);
12960
+ if (reachable.has(normalizedTo)) {
12961
+ return { ...finding, validatedBy: "heuristic" };
12785
12962
  }
12786
12963
  }
12787
12964
  }
12965
+ return {
12966
+ ...finding,
12967
+ severity: DOWNGRADE_MAP[finding.severity],
12968
+ validatedBy: "heuristic"
12969
+ };
12970
+ }
12971
+ async function processFinding(finding, exclusionSet, graph, projectRoot, fileContents) {
12972
+ if (isMechanicallyExcluded(finding, exclusionSet, projectRoot)) return null;
12973
+ const crossFileRefs = extractCrossFileRefs(finding);
12974
+ if (crossFileRefs.length === 0) return { ...finding };
12975
+ if (graph) {
12976
+ const { result } = await validateWithGraph(crossFileRefs, graph);
12977
+ if (result === "keep") return { ...finding, validatedBy: "graph" };
12978
+ if (result === "discard") return null;
12979
+ }
12980
+ return validateWithHeuristic(finding, crossFileRefs, fileContents, projectRoot);
12981
+ }
12982
+ async function validateFindings(options) {
12983
+ const { findings, exclusionSet, graph, projectRoot, fileContents } = options;
12984
+ const validated = [];
12985
+ for (const finding of findings) {
12986
+ const result = await processFinding(finding, exclusionSet, graph, projectRoot, fileContents);
12987
+ if (result !== null) validated.push(result);
12988
+ }
12788
12989
  return validated;
12789
12990
  }
12790
12991
 
@@ -13384,25 +13585,32 @@ function serializeRoadmap(roadmap) {
13384
13585
  function serializeMilestoneHeading(milestone) {
13385
13586
  return milestone.isBacklog ? "## Backlog" : `## ${milestone.name}`;
13386
13587
  }
13588
+ function orDash(value) {
13589
+ return value ?? EM_DASH2;
13590
+ }
13591
+ function listOrDash(items) {
13592
+ return items.length > 0 ? items.join(", ") : EM_DASH2;
13593
+ }
13594
+ function serializeExtendedLines(feature) {
13595
+ const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
13596
+ if (!hasExtended) return [];
13597
+ return [
13598
+ `- **Assignee:** ${orDash(feature.assignee)}`,
13599
+ `- **Priority:** ${orDash(feature.priority)}`,
13600
+ `- **External-ID:** ${orDash(feature.externalId)}`
13601
+ ];
13602
+ }
13387
13603
  function serializeFeature(feature) {
13388
- const spec = feature.spec ?? EM_DASH2;
13389
- const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
13390
- const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
13391
13604
  const lines = [
13392
13605
  `### ${feature.name}`,
13393
13606
  "",
13394
13607
  `- **Status:** ${feature.status}`,
13395
- `- **Spec:** ${spec}`,
13608
+ `- **Spec:** ${orDash(feature.spec)}`,
13396
13609
  `- **Summary:** ${feature.summary}`,
13397
- `- **Blockers:** ${blockedBy}`,
13398
- `- **Plan:** ${plans}`
13610
+ `- **Blockers:** ${listOrDash(feature.blockedBy)}`,
13611
+ `- **Plan:** ${listOrDash(feature.plans)}`,
13612
+ ...serializeExtendedLines(feature)
13399
13613
  ];
13400
- const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
13401
- if (hasExtended) {
13402
- lines.push(`- **Assignee:** ${feature.assignee ?? EM_DASH2}`);
13403
- lines.push(`- **Priority:** ${feature.priority ?? EM_DASH2}`);
13404
- lines.push(`- **External-ID:** ${feature.externalId ?? EM_DASH2}`);
13405
- }
13406
13614
  return lines;
13407
13615
  }
13408
13616
  function serializeAssignmentHistory(records) {
@@ -13436,6 +13644,26 @@ function isRegression(from, to) {
13436
13644
  }
13437
13645
 
13438
13646
  // src/roadmap/sync.ts
13647
+ function collectAutopilotStatuses(autopilotPath, featurePlans, allTaskStatuses) {
13648
+ try {
13649
+ const raw = fs24.readFileSync(autopilotPath, "utf-8");
13650
+ const autopilot = JSON.parse(raw);
13651
+ if (!autopilot.phases) return;
13652
+ const linkedPhases = autopilot.phases.filter(
13653
+ (phase) => phase.planPath ? featurePlans.some((p) => p === phase.planPath || phase.planPath.endsWith(p)) : false
13654
+ );
13655
+ for (const phase of linkedPhases) {
13656
+ if (phase.status === "complete") {
13657
+ allTaskStatuses.push("complete");
13658
+ } else if (phase.status === "pending") {
13659
+ allTaskStatuses.push("pending");
13660
+ } else {
13661
+ allTaskStatuses.push("in_progress");
13662
+ }
13663
+ }
13664
+ } catch {
13665
+ }
13666
+ }
13439
13667
  function inferStatus(feature, projectPath, allFeatures) {
13440
13668
  if (feature.blockedBy.length > 0) {
13441
13669
  const blockerNotDone = feature.blockedBy.some((blockerName) => {
@@ -13471,26 +13699,7 @@ function inferStatus(feature, projectPath, allFeatures) {
13471
13699
  if (!entry.isDirectory()) continue;
13472
13700
  const autopilotPath = path24.join(sessionsDir, entry.name, "autopilot-state.json");
13473
13701
  if (!fs24.existsSync(autopilotPath)) continue;
13474
- try {
13475
- const raw = fs24.readFileSync(autopilotPath, "utf-8");
13476
- const autopilot = JSON.parse(raw);
13477
- if (!autopilot.phases) continue;
13478
- const linkedPhases = autopilot.phases.filter(
13479
- (phase) => phase.planPath ? feature.plans.some((p) => p === phase.planPath || phase.planPath.endsWith(p)) : false
13480
- );
13481
- if (linkedPhases.length > 0) {
13482
- for (const phase of linkedPhases) {
13483
- if (phase.status === "complete") {
13484
- allTaskStatuses.push("complete");
13485
- } else if (phase.status === "pending") {
13486
- allTaskStatuses.push("pending");
13487
- } else {
13488
- allTaskStatuses.push("in_progress");
13489
- }
13490
- }
13491
- }
13492
- } catch {
13493
- }
13702
+ collectAutopilotStatuses(autopilotPath, feature.plans, allTaskStatuses);
13494
13703
  }
13495
13704
  } catch {
13496
13705
  }
@@ -13807,23 +14016,36 @@ var GitHubIssuesSyncAdapter = class {
13807
14016
  return (0, import_types25.Err)(error instanceof Error ? error : new Error(String(error)));
13808
14017
  }
13809
14018
  }
14019
+ buildLabelsParam() {
14020
+ const filterLabels = this.config.labels ?? [];
14021
+ return filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
14022
+ }
14023
+ issueToTicketState(issue) {
14024
+ return {
14025
+ externalId: buildExternalId(this.owner, this.repo, issue.number),
14026
+ title: issue.title,
14027
+ status: issue.state,
14028
+ labels: issue.labels.map((l) => l.name),
14029
+ assignee: issue.assignee ? `@${issue.assignee.login}` : null
14030
+ };
14031
+ }
14032
+ async fetchIssuePage(page, labelsParam) {
14033
+ const perPage = 100;
14034
+ return fetchWithRetry(
14035
+ this.fetchFn,
14036
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
14037
+ { method: "GET", headers: this.headers() },
14038
+ this.retryOpts
14039
+ );
14040
+ }
13810
14041
  async fetchAllTickets() {
13811
14042
  try {
13812
- const filterLabels = this.config.labels ?? [];
13813
- const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
14043
+ const labelsParam = this.buildLabelsParam();
13814
14044
  const tickets = [];
13815
14045
  let page = 1;
13816
14046
  const perPage = 100;
13817
14047
  while (true) {
13818
- const response = await fetchWithRetry(
13819
- this.fetchFn,
13820
- `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
13821
- {
13822
- method: "GET",
13823
- headers: this.headers()
13824
- },
13825
- this.retryOpts
13826
- );
14048
+ const response = await this.fetchIssuePage(page, labelsParam);
13827
14049
  if (!response.ok) {
13828
14050
  const text = await response.text();
13829
14051
  return (0, import_types25.Err)(new Error(`GitHub API error ${response.status}: ${text}`));
@@ -13831,13 +14053,7 @@ var GitHubIssuesSyncAdapter = class {
13831
14053
  const data = await response.json();
13832
14054
  const issues = data.filter((d) => !d.pull_request);
13833
14055
  for (const issue of issues) {
13834
- tickets.push({
13835
- externalId: buildExternalId(this.owner, this.repo, issue.number),
13836
- title: issue.title,
13837
- status: issue.state,
13838
- labels: issue.labels.map((l) => l.name),
13839
- assignee: issue.assignee ? `@${issue.assignee.login}` : null
13840
- });
14056
+ tickets.push(this.issueToTicketState(issue));
13841
14057
  }
13842
14058
  if (data.length < perPage) break;
13843
14059
  page++;
@@ -13932,6 +14148,22 @@ async function syncToExternal(roadmap, adapter, config, prefetchedTickets) {
13932
14148
  }
13933
14149
  return result;
13934
14150
  }
14151
+ function applyTicketToFeature(ticketState, feature, config, forceSync, result) {
14152
+ if (ticketState.assignee !== feature.assignee) {
14153
+ result.assignmentChanges.push({
14154
+ feature: feature.name,
14155
+ from: feature.assignee,
14156
+ to: ticketState.assignee
14157
+ });
14158
+ feature.assignee = ticketState.assignee;
14159
+ }
14160
+ const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
14161
+ if (!resolvedStatus || resolvedStatus === feature.status) return;
14162
+ const newStatus = resolvedStatus;
14163
+ if (!forceSync && isRegression(feature.status, newStatus)) return;
14164
+ if (!forceSync && feature.status === "blocked" && newStatus === "planned") return;
14165
+ feature.status = newStatus;
14166
+ }
13935
14167
  async function syncFromExternal(roadmap, adapter, config, options, prefetchedTickets) {
13936
14168
  const result = emptySyncResult();
13937
14169
  const forceSync = options?.forceSync ?? false;
@@ -13958,25 +14190,7 @@ async function syncFromExternal(roadmap, adapter, config, options, prefetchedTic
13958
14190
  for (const ticketState of tickets) {
13959
14191
  const feature = featureByExternalId.get(ticketState.externalId);
13960
14192
  if (!feature) continue;
13961
- if (ticketState.assignee !== feature.assignee) {
13962
- result.assignmentChanges.push({
13963
- feature: feature.name,
13964
- from: feature.assignee,
13965
- to: ticketState.assignee
13966
- });
13967
- feature.assignee = ticketState.assignee;
13968
- }
13969
- const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
13970
- if (resolvedStatus && resolvedStatus !== feature.status) {
13971
- const newStatus = resolvedStatus;
13972
- if (!forceSync && isRegression(feature.status, newStatus)) {
13973
- continue;
13974
- }
13975
- if (!forceSync && feature.status === "blocked" && newStatus === "planned") {
13976
- continue;
13977
- }
13978
- feature.status = newStatus;
13979
- }
14193
+ applyTicketToFeature(ticketState, feature, config, forceSync, result);
13980
14194
  }
13981
14195
  return result;
13982
14196
  }
@@ -14024,6 +14238,24 @@ var PRIORITY_RANK = {
14024
14238
  var POSITION_WEIGHT = 0.5;
14025
14239
  var DEPENDENTS_WEIGHT = 0.3;
14026
14240
  var AFFINITY_WEIGHT = 0.2;
14241
+ function isEligibleCandidate(feature, allFeatureNames, doneFeatures) {
14242
+ if (feature.status !== "planned" && feature.status !== "backlog") return false;
14243
+ const isBlocked = feature.blockedBy.some((blocker) => {
14244
+ const key = blocker.toLowerCase();
14245
+ return allFeatureNames.has(key) && !doneFeatures.has(key);
14246
+ });
14247
+ return !isBlocked;
14248
+ }
14249
+ function computeAffinityScore(feature, milestoneName, milestoneMap, userCompletedFeatures) {
14250
+ if (userCompletedFeatures.size === 0) return 0;
14251
+ const completedBlocker = feature.blockedBy.some(
14252
+ (b) => userCompletedFeatures.has(b.toLowerCase())
14253
+ );
14254
+ if (completedBlocker) return 1;
14255
+ const siblings = milestoneMap.get(milestoneName) ?? [];
14256
+ const completedSibling = siblings.some((s) => userCompletedFeatures.has(s));
14257
+ return completedSibling ? 0.5 : 0;
14258
+ }
14027
14259
  function scoreRoadmapCandidates(roadmap, options) {
14028
14260
  const allFeatures = roadmap.milestones.flatMap((m) => m.features);
14029
14261
  const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
@@ -14062,33 +14294,18 @@ function scoreRoadmapCandidates(roadmap, options) {
14062
14294
  const candidates = [];
14063
14295
  let globalPosition = 0;
14064
14296
  for (const ms of roadmap.milestones) {
14065
- for (let featureIdx = 0; featureIdx < ms.features.length; featureIdx++) {
14066
- const feature = ms.features[featureIdx];
14297
+ for (const feature of ms.features) {
14067
14298
  globalPosition++;
14068
- if (feature.status !== "planned" && feature.status !== "backlog") continue;
14069
- const isBlocked = feature.blockedBy.some((blocker) => {
14070
- const key = blocker.toLowerCase();
14071
- return allFeatureNames.has(key) && !doneFeatures.has(key);
14072
- });
14073
- if (isBlocked) continue;
14299
+ if (!isEligibleCandidate(feature, allFeatureNames, doneFeatures)) continue;
14074
14300
  const positionScore = 1 - (globalPosition - 1) / totalPositions;
14075
14301
  const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
14076
14302
  const dependentsScore = deps / maxDependents;
14077
- let affinityScore = 0;
14078
- if (userCompletedFeatures.size > 0) {
14079
- const completedBlockers = feature.blockedBy.filter(
14080
- (b) => userCompletedFeatures.has(b.toLowerCase())
14081
- );
14082
- if (completedBlockers.length > 0) {
14083
- affinityScore = 1;
14084
- } else {
14085
- const siblings = milestoneMap.get(ms.name) ?? [];
14086
- const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
14087
- if (completedSiblings.length > 0) {
14088
- affinityScore = 0.5;
14089
- }
14090
- }
14091
- }
14303
+ const affinityScore = computeAffinityScore(
14304
+ feature,
14305
+ ms.name,
14306
+ milestoneMap,
14307
+ userCompletedFeatures
14308
+ );
14092
14309
  const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
14093
14310
  const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
14094
14311
  candidates.push({
@@ -14430,9 +14647,9 @@ async function resolveWasmPath(grammarName) {
14430
14647
  const { createRequire } = await import("module");
14431
14648
  const require2 = createRequire(import_meta.url ?? __filename);
14432
14649
  const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
14433
- const path31 = await import("path");
14434
- const pkgDir = path31.dirname(pkgPath);
14435
- return path31.join(pkgDir, "out", `${grammarName}.wasm`);
14650
+ const path34 = await import("path");
14651
+ const pkgDir = path34.dirname(pkgPath);
14652
+ return path34.join(pkgDir, "out", `${grammarName}.wasm`);
14436
14653
  }
14437
14654
  async function loadLanguage(lang) {
14438
14655
  const grammarName = GRAMMAR_MAP[lang];
@@ -15150,6 +15367,27 @@ function aggregateByDay(records) {
15150
15367
  // src/usage/jsonl-reader.ts
15151
15368
  var fs30 = __toESM(require("fs"));
15152
15369
  var path29 = __toESM(require("path"));
15370
+ function extractTokenUsage(entry, lineNumber) {
15371
+ const tokenUsage = entry.token_usage;
15372
+ if (!tokenUsage || typeof tokenUsage !== "object") {
15373
+ console.warn(
15374
+ `[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
15375
+ );
15376
+ return null;
15377
+ }
15378
+ return tokenUsage;
15379
+ }
15380
+ function applyOptionalFields2(record, entry) {
15381
+ if (entry.cache_creation_tokens != null) {
15382
+ record.cacheCreationTokens = entry.cache_creation_tokens;
15383
+ }
15384
+ if (entry.cache_read_tokens != null) {
15385
+ record.cacheReadTokens = entry.cache_read_tokens;
15386
+ }
15387
+ if (entry.model != null) {
15388
+ record.model = entry.model;
15389
+ }
15390
+ }
15153
15391
  function parseLine(line, lineNumber) {
15154
15392
  let entry;
15155
15393
  try {
@@ -15158,13 +15396,8 @@ function parseLine(line, lineNumber) {
15158
15396
  console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
15159
15397
  return null;
15160
15398
  }
15161
- const tokenUsage = entry.token_usage;
15162
- if (!tokenUsage || typeof tokenUsage !== "object") {
15163
- console.warn(
15164
- `[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
15165
- );
15166
- return null;
15167
- }
15399
+ const tokenUsage = extractTokenUsage(entry, lineNumber);
15400
+ if (!tokenUsage) return null;
15168
15401
  const inputTokens = tokenUsage.input_tokens ?? 0;
15169
15402
  const outputTokens = tokenUsage.output_tokens ?? 0;
15170
15403
  const record = {
@@ -15176,15 +15409,7 @@ function parseLine(line, lineNumber) {
15176
15409
  totalTokens: inputTokens + outputTokens
15177
15410
  }
15178
15411
  };
15179
- if (entry.cache_creation_tokens != null) {
15180
- record.cacheCreationTokens = entry.cache_creation_tokens;
15181
- }
15182
- if (entry.cache_read_tokens != null) {
15183
- record.cacheReadTokens = entry.cache_read_tokens;
15184
- }
15185
- if (entry.model != null) {
15186
- record.model = entry.model;
15187
- }
15412
+ applyOptionalFields2(record, entry);
15188
15413
  return record;
15189
15414
  }
15190
15415
  function readCostRecords(projectRoot) {
@@ -15219,6 +15444,14 @@ function extractUsage(entry) {
15219
15444
  const usage = message.usage;
15220
15445
  return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
15221
15446
  }
15447
+ function applyOptionalCCFields(record, message, usage) {
15448
+ const model = message.model;
15449
+ if (model) record.model = model;
15450
+ const cacheCreate = usage.cache_creation_input_tokens;
15451
+ const cacheRead = usage.cache_read_input_tokens;
15452
+ if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
15453
+ if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
15454
+ }
15222
15455
  function buildRecord(entry, usage) {
15223
15456
  const inputTokens = Number(usage.input_tokens) || 0;
15224
15457
  const outputTokens = Number(usage.output_tokens) || 0;
@@ -15229,12 +15462,7 @@ function buildRecord(entry, usage) {
15229
15462
  tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
15230
15463
  _source: "claude-code"
15231
15464
  };
15232
- const model = message.model;
15233
- if (model) record.model = model;
15234
- const cacheCreate = usage.cache_creation_input_tokens;
15235
- const cacheRead = usage.cache_read_input_tokens;
15236
- if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
15237
- if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
15465
+ applyOptionalCCFields(record, message, usage);
15238
15466
  return record;
15239
15467
  }
15240
15468
  function parseCCLine(line, filePath, lineNumber) {
@@ -15301,14 +15529,458 @@ function parseCCRecords() {
15301
15529
  return records;
15302
15530
  }
15303
15531
 
15304
- // src/index.ts
15305
- var VERSION = "0.15.0";
15532
+ // src/adoption/reader.ts
15533
+ var fs32 = __toESM(require("fs"));
15534
+ var path31 = __toESM(require("path"));
15535
+ function parseLine2(line, lineNumber) {
15536
+ try {
15537
+ const parsed = JSON.parse(line);
15538
+ if (typeof parsed.skill !== "string" || typeof parsed.startedAt !== "string" || typeof parsed.duration !== "number" || typeof parsed.outcome !== "string" || !Array.isArray(parsed.phasesReached)) {
15539
+ process.stderr.write(
15540
+ `[harness adoption] Skipping malformed JSONL line ${lineNumber}: missing required fields
15541
+ `
15542
+ );
15543
+ return null;
15544
+ }
15545
+ return parsed;
15546
+ } catch {
15547
+ process.stderr.write(`[harness adoption] Skipping malformed JSONL line ${lineNumber}
15548
+ `);
15549
+ return null;
15550
+ }
15551
+ }
15552
+ function readAdoptionRecords(projectRoot) {
15553
+ const adoptionFile = path31.join(projectRoot, ".harness", "metrics", "adoption.jsonl");
15554
+ let raw;
15555
+ try {
15556
+ raw = fs32.readFileSync(adoptionFile, "utf-8");
15557
+ } catch {
15558
+ return [];
15559
+ }
15560
+ const records = [];
15561
+ const lines = raw.split("\n");
15562
+ for (let i = 0; i < lines.length; i++) {
15563
+ const line = lines[i]?.trim();
15564
+ if (!line) continue;
15565
+ const record = parseLine2(line, i + 1);
15566
+ if (record) {
15567
+ records.push(record);
15568
+ }
15569
+ }
15570
+ return records;
15571
+ }
15572
+
15573
+ // src/adoption/aggregator.ts
15574
+ function aggregateBySkill(records) {
15575
+ if (records.length === 0) return [];
15576
+ const skillMap = /* @__PURE__ */ new Map();
15577
+ for (const record of records) {
15578
+ if (!skillMap.has(record.skill)) {
15579
+ const entry = { records: [] };
15580
+ if (record.tier != null) entry.tier = record.tier;
15581
+ skillMap.set(record.skill, entry);
15582
+ }
15583
+ skillMap.get(record.skill).records.push(record);
15584
+ }
15585
+ const results = [];
15586
+ for (const [skill, bucket] of skillMap) {
15587
+ const invocations = bucket.records.length;
15588
+ const completedCount = bucket.records.filter((r) => r.outcome === "completed").length;
15589
+ const totalDuration = bucket.records.reduce((sum, r) => sum + r.duration, 0);
15590
+ const timestamps = bucket.records.map((r) => r.startedAt).sort();
15591
+ const summary = {
15592
+ skill,
15593
+ invocations,
15594
+ successRate: completedCount / invocations,
15595
+ avgDuration: totalDuration / invocations,
15596
+ lastUsed: timestamps[timestamps.length - 1]
15597
+ };
15598
+ if (bucket.tier != null) summary.tier = bucket.tier;
15599
+ results.push(summary);
15600
+ }
15601
+ results.sort((a, b) => b.invocations - a.invocations);
15602
+ return results;
15603
+ }
15604
+ function aggregateByDay2(records) {
15605
+ if (records.length === 0) return [];
15606
+ const dayMap = /* @__PURE__ */ new Map();
15607
+ for (const record of records) {
15608
+ const date = record.startedAt.slice(0, 10);
15609
+ if (!dayMap.has(date)) {
15610
+ dayMap.set(date, { invocations: 0, skills: /* @__PURE__ */ new Set() });
15611
+ }
15612
+ const bucket = dayMap.get(date);
15613
+ bucket.invocations++;
15614
+ bucket.skills.add(record.skill);
15615
+ }
15616
+ const results = [];
15617
+ for (const [date, bucket] of dayMap) {
15618
+ results.push({
15619
+ date,
15620
+ invocations: bucket.invocations,
15621
+ uniqueSkills: bucket.skills.size
15622
+ });
15623
+ }
15624
+ results.sort((a, b) => b.date.localeCompare(a.date));
15625
+ return results;
15626
+ }
15627
+ function topSkills(records, n) {
15628
+ return aggregateBySkill(records).slice(0, n);
15629
+ }
15630
+
15631
+ // src/compaction/strategies/structural.ts
15632
+ function isEmptyObject(v) {
15633
+ return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length === 0;
15634
+ }
15635
+ function isRetainable(v) {
15636
+ return v !== void 0 && v !== "" && v !== null && !isEmptyObject(v);
15637
+ }
15638
+ function cleanArray(value) {
15639
+ const cleaned = value.map(cleanValue).filter(isRetainable);
15640
+ if (cleaned.length === 0) return void 0;
15641
+ if (cleaned.length === 1) return cleaned[0];
15642
+ return cleaned;
15643
+ }
15644
+ function cleanRecord(value) {
15645
+ const cleaned = {};
15646
+ for (const [k, v] of Object.entries(value)) {
15647
+ const result = cleanValue(v);
15648
+ if (isRetainable(result)) {
15649
+ cleaned[k] = result;
15650
+ }
15651
+ }
15652
+ if (Object.keys(cleaned).length === 0) return void 0;
15653
+ return cleaned;
15654
+ }
15655
+ function cleanValue(value) {
15656
+ if (value === null || value === void 0) return void 0;
15657
+ if (typeof value === "string") return value.replace(/\s+/g, " ").trim();
15658
+ if (Array.isArray(value)) return cleanArray(value);
15659
+ if (typeof value === "object") return cleanRecord(value);
15660
+ return value;
15661
+ }
15662
+ var StructuralStrategy = class {
15663
+ name = "structural";
15664
+ lossy = false;
15665
+ apply(content, _budget) {
15666
+ let parsed;
15667
+ try {
15668
+ parsed = JSON.parse(content);
15669
+ } catch {
15670
+ return content;
15671
+ }
15672
+ const cleaned = cleanValue(parsed);
15673
+ return JSON.stringify(cleaned) ?? "";
15674
+ }
15675
+ };
15676
+
15677
+ // src/compaction/strategies/truncation.ts
15678
+ var DEFAULT_TOKEN_BUDGET = 4e3;
15679
+ var CHARS_PER_TOKEN = 4;
15680
+ var TRUNCATION_MARKER = "\n[truncated \u2014 prioritized truncation applied]";
15681
+ function lineScore(line) {
15682
+ let score = 0;
15683
+ if (/\/[\w./-]/.test(line)) score += 40;
15684
+ if (/error|Error|ERROR|fail|FAIL|status/i.test(line)) score += 35;
15685
+ if (/\b[A-Z][a-z]+[A-Z]/.test(line) || /\b[a-z]+[A-Z]/.test(line)) score += 20;
15686
+ if (line.trim().length < 40) score += 10;
15687
+ return score;
15688
+ }
15689
+ function selectLines(lines, charBudget) {
15690
+ const scored = lines.map((line, idx) => ({ line, idx, score: lineScore(line) }));
15691
+ scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
15692
+ const kept = [];
15693
+ let used = 0;
15694
+ for (const item of scored) {
15695
+ const lineLen = item.line.length + 1;
15696
+ if (used + lineLen > charBudget) continue;
15697
+ kept.push({ line: item.line, idx: item.idx });
15698
+ used += lineLen;
15699
+ }
15700
+ kept.sort((a, b) => a.idx - b.idx);
15701
+ return kept;
15702
+ }
15703
+ var TruncationStrategy = class {
15704
+ name = "truncate";
15705
+ lossy = false;
15706
+ // deliberate: spec Decision 2 — truncation is classified lossless at the pipeline level
15707
+ apply(content, budget = DEFAULT_TOKEN_BUDGET) {
15708
+ if (!content) return content;
15709
+ const charBudget = budget * CHARS_PER_TOKEN;
15710
+ if (content.length <= charBudget) return content;
15711
+ const lines = content.split("\n");
15712
+ const available = charBudget - TRUNCATION_MARKER.length;
15713
+ const kept = available > 0 ? selectLines(lines, available) : [{ line: (lines[0] ?? "").slice(0, charBudget), idx: 0 }];
15714
+ return kept.map((k) => k.line).join("\n") + TRUNCATION_MARKER;
15715
+ }
15716
+ };
15717
+
15718
+ // src/compaction/pipeline.ts
15719
+ var CompactionPipeline = class {
15720
+ strategies;
15721
+ constructor(strategies) {
15722
+ this.strategies = strategies;
15723
+ }
15724
+ /** The ordered list of strategy names in this pipeline. */
15725
+ get strategyNames() {
15726
+ return this.strategies.map((s) => s.name);
15727
+ }
15728
+ /**
15729
+ * Apply all strategies in order.
15730
+ * @param content — input string
15731
+ * @param budget — optional token budget forwarded to each strategy
15732
+ */
15733
+ apply(content, budget) {
15734
+ return this.strategies.reduce((current, strategy) => {
15735
+ return strategy.apply(current, budget);
15736
+ }, content);
15737
+ }
15738
+ };
15739
+
15740
+ // src/caching/stability.ts
15741
+ var import_graph2 = require("@harness-engineering/graph");
15742
+ var STABILITY_LOOKUP = {};
15743
+ for (const [key, tier] of Object.entries(import_graph2.NODE_STABILITY)) {
15744
+ STABILITY_LOOKUP[key] = tier;
15745
+ STABILITY_LOOKUP[key.toLowerCase()] = tier;
15746
+ }
15747
+ STABILITY_LOOKUP["packed_summary"] = "session";
15748
+ STABILITY_LOOKUP["skill"] = "static";
15749
+ function resolveStability(contentType) {
15750
+ return STABILITY_LOOKUP[contentType] ?? "ephemeral";
15751
+ }
15752
+
15753
+ // src/caching/adapters/anthropic.ts
15754
+ var TIER_ORDER = {
15755
+ static: 0,
15756
+ session: 1,
15757
+ ephemeral: 2
15758
+ };
15759
+ var AnthropicCacheAdapter = class {
15760
+ provider = "claude";
15761
+ wrapSystemBlock(content, stability) {
15762
+ if (stability === "ephemeral") {
15763
+ return { type: "text", text: content };
15764
+ }
15765
+ const ttl = stability === "static" ? "1h" : void 0;
15766
+ return {
15767
+ type: "text",
15768
+ text: content,
15769
+ cache_control: {
15770
+ type: "ephemeral",
15771
+ ...ttl !== void 0 && { ttl }
15772
+ }
15773
+ };
15774
+ }
15775
+ wrapTools(tools, stability) {
15776
+ if (tools.length === 0 || stability === "ephemeral") {
15777
+ return { tools: tools.map((t) => ({ ...t })) };
15778
+ }
15779
+ const wrapped = tools.map((t) => ({ ...t }));
15780
+ const last = wrapped[wrapped.length - 1];
15781
+ if (last) {
15782
+ last.cache_control = { type: "ephemeral" };
15783
+ }
15784
+ return { tools: wrapped };
15785
+ }
15786
+ orderContent(blocks) {
15787
+ return [...blocks].sort((a, b) => TIER_ORDER[a.stability] - TIER_ORDER[b.stability]);
15788
+ }
15789
+ parseCacheUsage(response) {
15790
+ const resp = response;
15791
+ const usage = resp?.usage;
15792
+ return {
15793
+ cacheCreationTokens: usage?.cache_creation_input_tokens ?? 0,
15794
+ cacheReadTokens: usage?.cache_read_input_tokens ?? 0
15795
+ };
15796
+ }
15797
+ };
15798
+
15799
+ // src/caching/adapters/openai.ts
15800
+ var TIER_ORDER2 = {
15801
+ static: 0,
15802
+ session: 1,
15803
+ ephemeral: 2
15804
+ };
15805
+ var OpenAICacheAdapter = class {
15806
+ provider = "openai";
15807
+ wrapSystemBlock(content, _stability) {
15808
+ return { type: "text", text: content };
15809
+ }
15810
+ wrapTools(tools, _stability) {
15811
+ return { tools: tools.map((t) => ({ ...t })) };
15812
+ }
15813
+ orderContent(blocks) {
15814
+ return [...blocks].sort((a, b) => TIER_ORDER2[a.stability] - TIER_ORDER2[b.stability]);
15815
+ }
15816
+ parseCacheUsage(response) {
15817
+ const resp = response;
15818
+ const usage = resp?.usage;
15819
+ const details = usage?.prompt_tokens_details;
15820
+ return {
15821
+ cacheCreationTokens: 0,
15822
+ cacheReadTokens: details?.cached_tokens ?? 0
15823
+ };
15824
+ }
15825
+ };
15826
+
15827
+ // src/caching/adapters/gemini.ts
15828
+ var TIER_ORDER3 = {
15829
+ static: 0,
15830
+ session: 1,
15831
+ ephemeral: 2
15832
+ };
15833
+ var CACHED_CONTENT_MARKER = "cachedContents:pending";
15834
+ var GeminiCacheAdapter = class {
15835
+ provider = "gemini";
15836
+ wrapSystemBlock(content, stability) {
15837
+ if (stability === "static") {
15838
+ return {
15839
+ type: "text",
15840
+ text: content,
15841
+ cachedContentRef: CACHED_CONTENT_MARKER
15842
+ };
15843
+ }
15844
+ return { type: "text", text: content };
15845
+ }
15846
+ wrapTools(tools, _stability) {
15847
+ return { tools: tools.map((t) => ({ ...t })) };
15848
+ }
15849
+ orderContent(blocks) {
15850
+ return [...blocks].sort((a, b) => TIER_ORDER3[a.stability] - TIER_ORDER3[b.stability]);
15851
+ }
15852
+ parseCacheUsage(response) {
15853
+ const resp = response;
15854
+ const metadata = resp?.usageMetadata;
15855
+ return {
15856
+ cacheCreationTokens: 0,
15857
+ cacheReadTokens: metadata?.cachedContentTokenCount ?? 0
15858
+ };
15859
+ }
15860
+ };
15861
+
15862
+ // src/telemetry/consent.ts
15863
+ var fs34 = __toESM(require("fs"));
15864
+ var path33 = __toESM(require("path"));
15865
+
15866
+ // src/telemetry/install-id.ts
15867
+ var fs33 = __toESM(require("fs"));
15868
+ var path32 = __toESM(require("path"));
15869
+ var crypto4 = __toESM(require("crypto"));
15870
+ function getOrCreateInstallId(projectRoot) {
15871
+ const harnessDir = path32.join(projectRoot, ".harness");
15872
+ const installIdFile = path32.join(harnessDir, ".install-id");
15873
+ const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
15874
+ try {
15875
+ const existing = fs33.readFileSync(installIdFile, "utf-8").trim();
15876
+ if (UUID_V4_RE.test(existing)) {
15877
+ return existing;
15878
+ }
15879
+ } catch {
15880
+ }
15881
+ const id = crypto4.randomUUID();
15882
+ fs33.mkdirSync(harnessDir, { recursive: true });
15883
+ fs33.writeFileSync(installIdFile, id, { encoding: "utf-8", mode: 384 });
15884
+ return id;
15885
+ }
15886
+
15887
+ // src/telemetry/consent.ts
15888
+ function readIdentity(projectRoot) {
15889
+ const filePath = path33.join(projectRoot, ".harness", "telemetry.json");
15890
+ try {
15891
+ const raw = fs34.readFileSync(filePath, "utf-8");
15892
+ const parsed = JSON.parse(raw);
15893
+ if (parsed && typeof parsed === "object" && parsed.identity) {
15894
+ const { project, team, alias } = parsed.identity;
15895
+ const identity = {};
15896
+ if (typeof project === "string") identity.project = project;
15897
+ if (typeof team === "string") identity.team = team;
15898
+ if (typeof alias === "string") identity.alias = alias;
15899
+ return identity;
15900
+ }
15901
+ return {};
15902
+ } catch {
15903
+ return {};
15904
+ }
15905
+ }
15906
+ function resolveConsent(projectRoot, config) {
15907
+ if (process.env.DO_NOT_TRACK === "1") return { allowed: false };
15908
+ if (process.env.HARNESS_TELEMETRY_OPTOUT === "1") return { allowed: false };
15909
+ const enabled = config?.enabled ?? true;
15910
+ if (!enabled) return { allowed: false };
15911
+ const installId = getOrCreateInstallId(projectRoot);
15912
+ const identity = readIdentity(projectRoot);
15913
+ return { allowed: true, installId, identity };
15914
+ }
15915
+
15916
+ // src/version.ts
15917
+ var VERSION = "0.21.3";
15918
+
15919
+ // src/telemetry/collector.ts
15920
+ function mapOutcome(outcome) {
15921
+ return outcome === "completed" ? "success" : "failure";
15922
+ }
15923
+ function collectEvents(projectRoot, consent) {
15924
+ const records = readAdoptionRecords(projectRoot);
15925
+ if (records.length === 0) return [];
15926
+ const { installId, identity } = consent;
15927
+ const distinctId = identity.alias ?? installId;
15928
+ return records.map(
15929
+ (record) => ({
15930
+ event: "skill_invocation",
15931
+ distinctId,
15932
+ timestamp: record.startedAt,
15933
+ properties: {
15934
+ installId,
15935
+ os: process.platform,
15936
+ nodeVersion: process.version,
15937
+ harnessVersion: VERSION,
15938
+ skillName: record.skill,
15939
+ duration: record.duration,
15940
+ outcome: mapOutcome(record.outcome),
15941
+ phasesReached: record.phasesReached,
15942
+ ...identity.project ? { project: identity.project } : {},
15943
+ ...identity.team ? { team: identity.team } : {}
15944
+ }
15945
+ })
15946
+ );
15947
+ }
15948
+
15949
+ // src/telemetry/transport.ts
15950
+ var POSTHOG_BATCH_URL = "https://app.posthog.com/batch";
15951
+ var MAX_ATTEMPTS = 3;
15952
+ var TIMEOUT_MS = 5e3;
15953
+ function sleep2(ms) {
15954
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
15955
+ }
15956
+ async function send(events, apiKey) {
15957
+ if (events.length === 0) return;
15958
+ const payload = { api_key: apiKey, batch: events };
15959
+ const body = JSON.stringify(payload);
15960
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
15961
+ try {
15962
+ const res = await fetch(POSTHOG_BATCH_URL, {
15963
+ method: "POST",
15964
+ headers: { "Content-Type": "application/json" },
15965
+ body,
15966
+ signal: AbortSignal.timeout(TIMEOUT_MS)
15967
+ });
15968
+ if (res.ok) return;
15969
+ if (res.status < 500) return;
15970
+ } catch {
15971
+ }
15972
+ if (attempt < MAX_ATTEMPTS - 1) {
15973
+ await sleep2(1e3 * (attempt + 1));
15974
+ }
15975
+ }
15976
+ }
15306
15977
  // Annotate the CommonJS export names for ESM import in node:
15307
15978
  0 && (module.exports = {
15308
15979
  AGENT_DESCRIPTORS,
15309
15980
  ARCHITECTURE_DESCRIPTOR,
15310
15981
  AdjustedForecastSchema,
15311
15982
  AgentActionEmitter,
15983
+ AnthropicCacheAdapter,
15312
15984
  ArchBaselineManager,
15313
15985
  ArchBaselineSchema,
15314
15986
  ArchConfigSchema,
@@ -15328,12 +16000,12 @@ var VERSION = "0.15.0";
15328
16000
  CategorySnapshotSchema,
15329
16001
  ChecklistBuilder,
15330
16002
  CircularDepsCollector,
16003
+ CompactionPipeline,
15331
16004
  ComplexityCollector,
15332
16005
  ConfidenceTierSchema,
15333
16006
  ConfirmationSchema,
15334
16007
  ConsoleSink,
15335
16008
  ConstraintRuleSchema,
15336
- ContentPipeline,
15337
16009
  ContributingFeatureSchema,
15338
16010
  ContributionsSchema,
15339
16011
  CouplingCollector,
@@ -15343,6 +16015,7 @@ var VERSION = "0.15.0";
15343
16015
  DEFAULT_STABILITY_THRESHOLDS,
15344
16016
  DEFAULT_STATE,
15345
16017
  DEFAULT_STREAM_INDEX,
16018
+ DEFAULT_TOKEN_BUDGET,
15346
16019
  DESTRUCTIVE_BASH,
15347
16020
  DepDepthCollector,
15348
16021
  DirectionSchema,
@@ -15356,6 +16029,7 @@ var VERSION = "0.15.0";
15356
16029
  ForbiddenImportCollector,
15357
16030
  GateConfigSchema,
15358
16031
  GateResultSchema,
16032
+ GeminiCacheAdapter,
15359
16033
  GitHubIssuesSyncAdapter,
15360
16034
  HandoffSchema,
15361
16035
  HarnessStateSchema,
@@ -15370,6 +16044,7 @@ var VERSION = "0.15.0";
15370
16044
  NoOpExecutor,
15371
16045
  NoOpSink,
15372
16046
  NoOpTelemetryAdapter,
16047
+ OpenAICacheAdapter,
15373
16048
  PatternConfigSchema,
15374
16049
  PredictionEngine,
15375
16050
  PredictionOptionsSchema,
@@ -15397,6 +16072,7 @@ var VERSION = "0.15.0";
15397
16072
  StabilityForecastSchema,
15398
16073
  StreamIndexSchema,
15399
16074
  StreamInfoSchema,
16075
+ StructuralStrategy,
15400
16076
  ThresholdConfigSchema,
15401
16077
  TimelineFileSchema,
15402
16078
  TimelineManager,
@@ -15404,13 +16080,16 @@ var VERSION = "0.15.0";
15404
16080
  TransitionSchema,
15405
16081
  TrendLineSchema,
15406
16082
  TrendResultSchema,
16083
+ TruncationStrategy,
15407
16084
  TypeScriptParser,
15408
16085
  VERSION,
15409
16086
  ViolationSchema,
15410
16087
  addProvenance,
15411
16088
  agentConfigRules,
16089
+ aggregateAdoptionByDay,
15412
16090
  aggregateByDay,
15413
16091
  aggregateBySession,
16092
+ aggregateBySkill,
15414
16093
  analyzeDiff,
15415
16094
  analyzeLearningPatterns,
15416
16095
  appendFailure,
@@ -15442,6 +16121,7 @@ var VERSION = "0.15.0";
15442
16121
  clearFailuresCache,
15443
16122
  clearLearningsCache,
15444
16123
  clearTaint,
16124
+ collectEvents,
15445
16125
  computeContentHash,
15446
16126
  computeOverallSeverity,
15447
16127
  computeScanExitCode,
@@ -15481,6 +16161,7 @@ var VERSION = "0.15.0";
15481
16161
  determineAssessment,
15482
16162
  diff,
15483
16163
  emitEvent,
16164
+ estimateTokens,
15484
16165
  executeWorkflow,
15485
16166
  expressRules,
15486
16167
  extractBundle,
@@ -15502,6 +16183,7 @@ var VERSION = "0.15.0";
15502
16183
  getFeedbackConfig,
15503
16184
  getInjectionPatterns,
15504
16185
  getModelPrice,
16186
+ getOrCreateInstallId,
15505
16187
  getOutline,
15506
16188
  getParser,
15507
16189
  getPhaseCategories,
@@ -15554,8 +16236,10 @@ var VERSION = "0.15.0";
15554
16236
  promoteSessionLearnings,
15555
16237
  pruneLearnings,
15556
16238
  reactRules,
16239
+ readAdoptionRecords,
15557
16240
  readCheckState,
15558
16241
  readCostRecords,
16242
+ readIdentity,
15559
16243
  readLockfile,
15560
16244
  readSessionSection,
15561
16245
  readSessionSections,
@@ -15566,11 +16250,13 @@ var VERSION = "0.15.0";
15566
16250
  requestPeerReview,
15567
16251
  resetFeedbackConfig,
15568
16252
  resetParserCache,
16253
+ resolveConsent,
15569
16254
  resolveFileToLayer,
15570
16255
  resolveModelTier,
15571
16256
  resolveReverseStatus,
15572
16257
  resolveRuleSeverity,
15573
16258
  resolveSessionDir,
16259
+ resolveStability,
15574
16260
  resolveStreamPath,
15575
16261
  resolveThresholds,
15576
16262
  runAll,
@@ -15592,6 +16278,8 @@ var VERSION = "0.15.0";
15592
16278
  scoreRoadmapCandidates,
15593
16279
  searchSymbols,
15594
16280
  secretRules,
16281
+ send,
16282
+ serializeEnvelope,
15595
16283
  serializeRoadmap,
15596
16284
  setActiveStream,
15597
16285
  sharpEdgesRules,
@@ -15602,6 +16290,7 @@ var VERSION = "0.15.0";
15602
16290
  syncRoadmap,
15603
16291
  syncToExternal,
15604
16292
  tagUncitedFindings,
16293
+ topSkills,
15605
16294
  touchStream,
15606
16295
  trackAction,
15607
16296
  unfoldRange,