@harness-engineering/core 0.21.1 → 0.21.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/architecture/matchers.js +383 -332
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-BQUWXBGR.mjs → chunk-4W4FRAA6.mjs} +383 -332
- package/dist/index.d.mts +362 -45
- package/dist/index.d.ts +362 -45
- package/dist/index.js +2052 -1363
- package/dist/index.mjs +1653 -1033
- package/package.json +3 -3
package/dist/index.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: () =>
|
|
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(
|
|
394
|
+
async function fileExists(path34) {
|
|
377
395
|
try {
|
|
378
|
-
await accessAsync(
|
|
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(
|
|
402
|
+
async function readFileContent(path34) {
|
|
385
403
|
try {
|
|
386
|
-
const content = await readFileAsync(
|
|
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
|
|
438
|
-
const pathDisplay =
|
|
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 "${
|
|
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
|
|
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 (
|
|
516
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
581
|
+
return (0, import_types.Ok)({
|
|
560
582
|
valid: true,
|
|
561
583
|
type,
|
|
562
584
|
...scope && { scope },
|
|
563
|
-
breaking:
|
|
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(
|
|
662
|
-
return
|
|
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(
|
|
668
|
-
const contentResult = await readFileContent(
|
|
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:
|
|
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)(
|
|
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(
|
|
823
|
-
const targetName = (0, import_path3.basename)(
|
|
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 "${
|
|
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
|
-
|
|
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
|
|
1438
|
-
return
|
|
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
|
-
|
|
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(
|
|
2071
|
-
const contentResult = await readFileContent(
|
|
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: ${
|
|
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:
|
|
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 ${
|
|
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(
|
|
2281
|
-
const contentResult = await readFileContent(
|
|
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: ${
|
|
2287
|
-
{ file:
|
|
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 =
|
|
2314
|
+
const type = path34.endsWith(".md") ? "markdown" : "text";
|
|
2294
2315
|
return (0, import_types.Ok)({
|
|
2295
|
-
path:
|
|
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"
|
|
2308
|
-
|
|
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
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2356
|
-
|
|
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:
|
|
2359
|
-
line:
|
|
2360
|
-
column:
|
|
2361
|
-
reference:
|
|
2362
|
-
context: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2780
|
-
|
|
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
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
for (const pattern of
|
|
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
|
|
3506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
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
|
|
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
|
-
|
|
4163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
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
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
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
|
-
|
|
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:
|
|
5207
|
+
id: nextId(),
|
|
5062
5208
|
category: "diff",
|
|
5063
|
-
check: `
|
|
5209
|
+
check: `Test coverage: ${sourceFile.path}`,
|
|
5064
5210
|
passed: false,
|
|
5065
5211
|
severity: "warning",
|
|
5066
|
-
details:
|
|
5067
|
-
|
|
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
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
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
|
-
|
|
5089
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
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
|
-
|
|
6151
|
-
|
|
6265
|
+
if (entry.isFile() && isTsSourceFile(entry.name)) {
|
|
6266
|
+
tsFiles.push(fullPath);
|
|
6152
6267
|
}
|
|
6153
6268
|
}
|
|
6154
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6316
|
-
|
|
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
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
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
|
|
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
|
|
6444
|
-
|
|
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
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
7489
|
-
|
|
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
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
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
|
-
|
|
7757
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
9492
|
-
return `phase: ${
|
|
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
|
|
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
|
|
9774
|
-
const
|
|
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
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
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
|
-
|
|
9795
|
-
|
|
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
|
|
9799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
12122
|
-
const
|
|
12123
|
-
|
|
12124
|
-
|
|
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
|
-
|
|
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*/)
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
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
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12731
|
-
const
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
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
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
14066
|
-
const feature = ms.features[featureIdx];
|
|
14297
|
+
for (const feature of ms.features) {
|
|
14067
14298
|
globalPosition++;
|
|
14068
|
-
if (feature
|
|
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
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
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
|
|
14434
|
-
const pkgDir =
|
|
14435
|
-
return
|
|
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
|
|
15162
|
-
if (!tokenUsage
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
15305
|
-
var
|
|
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,
|