@harness-engineering/core 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +2310 -0
- package/dist/index.d.ts +2310 -0
- package/dist/index.js +4077 -0
- package/dist/index.mjs +3957 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4077 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
AgentActionEmitter: () => AgentActionEmitter,
|
|
35
|
+
ChecklistBuilder: () => ChecklistBuilder,
|
|
36
|
+
ConsoleSink: () => ConsoleSink,
|
|
37
|
+
DEFAULT_STATE: () => DEFAULT_STATE,
|
|
38
|
+
EntropyAnalyzer: () => EntropyAnalyzer,
|
|
39
|
+
EntropyConfigSchema: () => EntropyConfigSchema,
|
|
40
|
+
Err: () => Err,
|
|
41
|
+
FailureEntrySchema: () => FailureEntrySchema,
|
|
42
|
+
FileSink: () => FileSink,
|
|
43
|
+
GateConfigSchema: () => GateConfigSchema,
|
|
44
|
+
GateResultSchema: () => GateResultSchema,
|
|
45
|
+
HandoffSchema: () => HandoffSchema,
|
|
46
|
+
HarnessStateSchema: () => HarnessStateSchema,
|
|
47
|
+
NoOpExecutor: () => NoOpExecutor,
|
|
48
|
+
NoOpSink: () => NoOpSink,
|
|
49
|
+
NoOpTelemetryAdapter: () => NoOpTelemetryAdapter,
|
|
50
|
+
Ok: () => Ok,
|
|
51
|
+
PatternConfigSchema: () => PatternConfigSchema,
|
|
52
|
+
REQUIRED_SECTIONS: () => REQUIRED_SECTIONS,
|
|
53
|
+
TypeScriptParser: () => TypeScriptParser,
|
|
54
|
+
VERSION: () => VERSION,
|
|
55
|
+
analyzeDiff: () => analyzeDiff,
|
|
56
|
+
appendFailure: () => appendFailure,
|
|
57
|
+
appendLearning: () => appendLearning,
|
|
58
|
+
applyFixes: () => applyFixes,
|
|
59
|
+
archiveFailures: () => archiveFailures,
|
|
60
|
+
buildDependencyGraph: () => buildDependencyGraph,
|
|
61
|
+
buildReachabilityMap: () => buildReachabilityMap,
|
|
62
|
+
buildSnapshot: () => buildSnapshot,
|
|
63
|
+
checkConfigPattern: () => checkConfigPattern,
|
|
64
|
+
checkDocCoverage: () => checkDocCoverage,
|
|
65
|
+
configureFeedback: () => configureFeedback,
|
|
66
|
+
contextBudget: () => contextBudget,
|
|
67
|
+
contextFilter: () => contextFilter,
|
|
68
|
+
createBoundaryValidator: () => createBoundaryValidator,
|
|
69
|
+
createError: () => createError,
|
|
70
|
+
createFixes: () => createFixes,
|
|
71
|
+
createParseError: () => createParseError,
|
|
72
|
+
createSelfReview: () => createSelfReview,
|
|
73
|
+
defineLayer: () => defineLayer,
|
|
74
|
+
detectCircularDeps: () => detectCircularDeps,
|
|
75
|
+
detectCircularDepsInFiles: () => detectCircularDepsInFiles,
|
|
76
|
+
detectDeadCode: () => detectDeadCode,
|
|
77
|
+
detectDocDrift: () => detectDocDrift,
|
|
78
|
+
detectPatternViolations: () => detectPatternViolations,
|
|
79
|
+
executeWorkflow: () => executeWorkflow,
|
|
80
|
+
extractMarkdownLinks: () => extractMarkdownLinks,
|
|
81
|
+
extractSections: () => extractSections,
|
|
82
|
+
findPossibleMatches: () => findPossibleMatches,
|
|
83
|
+
generateAgentsMap: () => generateAgentsMap,
|
|
84
|
+
generateSuggestions: () => generateSuggestions,
|
|
85
|
+
getActionEmitter: () => getActionEmitter,
|
|
86
|
+
getFeedbackConfig: () => getFeedbackConfig,
|
|
87
|
+
getPhaseCategories: () => getPhaseCategories,
|
|
88
|
+
isErr: () => isErr,
|
|
89
|
+
isOk: () => isOk,
|
|
90
|
+
levenshteinDistance: () => levenshteinDistance,
|
|
91
|
+
loadFailures: () => loadFailures,
|
|
92
|
+
loadHandoff: () => loadHandoff,
|
|
93
|
+
loadRelevantLearnings: () => loadRelevantLearnings,
|
|
94
|
+
loadState: () => loadState,
|
|
95
|
+
logAgentAction: () => logAgentAction,
|
|
96
|
+
parseDiff: () => parseDiff,
|
|
97
|
+
parseDocumentationFile: () => parseDocumentationFile,
|
|
98
|
+
previewFix: () => previewFix,
|
|
99
|
+
requestMultiplePeerReviews: () => requestMultiplePeerReviews,
|
|
100
|
+
requestPeerReview: () => requestPeerReview,
|
|
101
|
+
resetFeedbackConfig: () => resetFeedbackConfig,
|
|
102
|
+
resolveEntryPoints: () => resolveEntryPoints,
|
|
103
|
+
resolveFileToLayer: () => resolveFileToLayer,
|
|
104
|
+
runMechanicalGate: () => runMechanicalGate,
|
|
105
|
+
runMultiTurnPipeline: () => runMultiTurnPipeline,
|
|
106
|
+
runPipeline: () => runPipeline,
|
|
107
|
+
saveHandoff: () => saveHandoff,
|
|
108
|
+
saveState: () => saveState,
|
|
109
|
+
trackAction: () => trackAction,
|
|
110
|
+
validateAgentsMap: () => validateAgentsMap,
|
|
111
|
+
validateBoundaries: () => validateBoundaries,
|
|
112
|
+
validateCommitMessage: () => validateCommitMessage,
|
|
113
|
+
validateConfig: () => validateConfig,
|
|
114
|
+
validateDependencies: () => validateDependencies,
|
|
115
|
+
validateFileStructure: () => validateFileStructure,
|
|
116
|
+
validateKnowledgeMap: () => validateKnowledgeMap,
|
|
117
|
+
validatePatternConfig: () => validatePatternConfig
|
|
118
|
+
});
|
|
119
|
+
module.exports = __toCommonJS(index_exports);
|
|
120
|
+
__reExport(index_exports, require("@harness-engineering/types"), module.exports);
|
|
121
|
+
|
|
122
|
+
// src/shared/result.ts
|
|
123
|
+
var Ok = (value) => ({
|
|
124
|
+
ok: true,
|
|
125
|
+
value
|
|
126
|
+
});
|
|
127
|
+
function isOk(result) {
|
|
128
|
+
return result.ok === true;
|
|
129
|
+
}
|
|
130
|
+
var Err = (error) => ({
|
|
131
|
+
ok: false,
|
|
132
|
+
error
|
|
133
|
+
});
|
|
134
|
+
function isErr(result) {
|
|
135
|
+
return result.ok === false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/shared/errors.ts
|
|
139
|
+
function createError(code, message, details = {}, suggestions = []) {
|
|
140
|
+
return { code, message, details, suggestions };
|
|
141
|
+
}
|
|
142
|
+
function createEntropyError(code, message, details = {}, suggestions = []) {
|
|
143
|
+
return { code, message, details, suggestions };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/shared/fs-utils.ts
|
|
147
|
+
var import_fs = require("fs");
|
|
148
|
+
var import_util = require("util");
|
|
149
|
+
var import_glob = require("glob");
|
|
150
|
+
var accessAsync = (0, import_util.promisify)(import_fs.access);
|
|
151
|
+
var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
|
|
152
|
+
async function fileExists(path2) {
|
|
153
|
+
try {
|
|
154
|
+
await accessAsync(path2, import_fs.constants.F_OK);
|
|
155
|
+
return true;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function readFileContent(path2) {
|
|
161
|
+
try {
|
|
162
|
+
const content = await readFileAsync(path2, "utf-8");
|
|
163
|
+
return Ok(content);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return Err(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function findFiles(pattern, cwd = process.cwd()) {
|
|
169
|
+
return (0, import_glob.glob)(pattern, { cwd, absolute: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/validation/file-structure.ts
|
|
173
|
+
async function validateFileStructure(projectPath, conventions) {
|
|
174
|
+
const missing = [];
|
|
175
|
+
const unexpected = [];
|
|
176
|
+
let foundRequired = 0;
|
|
177
|
+
const totalRequired = conventions.filter((c) => c.required).length;
|
|
178
|
+
for (const convention of conventions) {
|
|
179
|
+
const files = await findFiles(convention.pattern, projectPath);
|
|
180
|
+
if (convention.required) {
|
|
181
|
+
if (files.length === 0) {
|
|
182
|
+
missing.push(convention.pattern);
|
|
183
|
+
} else {
|
|
184
|
+
foundRequired++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const conformance = totalRequired === 0 ? 100 : foundRequired / totalRequired * 100;
|
|
189
|
+
const validation = {
|
|
190
|
+
valid: missing.length === 0,
|
|
191
|
+
missing,
|
|
192
|
+
unexpected,
|
|
193
|
+
conformance
|
|
194
|
+
};
|
|
195
|
+
return Ok(validation);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/validation/config.ts
|
|
199
|
+
function validateConfig(data, schema) {
|
|
200
|
+
const result = schema.safeParse(data);
|
|
201
|
+
if (result.success) {
|
|
202
|
+
return Ok(result.data);
|
|
203
|
+
}
|
|
204
|
+
const zodErrors = result.error;
|
|
205
|
+
const firstError = zodErrors.errors[0];
|
|
206
|
+
let code = "VALIDATION_FAILED";
|
|
207
|
+
let message = "Configuration validation failed";
|
|
208
|
+
const suggestions = [];
|
|
209
|
+
if (firstError) {
|
|
210
|
+
const path2 = firstError.path.join(".");
|
|
211
|
+
const pathDisplay = path2 ? ` at "${path2}"` : "";
|
|
212
|
+
if (firstError.code === "invalid_type") {
|
|
213
|
+
const received = firstError.received;
|
|
214
|
+
const expected = firstError.expected;
|
|
215
|
+
if (received === "undefined") {
|
|
216
|
+
code = "MISSING_FIELD";
|
|
217
|
+
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
218
|
+
suggestions.push(`Field "${path2}" is required and must be of type "${expected}"`);
|
|
219
|
+
} else {
|
|
220
|
+
code = "INVALID_TYPE";
|
|
221
|
+
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
222
|
+
suggestions.push(`Expected ${expected} but got ${received}`);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
code = "VALIDATION_FAILED";
|
|
226
|
+
message = `${firstError.message}${pathDisplay}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const error = createError(
|
|
230
|
+
code,
|
|
231
|
+
message,
|
|
232
|
+
{
|
|
233
|
+
zodError: zodErrors,
|
|
234
|
+
path: firstError?.path
|
|
235
|
+
},
|
|
236
|
+
suggestions
|
|
237
|
+
);
|
|
238
|
+
return Err(error);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/validation/commit-message.ts
|
|
242
|
+
var VALID_TYPES = [
|
|
243
|
+
"feat",
|
|
244
|
+
"fix",
|
|
245
|
+
"docs",
|
|
246
|
+
"style",
|
|
247
|
+
"refactor",
|
|
248
|
+
"perf",
|
|
249
|
+
"test",
|
|
250
|
+
"build",
|
|
251
|
+
"ci",
|
|
252
|
+
"chore",
|
|
253
|
+
"revert"
|
|
254
|
+
];
|
|
255
|
+
var CONVENTIONAL_PATTERN = /^(\w+)(\(([^)]+)\))?(!)?: (.+)$/;
|
|
256
|
+
function validateCommitMessage(message, format = "conventional") {
|
|
257
|
+
if (!message || typeof message !== "string") {
|
|
258
|
+
const error = createError(
|
|
259
|
+
"VALIDATION_FAILED",
|
|
260
|
+
"Commit message must be a non-empty string",
|
|
261
|
+
{ message },
|
|
262
|
+
["Provide a valid commit message"]
|
|
263
|
+
);
|
|
264
|
+
return Err(error);
|
|
265
|
+
}
|
|
266
|
+
if (format === "conventional" || format === "angular") {
|
|
267
|
+
return validateConventionalCommit(message);
|
|
268
|
+
}
|
|
269
|
+
return Ok({
|
|
270
|
+
valid: true,
|
|
271
|
+
breaking: false,
|
|
272
|
+
issues: []
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function validateConventionalCommit(message) {
|
|
276
|
+
const lines = message.split("\n");
|
|
277
|
+
const headerLine = lines[0];
|
|
278
|
+
if (!headerLine) {
|
|
279
|
+
const error = createError(
|
|
280
|
+
"VALIDATION_FAILED",
|
|
281
|
+
"Commit message header cannot be empty",
|
|
282
|
+
{ message },
|
|
283
|
+
["Provide a commit message with at least a header line"]
|
|
284
|
+
);
|
|
285
|
+
return Err(error);
|
|
286
|
+
}
|
|
287
|
+
const match = headerLine.match(CONVENTIONAL_PATTERN);
|
|
288
|
+
if (!match) {
|
|
289
|
+
const error = createError(
|
|
290
|
+
"VALIDATION_FAILED",
|
|
291
|
+
"Commit message does not follow conventional format",
|
|
292
|
+
{ message, header: headerLine },
|
|
293
|
+
[
|
|
294
|
+
"Use format: type(scope)?: description",
|
|
295
|
+
"Valid types: " + VALID_TYPES.join(", "),
|
|
296
|
+
"Example: feat(core): add new feature"
|
|
297
|
+
]
|
|
298
|
+
);
|
|
299
|
+
return Err(error);
|
|
300
|
+
}
|
|
301
|
+
const type = match[1];
|
|
302
|
+
const scope = match[3];
|
|
303
|
+
const breaking = match[4] === "!";
|
|
304
|
+
const description = match[5];
|
|
305
|
+
const issues = [];
|
|
306
|
+
if (!VALID_TYPES.includes(type)) {
|
|
307
|
+
issues.push(`Invalid commit type "${type}". Valid types: ${VALID_TYPES.join(", ")}`);
|
|
308
|
+
}
|
|
309
|
+
if (!description || description.trim() === "") {
|
|
310
|
+
issues.push("Commit description cannot be empty");
|
|
311
|
+
}
|
|
312
|
+
let hasBreakingChange = breaking;
|
|
313
|
+
if (lines.length > 1) {
|
|
314
|
+
const body = lines.slice(1).join("\n");
|
|
315
|
+
if (body.includes("BREAKING CHANGE:")) {
|
|
316
|
+
hasBreakingChange = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (issues.length > 0) {
|
|
320
|
+
let errorMessage = `Commit message validation failed: ${issues.join("; ")}`;
|
|
321
|
+
if (issues.some((issue) => issue.includes("description cannot be empty"))) {
|
|
322
|
+
errorMessage = `Commit message validation failed: ${issues.join("; ")}`;
|
|
323
|
+
}
|
|
324
|
+
const error = createError(
|
|
325
|
+
"VALIDATION_FAILED",
|
|
326
|
+
errorMessage,
|
|
327
|
+
{ message, issues, type, scope },
|
|
328
|
+
["Review and fix the validation issues above"]
|
|
329
|
+
);
|
|
330
|
+
return Err(error);
|
|
331
|
+
}
|
|
332
|
+
const result = {
|
|
333
|
+
valid: true,
|
|
334
|
+
type,
|
|
335
|
+
...scope && { scope },
|
|
336
|
+
breaking: hasBreakingChange,
|
|
337
|
+
issues: []
|
|
338
|
+
};
|
|
339
|
+
return Ok(result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/context/types.ts
|
|
343
|
+
var REQUIRED_SECTIONS = [
|
|
344
|
+
"Project Overview",
|
|
345
|
+
"Repository Structure",
|
|
346
|
+
"Development Workflow"
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
// src/context/agents-map.ts
|
|
350
|
+
var import_path = require("path");
|
|
351
|
+
function extractMarkdownLinks(content) {
|
|
352
|
+
const links = [];
|
|
353
|
+
const lines = content.split("\n");
|
|
354
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
355
|
+
for (let i = 0; i < lines.length; i++) {
|
|
356
|
+
const line = lines[i] ?? "";
|
|
357
|
+
let match;
|
|
358
|
+
linkPattern.lastIndex = 0;
|
|
359
|
+
while ((match = linkPattern.exec(line)) !== null) {
|
|
360
|
+
if (match[1] && match[2]) {
|
|
361
|
+
links.push({
|
|
362
|
+
text: match[1],
|
|
363
|
+
path: match[2],
|
|
364
|
+
line: i + 1
|
|
365
|
+
// 1-indexed
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return links;
|
|
371
|
+
}
|
|
372
|
+
function extractSections(content) {
|
|
373
|
+
const lines = content.split("\n");
|
|
374
|
+
const sections = [];
|
|
375
|
+
const headingPattern = /^(#{1,6})\s+(.+)$/;
|
|
376
|
+
for (let i = 0; i < lines.length; i++) {
|
|
377
|
+
const line = lines[i] ?? "";
|
|
378
|
+
const match = line.match(headingPattern);
|
|
379
|
+
if (match && match[1] && match[2]) {
|
|
380
|
+
sections.push({
|
|
381
|
+
title: match[2].trim(),
|
|
382
|
+
level: match[1].length,
|
|
383
|
+
line: i + 1,
|
|
384
|
+
// 1-indexed
|
|
385
|
+
startIndex: i
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (let i = 0; i < sections.length; i++) {
|
|
390
|
+
const currentSection = sections[i];
|
|
391
|
+
const nextSection = sections[i + 1];
|
|
392
|
+
if (currentSection) {
|
|
393
|
+
currentSection.endIndex = nextSection ? nextSection.startIndex : lines.length;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return sections.map((section) => {
|
|
397
|
+
const endIndex = section.endIndex ?? lines.length;
|
|
398
|
+
const sectionLines = lines.slice(section.startIndex + 1, endIndex);
|
|
399
|
+
const sectionContent = sectionLines.join("\n");
|
|
400
|
+
const links = extractMarkdownLinks(sectionContent).map((link) => ({
|
|
401
|
+
...link,
|
|
402
|
+
line: link.line + section.startIndex + 1,
|
|
403
|
+
// Adjust line number
|
|
404
|
+
exists: false
|
|
405
|
+
// Will be set later by validateAgentsMap
|
|
406
|
+
}));
|
|
407
|
+
const descriptionLines = [];
|
|
408
|
+
for (const line of sectionLines) {
|
|
409
|
+
const trimmed = line.trim();
|
|
410
|
+
if (trimmed === "") {
|
|
411
|
+
if (descriptionLines.length > 0) break;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (trimmed.startsWith("#")) break;
|
|
415
|
+
if (trimmed.startsWith("-") || trimmed.startsWith("*")) break;
|
|
416
|
+
if (trimmed.startsWith("```")) break;
|
|
417
|
+
descriptionLines.push(trimmed);
|
|
418
|
+
}
|
|
419
|
+
const result = {
|
|
420
|
+
title: section.title,
|
|
421
|
+
level: section.level,
|
|
422
|
+
line: section.line,
|
|
423
|
+
links
|
|
424
|
+
};
|
|
425
|
+
if (descriptionLines.length > 0) {
|
|
426
|
+
result.description = descriptionLines.join(" ");
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function isExternalLink(path2) {
|
|
432
|
+
return path2.startsWith("http://") || path2.startsWith("https://") || path2.startsWith("#") || path2.startsWith("mailto:");
|
|
433
|
+
}
|
|
434
|
+
function resolveLinkPath(linkPath, baseDir) {
|
|
435
|
+
return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
|
|
436
|
+
}
|
|
437
|
+
async function validateAgentsMap(path2 = "./AGENTS.md") {
|
|
438
|
+
const contentResult = await readFileContent(path2);
|
|
439
|
+
if (!contentResult.ok) {
|
|
440
|
+
return Err(
|
|
441
|
+
createError(
|
|
442
|
+
"PARSE_ERROR",
|
|
443
|
+
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
444
|
+
{ path: path2 },
|
|
445
|
+
["Ensure the file exists", "Check file permissions"]
|
|
446
|
+
)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const content = contentResult.value;
|
|
450
|
+
const sections = extractSections(content);
|
|
451
|
+
const baseDir = (0, import_path.dirname)(path2);
|
|
452
|
+
const sectionTitles = sections.map((s) => s.title);
|
|
453
|
+
const missingSections = REQUIRED_SECTIONS.filter(
|
|
454
|
+
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
455
|
+
);
|
|
456
|
+
const allLinks = [];
|
|
457
|
+
const brokenLinks = [];
|
|
458
|
+
for (const section of sections) {
|
|
459
|
+
for (const link of section.links) {
|
|
460
|
+
if (isExternalLink(link.path)) {
|
|
461
|
+
const externalLink = { ...link, exists: true };
|
|
462
|
+
allLinks.push(externalLink);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const absolutePath = resolveLinkPath(link.path, baseDir);
|
|
466
|
+
const exists = await fileExists(absolutePath);
|
|
467
|
+
const fullLink = { ...link, exists };
|
|
468
|
+
allLinks.push(fullLink);
|
|
469
|
+
if (!exists) {
|
|
470
|
+
brokenLinks.push(fullLink);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
section.links = section.links.map(
|
|
474
|
+
(link) => allLinks.find((l) => l.path === link.path && l.line === link.line) || {
|
|
475
|
+
...link,
|
|
476
|
+
exists: false
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const valid = missingSections.length === 0 && brokenLinks.length === 0;
|
|
481
|
+
return Ok({
|
|
482
|
+
valid,
|
|
483
|
+
sections,
|
|
484
|
+
totalLinks: allLinks.length,
|
|
485
|
+
brokenLinks,
|
|
486
|
+
missingSections: [...missingSections]
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/context/doc-coverage.ts
|
|
491
|
+
var import_path2 = require("path");
|
|
492
|
+
function determineImportance(filePath) {
|
|
493
|
+
const name = (0, import_path2.basename)(filePath).toLowerCase();
|
|
494
|
+
if (name === "index.ts" || name === "index.js" || name === "main.ts") {
|
|
495
|
+
return "high";
|
|
496
|
+
}
|
|
497
|
+
if (name.includes("types") || name.endsWith(".d.ts")) {
|
|
498
|
+
return "high";
|
|
499
|
+
}
|
|
500
|
+
if (name.includes(".test.") || name.includes(".spec.") || name.includes("config")) {
|
|
501
|
+
return "low";
|
|
502
|
+
}
|
|
503
|
+
return "medium";
|
|
504
|
+
}
|
|
505
|
+
function suggestSection(filePath, domain) {
|
|
506
|
+
const name = (0, import_path2.basename)(filePath).toLowerCase();
|
|
507
|
+
if (name.includes("types")) return "API Types";
|
|
508
|
+
if (name === "index.ts" || name === "index.js") return "Module Exports";
|
|
509
|
+
if (name.includes("util")) return "Utilities";
|
|
510
|
+
return `${domain} Reference`;
|
|
511
|
+
}
|
|
512
|
+
async function checkDocCoverage(domain, options = {}) {
|
|
513
|
+
const { docsDir = "./docs", sourceDir = "./src", excludePatterns = [] } = options;
|
|
514
|
+
try {
|
|
515
|
+
const sourceFiles = await findFiles("**/*.{ts,js,tsx,jsx}", sourceDir);
|
|
516
|
+
const filteredSourceFiles = sourceFiles.filter((file) => {
|
|
517
|
+
const relativePath = (0, import_path2.relative)(sourceDir, file);
|
|
518
|
+
return !excludePatterns.some((pattern) => {
|
|
519
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
520
|
+
const regex = new RegExp("^" + escaped + "$");
|
|
521
|
+
return regex.test(relativePath) || regex.test(file);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
const docFiles = await findFiles("**/*.md", docsDir);
|
|
525
|
+
const documentedPaths = /* @__PURE__ */ new Set();
|
|
526
|
+
for (const docFile of docFiles) {
|
|
527
|
+
const contentResult = await readFileContent(docFile);
|
|
528
|
+
if (contentResult.ok) {
|
|
529
|
+
const links = extractMarkdownLinks(contentResult.value);
|
|
530
|
+
for (const link of links) {
|
|
531
|
+
const normalizedPath = link.path.replace(/^\.\.\//, "").replace(/^\.\//, "");
|
|
532
|
+
documentedPaths.add(normalizedPath);
|
|
533
|
+
const linkBasename = (0, import_path2.basename)(link.path);
|
|
534
|
+
documentedPaths.add(linkBasename);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const documented = [];
|
|
539
|
+
const undocumented = [];
|
|
540
|
+
const gaps = [];
|
|
541
|
+
for (const sourceFile of filteredSourceFiles) {
|
|
542
|
+
const relativePath = (0, import_path2.relative)(sourceDir, sourceFile);
|
|
543
|
+
const fileName = (0, import_path2.basename)(sourceFile);
|
|
544
|
+
const isDocumented = documentedPaths.has(relativePath) || documentedPaths.has(fileName) || documentedPaths.has(`src/${relativePath}`);
|
|
545
|
+
if (isDocumented) {
|
|
546
|
+
documented.push(relativePath);
|
|
547
|
+
} else {
|
|
548
|
+
undocumented.push(relativePath);
|
|
549
|
+
gaps.push({
|
|
550
|
+
file: relativePath,
|
|
551
|
+
suggestedSection: suggestSection(sourceFile, domain),
|
|
552
|
+
importance: determineImportance(sourceFile)
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const total = documented.length + undocumented.length;
|
|
557
|
+
const coveragePercentage = total > 0 ? Math.round(documented.length / total * 100) : 100;
|
|
558
|
+
return Ok({
|
|
559
|
+
domain,
|
|
560
|
+
documented,
|
|
561
|
+
undocumented,
|
|
562
|
+
coveragePercentage,
|
|
563
|
+
gaps
|
|
564
|
+
});
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return Err(
|
|
567
|
+
createError(
|
|
568
|
+
"PARSE_ERROR",
|
|
569
|
+
`Failed to analyze documentation coverage: ${error.message}`,
|
|
570
|
+
{ domain, docsDir, sourceDir },
|
|
571
|
+
["Ensure directories exist", "Check file permissions"]
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/context/knowledge-map.ts
|
|
578
|
+
var import_path3 = require("path");
|
|
579
|
+
function suggestFix(path2, existingFiles) {
|
|
580
|
+
const targetName = (0, import_path3.basename)(path2).toLowerCase();
|
|
581
|
+
const similar = existingFiles.find((file) => {
|
|
582
|
+
const fileName = (0, import_path3.basename)(file).toLowerCase();
|
|
583
|
+
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
584
|
+
});
|
|
585
|
+
if (similar) {
|
|
586
|
+
return `Did you mean "${similar}"?`;
|
|
587
|
+
}
|
|
588
|
+
return `Create the file "${path2}" or remove the link`;
|
|
589
|
+
}
|
|
590
|
+
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
591
|
+
const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
|
|
592
|
+
const agentsResult = await validateAgentsMap(agentsPath);
|
|
593
|
+
if (!agentsResult.ok) {
|
|
594
|
+
return Err(agentsResult.error);
|
|
595
|
+
}
|
|
596
|
+
const {
|
|
597
|
+
sections,
|
|
598
|
+
brokenLinks: agentsBrokenLinks,
|
|
599
|
+
totalLinks: agentsTotalLinks
|
|
600
|
+
} = agentsResult.value;
|
|
601
|
+
const existingFiles = await findFiles("**/*", rootDir);
|
|
602
|
+
const relativeExistingFiles = existingFiles.map((f) => (0, import_path3.relative)(rootDir, f));
|
|
603
|
+
const brokenLinks = agentsBrokenLinks.map((link) => {
|
|
604
|
+
const section = sections.find(
|
|
605
|
+
(s) => s.links.some((l) => l.path === link.path && l.line === link.line)
|
|
606
|
+
);
|
|
607
|
+
return {
|
|
608
|
+
text: link.text,
|
|
609
|
+
path: link.path,
|
|
610
|
+
line: link.line,
|
|
611
|
+
section: section?.title || "Unknown",
|
|
612
|
+
reason: "NOT_FOUND",
|
|
613
|
+
suggestion: suggestFix(link.path, relativeExistingFiles)
|
|
614
|
+
};
|
|
615
|
+
});
|
|
616
|
+
const validLinks = agentsTotalLinks - brokenLinks.length;
|
|
617
|
+
const integrity = agentsTotalLinks > 0 ? Math.round(validLinks / agentsTotalLinks * 100) : 100;
|
|
618
|
+
return Ok({
|
|
619
|
+
totalLinks: agentsTotalLinks,
|
|
620
|
+
brokenLinks,
|
|
621
|
+
validLinks,
|
|
622
|
+
integrity
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/context/generate.ts
|
|
627
|
+
var import_path4 = require("path");
|
|
628
|
+
var DEFAULT_SECTIONS = [
|
|
629
|
+
{
|
|
630
|
+
name: "Documentation",
|
|
631
|
+
pattern: "docs/**/*.md",
|
|
632
|
+
description: "Project documentation"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "Source Code",
|
|
636
|
+
pattern: "src/**/*.ts",
|
|
637
|
+
description: "Source code modules"
|
|
638
|
+
}
|
|
639
|
+
];
|
|
640
|
+
function groupByDirectory(files, rootDir) {
|
|
641
|
+
const groups = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const file of files) {
|
|
643
|
+
const relativePath = (0, import_path4.relative)(rootDir, file);
|
|
644
|
+
const dir = (0, import_path4.dirname)(relativePath);
|
|
645
|
+
if (!groups.has(dir)) {
|
|
646
|
+
groups.set(dir, []);
|
|
647
|
+
}
|
|
648
|
+
groups.get(dir).push(relativePath);
|
|
649
|
+
}
|
|
650
|
+
return groups;
|
|
651
|
+
}
|
|
652
|
+
function formatFileLink(filePath) {
|
|
653
|
+
const ext = filePath.match(/\.[^.]+$/)?.[0] || "";
|
|
654
|
+
const name = (0, import_path4.basename)(filePath, ext).replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
655
|
+
return `- [${name}](./${filePath})`;
|
|
656
|
+
}
|
|
657
|
+
function matchesExcludePattern(relativePath, excludePatterns) {
|
|
658
|
+
return excludePatterns.some((pattern) => {
|
|
659
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
660
|
+
const regex = new RegExp("^" + escaped + "$");
|
|
661
|
+
return regex.test(relativePath);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async function generateAgentsMap(config) {
|
|
665
|
+
const { rootDir, includePaths, excludePaths, sections = DEFAULT_SECTIONS } = config;
|
|
666
|
+
try {
|
|
667
|
+
const allFiles = [];
|
|
668
|
+
for (const pattern of includePaths) {
|
|
669
|
+
const files = await findFiles(pattern, rootDir);
|
|
670
|
+
allFiles.push(...files);
|
|
671
|
+
}
|
|
672
|
+
const filteredFiles = allFiles.filter((file) => {
|
|
673
|
+
const relativePath = (0, import_path4.relative)(rootDir, file);
|
|
674
|
+
return !matchesExcludePattern(relativePath, excludePaths);
|
|
675
|
+
});
|
|
676
|
+
const lines = [];
|
|
677
|
+
lines.push("# AI Agent Knowledge Map");
|
|
678
|
+
lines.push("");
|
|
679
|
+
lines.push("This is the single source of truth for AI agents working on this project.");
|
|
680
|
+
lines.push("");
|
|
681
|
+
lines.push("## Project Overview");
|
|
682
|
+
lines.push("");
|
|
683
|
+
lines.push("> Add a brief description of this project, its purpose, and key technologies.");
|
|
684
|
+
lines.push("");
|
|
685
|
+
lines.push("## Repository Structure");
|
|
686
|
+
lines.push("");
|
|
687
|
+
const grouped = groupByDirectory(filteredFiles, rootDir);
|
|
688
|
+
for (const [dir, files] of grouped) {
|
|
689
|
+
if (dir !== ".") {
|
|
690
|
+
lines.push(`### ${dir}/`);
|
|
691
|
+
lines.push("");
|
|
692
|
+
}
|
|
693
|
+
for (const file of files.slice(0, 10)) {
|
|
694
|
+
lines.push(formatFileLink(file));
|
|
695
|
+
}
|
|
696
|
+
if (files.length > 10) {
|
|
697
|
+
lines.push(`- _... and ${files.length - 10} more files_`);
|
|
698
|
+
}
|
|
699
|
+
lines.push("");
|
|
700
|
+
}
|
|
701
|
+
for (const section of sections) {
|
|
702
|
+
lines.push(`## ${section.name}`);
|
|
703
|
+
lines.push("");
|
|
704
|
+
if (section.description) {
|
|
705
|
+
lines.push(section.description);
|
|
706
|
+
lines.push("");
|
|
707
|
+
}
|
|
708
|
+
const sectionFiles = await findFiles(section.pattern, rootDir);
|
|
709
|
+
const filteredSectionFiles = sectionFiles.filter((file) => {
|
|
710
|
+
const relativePath = (0, import_path4.relative)(rootDir, file);
|
|
711
|
+
return !matchesExcludePattern(relativePath, excludePaths);
|
|
712
|
+
});
|
|
713
|
+
for (const file of filteredSectionFiles.slice(0, 20)) {
|
|
714
|
+
lines.push(formatFileLink((0, import_path4.relative)(rootDir, file)));
|
|
715
|
+
}
|
|
716
|
+
if (filteredSectionFiles.length > 20) {
|
|
717
|
+
lines.push(`- _... and ${filteredSectionFiles.length - 20} more files_`);
|
|
718
|
+
}
|
|
719
|
+
lines.push("");
|
|
720
|
+
}
|
|
721
|
+
lines.push("## Development Workflow");
|
|
722
|
+
lines.push("");
|
|
723
|
+
lines.push(
|
|
724
|
+
"> Document your development workflow: branching strategy, testing commands, deployment process."
|
|
725
|
+
);
|
|
726
|
+
lines.push("");
|
|
727
|
+
return Ok(lines.join("\n"));
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return Err(
|
|
730
|
+
createError(
|
|
731
|
+
"PARSE_ERROR",
|
|
732
|
+
`Failed to generate AGENTS.md: ${error.message}`,
|
|
733
|
+
{ rootDir },
|
|
734
|
+
["Check directory permissions", "Ensure glob patterns are valid"]
|
|
735
|
+
)
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/context/budget.ts
|
|
741
|
+
var DEFAULT_RATIOS = {
|
|
742
|
+
systemPrompt: 0.15,
|
|
743
|
+
projectManifest: 0.05,
|
|
744
|
+
taskSpec: 0.2,
|
|
745
|
+
activeCode: 0.4,
|
|
746
|
+
interfaces: 0.1,
|
|
747
|
+
reserve: 0.1
|
|
748
|
+
};
|
|
749
|
+
function contextBudget(totalTokens, overrides) {
|
|
750
|
+
const ratios = {
|
|
751
|
+
systemPrompt: DEFAULT_RATIOS.systemPrompt,
|
|
752
|
+
projectManifest: DEFAULT_RATIOS.projectManifest,
|
|
753
|
+
taskSpec: DEFAULT_RATIOS.taskSpec,
|
|
754
|
+
activeCode: DEFAULT_RATIOS.activeCode,
|
|
755
|
+
interfaces: DEFAULT_RATIOS.interfaces,
|
|
756
|
+
reserve: DEFAULT_RATIOS.reserve
|
|
757
|
+
};
|
|
758
|
+
if (overrides) {
|
|
759
|
+
let overrideSum = 0;
|
|
760
|
+
const overrideKeys = [];
|
|
761
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
762
|
+
if (value !== void 0 && key in ratios) {
|
|
763
|
+
const k = key;
|
|
764
|
+
ratios[k] = value;
|
|
765
|
+
overrideSum += value;
|
|
766
|
+
overrideKeys.push(k);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (overrideKeys.length > 0 && overrideKeys.length < 6) {
|
|
770
|
+
const remaining = 1 - overrideSum;
|
|
771
|
+
const nonOverridden = Object.keys(DEFAULT_RATIOS).filter(
|
|
772
|
+
(k) => !overrideKeys.includes(k)
|
|
773
|
+
);
|
|
774
|
+
const originalSum = nonOverridden.reduce((sum, k) => sum + DEFAULT_RATIOS[k], 0);
|
|
775
|
+
for (const k of nonOverridden) {
|
|
776
|
+
ratios[k] = remaining * (DEFAULT_RATIOS[k] / originalSum);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
total: totalTokens,
|
|
782
|
+
systemPrompt: Math.floor(totalTokens * ratios.systemPrompt),
|
|
783
|
+
projectManifest: Math.floor(totalTokens * ratios.projectManifest),
|
|
784
|
+
taskSpec: Math.floor(totalTokens * ratios.taskSpec),
|
|
785
|
+
activeCode: Math.floor(totalTokens * ratios.activeCode),
|
|
786
|
+
interfaces: Math.floor(totalTokens * ratios.interfaces),
|
|
787
|
+
reserve: Math.floor(totalTokens * ratios.reserve)
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/context/filter.ts
|
|
792
|
+
var PHASE_PRIORITIES = {
|
|
793
|
+
implement: [
|
|
794
|
+
{ category: "source", patterns: ["src/**/*.ts", "src/**/*.tsx"], priority: 1 },
|
|
795
|
+
{
|
|
796
|
+
category: "types",
|
|
797
|
+
patterns: ["src/**/types.ts", "src/**/interfaces.ts", "**/*.d.ts"],
|
|
798
|
+
priority: 2
|
|
799
|
+
},
|
|
800
|
+
{ category: "tests", patterns: ["tests/**/*.test.ts", "**/*.spec.ts"], priority: 3 },
|
|
801
|
+
{ category: "specs", patterns: ["docs/specs/**/*.md"], priority: 4 },
|
|
802
|
+
{ category: "config", patterns: ["package.json", "tsconfig.json"], priority: 5 }
|
|
803
|
+
],
|
|
804
|
+
review: [
|
|
805
|
+
{ category: "diff", patterns: [], priority: 1 },
|
|
806
|
+
// Diff is provided, not globbed
|
|
807
|
+
{ category: "specs", patterns: ["docs/specs/**/*.md", "docs/plans/**/*.md"], priority: 2 },
|
|
808
|
+
{ category: "learnings", patterns: [".harness/review-learnings.md"], priority: 3 },
|
|
809
|
+
{ category: "types", patterns: ["src/**/types.ts", "src/**/interfaces.ts"], priority: 4 },
|
|
810
|
+
{ category: "tests", patterns: ["tests/**/*.test.ts"], priority: 5 }
|
|
811
|
+
],
|
|
812
|
+
debug: [
|
|
813
|
+
{ category: "source", patterns: ["src/**/*.ts"], priority: 1 },
|
|
814
|
+
{ category: "tests", patterns: ["tests/**/*.test.ts"], priority: 2 },
|
|
815
|
+
{ category: "antipatterns", patterns: [".harness/anti-patterns.md"], priority: 3 },
|
|
816
|
+
{ category: "config", patterns: ["package.json", "tsconfig.json", ".env*"], priority: 4 },
|
|
817
|
+
{ category: "types", patterns: ["src/**/types.ts"], priority: 5 }
|
|
818
|
+
],
|
|
819
|
+
plan: [
|
|
820
|
+
{ category: "specs", patterns: ["docs/specs/**/*.md", "docs/plans/**/*.md"], priority: 1 },
|
|
821
|
+
{ category: "architecture", patterns: ["AGENTS.md", "docs/standard/**/*.md"], priority: 2 },
|
|
822
|
+
{ category: "handoffs", patterns: [".harness/handoff.md"], priority: 3 },
|
|
823
|
+
{ category: "types", patterns: ["src/**/types.ts", "src/**/interfaces.ts"], priority: 4 },
|
|
824
|
+
{ category: "config", patterns: ["harness.config.json", "package.json"], priority: 5 }
|
|
825
|
+
]
|
|
826
|
+
};
|
|
827
|
+
function contextFilter(phase, maxCategories) {
|
|
828
|
+
const categories = PHASE_PRIORITIES[phase];
|
|
829
|
+
const limit = maxCategories ?? categories.length;
|
|
830
|
+
const included = categories.slice(0, limit);
|
|
831
|
+
const excluded = categories.slice(limit);
|
|
832
|
+
return {
|
|
833
|
+
phase,
|
|
834
|
+
includedCategories: included.map((c) => c.category),
|
|
835
|
+
excludedCategories: excluded.map((c) => c.category),
|
|
836
|
+
filePatterns: included.flatMap((c) => c.patterns)
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function getPhaseCategories(phase) {
|
|
840
|
+
return [...PHASE_PRIORITIES[phase]];
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/constraints/layers.ts
|
|
844
|
+
var import_minimatch = require("minimatch");
|
|
845
|
+
function defineLayer(name, patterns, allowedDependencies) {
|
|
846
|
+
return {
|
|
847
|
+
name,
|
|
848
|
+
patterns,
|
|
849
|
+
allowedDependencies
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function resolveFileToLayer(file, layers) {
|
|
853
|
+
for (const layer of layers) {
|
|
854
|
+
for (const pattern of layer.patterns) {
|
|
855
|
+
if ((0, import_minimatch.minimatch)(file, pattern)) {
|
|
856
|
+
return layer;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return void 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/constraints/dependencies.ts
|
|
864
|
+
var import_path5 = require("path");
|
|
865
|
+
function resolveImportPath(importSource, fromFile, _rootDir) {
|
|
866
|
+
if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
const fromDir = (0, import_path5.dirname)(fromFile);
|
|
870
|
+
let resolved = (0, import_path5.resolve)(fromDir, importSource);
|
|
871
|
+
if (!resolved.endsWith(".ts") && !resolved.endsWith(".tsx")) {
|
|
872
|
+
resolved = resolved + ".ts";
|
|
873
|
+
}
|
|
874
|
+
return resolved;
|
|
875
|
+
}
|
|
876
|
+
function getImportType(imp) {
|
|
877
|
+
if (imp.kind === "type") return "type-only";
|
|
878
|
+
return "static";
|
|
879
|
+
}
|
|
880
|
+
async function buildDependencyGraph(files, parser) {
|
|
881
|
+
const nodes = [...files];
|
|
882
|
+
const edges = [];
|
|
883
|
+
for (const file of files) {
|
|
884
|
+
const parseResult = await parser.parseFile(file);
|
|
885
|
+
if (!parseResult.ok) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const importsResult = parser.extractImports(parseResult.value);
|
|
889
|
+
if (!importsResult.ok) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
for (const imp of importsResult.value) {
|
|
893
|
+
const resolvedPath = resolveImportPath(imp.source, file, "");
|
|
894
|
+
if (resolvedPath) {
|
|
895
|
+
edges.push({
|
|
896
|
+
from: file,
|
|
897
|
+
to: resolvedPath,
|
|
898
|
+
importType: getImportType(imp),
|
|
899
|
+
line: imp.location.line
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return Ok({ nodes, edges });
|
|
905
|
+
}
|
|
906
|
+
function checkLayerViolations(graph, layers, rootDir) {
|
|
907
|
+
const violations = [];
|
|
908
|
+
for (const edge of graph.edges) {
|
|
909
|
+
const fromRelative = (0, import_path5.relative)(rootDir, edge.from);
|
|
910
|
+
const toRelative = (0, import_path5.relative)(rootDir, edge.to);
|
|
911
|
+
const fromLayer = resolveFileToLayer(fromRelative, layers);
|
|
912
|
+
const toLayer = resolveFileToLayer(toRelative, layers);
|
|
913
|
+
if (!fromLayer || !toLayer) continue;
|
|
914
|
+
if (fromLayer.name === toLayer.name) continue;
|
|
915
|
+
if (!fromLayer.allowedDependencies.includes(toLayer.name)) {
|
|
916
|
+
violations.push({
|
|
917
|
+
file: edge.from,
|
|
918
|
+
imports: edge.to,
|
|
919
|
+
fromLayer: fromLayer.name,
|
|
920
|
+
toLayer: toLayer.name,
|
|
921
|
+
reason: "WRONG_LAYER",
|
|
922
|
+
line: edge.line,
|
|
923
|
+
suggestion: `Move the dependency to an allowed layer (${fromLayer.allowedDependencies.join(", ") || "none"}) or update layer rules`
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return violations;
|
|
928
|
+
}
|
|
929
|
+
async function validateDependencies(config) {
|
|
930
|
+
const { layers, rootDir, parser, fallbackBehavior = "error" } = config;
|
|
931
|
+
const healthResult = await parser.health();
|
|
932
|
+
if (!healthResult.ok || !healthResult.value.available) {
|
|
933
|
+
if (fallbackBehavior === "skip") {
|
|
934
|
+
return Ok({
|
|
935
|
+
valid: true,
|
|
936
|
+
violations: [],
|
|
937
|
+
graph: { nodes: [], edges: [] },
|
|
938
|
+
skipped: true,
|
|
939
|
+
reason: "Parser unavailable"
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
if (fallbackBehavior === "warn") {
|
|
943
|
+
console.warn(`Parser ${parser.name} unavailable, skipping validation`);
|
|
944
|
+
return Ok({
|
|
945
|
+
valid: true,
|
|
946
|
+
violations: [],
|
|
947
|
+
graph: { nodes: [], edges: [] },
|
|
948
|
+
skipped: true,
|
|
949
|
+
reason: "Parser unavailable"
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return Err(
|
|
953
|
+
createError(
|
|
954
|
+
"PARSER_UNAVAILABLE",
|
|
955
|
+
`Parser ${parser.name} is not available`,
|
|
956
|
+
{ parser: parser.name },
|
|
957
|
+
["Install required runtime", "Use different parser", 'Set fallbackBehavior: "skip"']
|
|
958
|
+
)
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
const allFiles = [];
|
|
962
|
+
for (const layer of layers) {
|
|
963
|
+
for (const pattern of layer.patterns) {
|
|
964
|
+
const files = await findFiles(pattern, rootDir);
|
|
965
|
+
allFiles.push(...files);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const uniqueFiles = [...new Set(allFiles)];
|
|
969
|
+
const graphResult = await buildDependencyGraph(uniqueFiles, parser);
|
|
970
|
+
if (!graphResult.ok) {
|
|
971
|
+
return Err(graphResult.error);
|
|
972
|
+
}
|
|
973
|
+
const violations = checkLayerViolations(graphResult.value, layers, rootDir);
|
|
974
|
+
return Ok({
|
|
975
|
+
valid: violations.length === 0,
|
|
976
|
+
violations,
|
|
977
|
+
graph: graphResult.value
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/constraints/circular-deps.ts
|
|
982
|
+
function tarjanSCC(graph) {
|
|
983
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
984
|
+
const stack = [];
|
|
985
|
+
const sccs = [];
|
|
986
|
+
let index = 0;
|
|
987
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
988
|
+
for (const node of graph.nodes) {
|
|
989
|
+
adjacency.set(node, []);
|
|
990
|
+
}
|
|
991
|
+
for (const edge of graph.edges) {
|
|
992
|
+
const neighbors = adjacency.get(edge.from);
|
|
993
|
+
if (neighbors && graph.nodes.includes(edge.to)) {
|
|
994
|
+
neighbors.push(edge.to);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function strongConnect(node) {
|
|
998
|
+
nodeMap.set(node, {
|
|
999
|
+
index,
|
|
1000
|
+
lowlink: index,
|
|
1001
|
+
onStack: true
|
|
1002
|
+
});
|
|
1003
|
+
index++;
|
|
1004
|
+
stack.push(node);
|
|
1005
|
+
const neighbors = adjacency.get(node) ?? [];
|
|
1006
|
+
for (const neighbor of neighbors) {
|
|
1007
|
+
const neighborData = nodeMap.get(neighbor);
|
|
1008
|
+
if (!neighborData) {
|
|
1009
|
+
strongConnect(neighbor);
|
|
1010
|
+
const nodeData2 = nodeMap.get(node);
|
|
1011
|
+
const updatedNeighborData = nodeMap.get(neighbor);
|
|
1012
|
+
nodeData2.lowlink = Math.min(nodeData2.lowlink, updatedNeighborData.lowlink);
|
|
1013
|
+
} else if (neighborData.onStack) {
|
|
1014
|
+
const nodeData2 = nodeMap.get(node);
|
|
1015
|
+
nodeData2.lowlink = Math.min(nodeData2.lowlink, neighborData.index);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const nodeData = nodeMap.get(node);
|
|
1019
|
+
if (nodeData.lowlink === nodeData.index) {
|
|
1020
|
+
const scc = [];
|
|
1021
|
+
let w;
|
|
1022
|
+
do {
|
|
1023
|
+
w = stack.pop();
|
|
1024
|
+
nodeMap.get(w).onStack = false;
|
|
1025
|
+
scc.push(w);
|
|
1026
|
+
} while (w !== node);
|
|
1027
|
+
if (scc.length > 1) {
|
|
1028
|
+
sccs.push(scc);
|
|
1029
|
+
} else if (scc.length === 1) {
|
|
1030
|
+
const selfNode = scc[0];
|
|
1031
|
+
const selfNeighbors = adjacency.get(selfNode) ?? [];
|
|
1032
|
+
if (selfNeighbors.includes(selfNode)) {
|
|
1033
|
+
sccs.push(scc);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
for (const node of graph.nodes) {
|
|
1039
|
+
if (!nodeMap.has(node)) {
|
|
1040
|
+
strongConnect(node);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return sccs;
|
|
1044
|
+
}
|
|
1045
|
+
function detectCircularDeps(graph) {
|
|
1046
|
+
const sccs = tarjanSCC(graph);
|
|
1047
|
+
const cycles = sccs.map((scc) => {
|
|
1048
|
+
const reversed = scc.reverse();
|
|
1049
|
+
const firstNode = reversed[reversed.length - 1];
|
|
1050
|
+
const cycle = [...reversed, firstNode];
|
|
1051
|
+
return {
|
|
1052
|
+
cycle,
|
|
1053
|
+
severity: "error",
|
|
1054
|
+
size: scc.length
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
1057
|
+
const largestCycle = cycles.reduce((max, c) => Math.max(max, c.size), 0);
|
|
1058
|
+
return Ok({
|
|
1059
|
+
hasCycles: cycles.length > 0,
|
|
1060
|
+
cycles,
|
|
1061
|
+
largestCycle
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
async function detectCircularDepsInFiles(files, parser) {
|
|
1065
|
+
const graphResult = await buildDependencyGraph(files, parser);
|
|
1066
|
+
if (!graphResult.ok) {
|
|
1067
|
+
return graphResult;
|
|
1068
|
+
}
|
|
1069
|
+
return detectCircularDeps(graphResult.value);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/constraints/boundary.ts
|
|
1073
|
+
function createBoundaryValidator(schema, name) {
|
|
1074
|
+
return {
|
|
1075
|
+
name,
|
|
1076
|
+
schema,
|
|
1077
|
+
parse(input) {
|
|
1078
|
+
const result = schema.safeParse(input);
|
|
1079
|
+
if (result.success) {
|
|
1080
|
+
return Ok(result.data);
|
|
1081
|
+
}
|
|
1082
|
+
const suggestions = result.error.issues.map((issue) => {
|
|
1083
|
+
const path2 = issue.path.join(".");
|
|
1084
|
+
return path2 ? `${path2}: ${issue.message}` : issue.message;
|
|
1085
|
+
});
|
|
1086
|
+
return Err(
|
|
1087
|
+
createError(
|
|
1088
|
+
"BOUNDARY_ERROR",
|
|
1089
|
+
`Boundary validation failed for ${name}`,
|
|
1090
|
+
{
|
|
1091
|
+
boundary: name,
|
|
1092
|
+
zodError: result.error,
|
|
1093
|
+
input
|
|
1094
|
+
},
|
|
1095
|
+
suggestions
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
},
|
|
1099
|
+
validate(input) {
|
|
1100
|
+
const result = schema.safeParse(input);
|
|
1101
|
+
return Ok(result.success);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function validateBoundaries(boundaries, data) {
|
|
1106
|
+
const violations = [];
|
|
1107
|
+
for (const boundary of boundaries) {
|
|
1108
|
+
const input = data.get(boundary.name);
|
|
1109
|
+
if (input === void 0) {
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
const result = boundary.schema.safeParse(input);
|
|
1113
|
+
if (!result.success) {
|
|
1114
|
+
violations.push({
|
|
1115
|
+
boundary: boundary.name,
|
|
1116
|
+
direction: boundary.direction,
|
|
1117
|
+
error: result.error,
|
|
1118
|
+
data: input
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return Ok({
|
|
1123
|
+
valid: violations.length === 0,
|
|
1124
|
+
violations
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/shared/parsers/typescript.ts
|
|
1129
|
+
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
1130
|
+
|
|
1131
|
+
// src/shared/parsers/base.ts
|
|
1132
|
+
function createParseError(code, message, details = {}, suggestions = []) {
|
|
1133
|
+
return { code, message, details, suggestions };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/shared/parsers/typescript.ts
|
|
1137
|
+
function walk(node, visitor) {
|
|
1138
|
+
if (!node || typeof node !== "object") return;
|
|
1139
|
+
if ("type" in node) {
|
|
1140
|
+
visitor(node);
|
|
1141
|
+
}
|
|
1142
|
+
for (const value of Object.values(node)) {
|
|
1143
|
+
if (Array.isArray(value)) {
|
|
1144
|
+
value.forEach((v) => walk(v, visitor));
|
|
1145
|
+
} else {
|
|
1146
|
+
walk(value, visitor);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
var TypeScriptParser = class {
|
|
1151
|
+
name = "typescript";
|
|
1152
|
+
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1153
|
+
async parseFile(path2) {
|
|
1154
|
+
const contentResult = await readFileContent(path2);
|
|
1155
|
+
if (!contentResult.ok) {
|
|
1156
|
+
return Err(
|
|
1157
|
+
createParseError("NOT_FOUND", `File not found: ${path2}`, { path: path2 }, [
|
|
1158
|
+
"Check that the file exists",
|
|
1159
|
+
"Verify the path is correct"
|
|
1160
|
+
])
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
try {
|
|
1164
|
+
const ast = (0, import_typescript_estree.parse)(contentResult.value, {
|
|
1165
|
+
loc: true,
|
|
1166
|
+
range: true,
|
|
1167
|
+
jsx: path2.endsWith(".tsx"),
|
|
1168
|
+
errorOnUnknownASTType: false
|
|
1169
|
+
});
|
|
1170
|
+
return Ok({
|
|
1171
|
+
type: "Program",
|
|
1172
|
+
body: ast,
|
|
1173
|
+
language: "typescript"
|
|
1174
|
+
});
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
const error = e;
|
|
1177
|
+
return Err(
|
|
1178
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path2}: ${error.message}`, { path: path2 }, [
|
|
1179
|
+
"Check for syntax errors in the file",
|
|
1180
|
+
"Ensure valid TypeScript syntax"
|
|
1181
|
+
])
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
extractImports(ast) {
|
|
1186
|
+
const imports = [];
|
|
1187
|
+
const program = ast.body;
|
|
1188
|
+
walk(program, (node) => {
|
|
1189
|
+
if (node.type === "ImportDeclaration") {
|
|
1190
|
+
const importDecl = node;
|
|
1191
|
+
const imp = {
|
|
1192
|
+
source: importDecl.source.value,
|
|
1193
|
+
specifiers: [],
|
|
1194
|
+
location: {
|
|
1195
|
+
file: "",
|
|
1196
|
+
line: importDecl.loc?.start.line ?? 0,
|
|
1197
|
+
column: importDecl.loc?.start.column ?? 0
|
|
1198
|
+
},
|
|
1199
|
+
kind: importDecl.importKind === "type" ? "type" : "value"
|
|
1200
|
+
};
|
|
1201
|
+
for (const spec of importDecl.specifiers) {
|
|
1202
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
1203
|
+
imp.default = spec.local.name;
|
|
1204
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
1205
|
+
imp.namespace = spec.local.name;
|
|
1206
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
1207
|
+
imp.specifiers.push(spec.local.name);
|
|
1208
|
+
if (spec.importKind === "type") {
|
|
1209
|
+
imp.kind = "type";
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
imports.push(imp);
|
|
1214
|
+
}
|
|
1215
|
+
if (node.type === "ImportExpression") {
|
|
1216
|
+
const importExpr = node;
|
|
1217
|
+
if (importExpr.source.type === "Literal" && typeof importExpr.source.value === "string") {
|
|
1218
|
+
imports.push({
|
|
1219
|
+
source: importExpr.source.value,
|
|
1220
|
+
specifiers: [],
|
|
1221
|
+
location: {
|
|
1222
|
+
file: "",
|
|
1223
|
+
line: importExpr.loc?.start.line ?? 0,
|
|
1224
|
+
column: importExpr.loc?.start.column ?? 0
|
|
1225
|
+
},
|
|
1226
|
+
kind: "value"
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
return Ok(imports);
|
|
1232
|
+
}
|
|
1233
|
+
extractExports(ast) {
|
|
1234
|
+
const exports2 = [];
|
|
1235
|
+
const program = ast.body;
|
|
1236
|
+
walk(program, (node) => {
|
|
1237
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
1238
|
+
const exportDecl = node;
|
|
1239
|
+
if (exportDecl.source) {
|
|
1240
|
+
for (const spec of exportDecl.specifiers) {
|
|
1241
|
+
if (spec.type === "ExportSpecifier") {
|
|
1242
|
+
const exported = spec.exported;
|
|
1243
|
+
const name = exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
1244
|
+
exports2.push({
|
|
1245
|
+
name,
|
|
1246
|
+
type: "named",
|
|
1247
|
+
location: {
|
|
1248
|
+
file: "",
|
|
1249
|
+
line: exportDecl.loc?.start.line ?? 0,
|
|
1250
|
+
column: exportDecl.loc?.start.column ?? 0
|
|
1251
|
+
},
|
|
1252
|
+
isReExport: true,
|
|
1253
|
+
source: exportDecl.source.value
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (exportDecl.declaration) {
|
|
1260
|
+
const decl = exportDecl.declaration;
|
|
1261
|
+
if (decl.type === "VariableDeclaration") {
|
|
1262
|
+
for (const declarator of decl.declarations) {
|
|
1263
|
+
if (declarator.id.type === "Identifier") {
|
|
1264
|
+
exports2.push({
|
|
1265
|
+
name: declarator.id.name,
|
|
1266
|
+
type: "named",
|
|
1267
|
+
location: {
|
|
1268
|
+
file: "",
|
|
1269
|
+
line: decl.loc?.start.line ?? 0,
|
|
1270
|
+
column: decl.loc?.start.column ?? 0
|
|
1271
|
+
},
|
|
1272
|
+
isReExport: false
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
} else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
|
|
1277
|
+
if (decl.id) {
|
|
1278
|
+
exports2.push({
|
|
1279
|
+
name: decl.id.name,
|
|
1280
|
+
type: "named",
|
|
1281
|
+
location: {
|
|
1282
|
+
file: "",
|
|
1283
|
+
line: decl.loc?.start.line ?? 0,
|
|
1284
|
+
column: decl.loc?.start.column ?? 0
|
|
1285
|
+
},
|
|
1286
|
+
isReExport: false
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
for (const spec of exportDecl.specifiers) {
|
|
1292
|
+
if (spec.type === "ExportSpecifier") {
|
|
1293
|
+
const exported = spec.exported;
|
|
1294
|
+
const name = exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
1295
|
+
exports2.push({
|
|
1296
|
+
name,
|
|
1297
|
+
type: "named",
|
|
1298
|
+
location: {
|
|
1299
|
+
file: "",
|
|
1300
|
+
line: exportDecl.loc?.start.line ?? 0,
|
|
1301
|
+
column: exportDecl.loc?.start.column ?? 0
|
|
1302
|
+
},
|
|
1303
|
+
isReExport: false
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
1309
|
+
const exportDecl = node;
|
|
1310
|
+
exports2.push({
|
|
1311
|
+
name: "default",
|
|
1312
|
+
type: "default",
|
|
1313
|
+
location: {
|
|
1314
|
+
file: "",
|
|
1315
|
+
line: exportDecl.loc?.start.line ?? 0,
|
|
1316
|
+
column: exportDecl.loc?.start.column ?? 0
|
|
1317
|
+
},
|
|
1318
|
+
isReExport: false
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (node.type === "ExportAllDeclaration") {
|
|
1322
|
+
const exportDecl = node;
|
|
1323
|
+
exports2.push({
|
|
1324
|
+
name: exportDecl.exported?.name ?? "*",
|
|
1325
|
+
type: "namespace",
|
|
1326
|
+
location: {
|
|
1327
|
+
file: "",
|
|
1328
|
+
line: exportDecl.loc?.start.line ?? 0,
|
|
1329
|
+
column: exportDecl.loc?.start.column ?? 0
|
|
1330
|
+
},
|
|
1331
|
+
isReExport: true,
|
|
1332
|
+
source: exportDecl.source.value
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
return Ok(exports2);
|
|
1337
|
+
}
|
|
1338
|
+
async health() {
|
|
1339
|
+
return Ok({ available: true, version: "7.0.0" });
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/entropy/snapshot.ts
|
|
1344
|
+
var import_path6 = require("path");
|
|
1345
|
+
var import_minimatch2 = require("minimatch");
|
|
1346
|
+
async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
1347
|
+
if (explicitEntries && explicitEntries.length > 0) {
|
|
1348
|
+
const resolved = explicitEntries.map((e) => (0, import_path6.resolve)(rootDir, e));
|
|
1349
|
+
return Ok(resolved);
|
|
1350
|
+
}
|
|
1351
|
+
const pkgPath = (0, import_path6.join)(rootDir, "package.json");
|
|
1352
|
+
if (await fileExists(pkgPath)) {
|
|
1353
|
+
const pkgContent = await readFileContent(pkgPath);
|
|
1354
|
+
if (pkgContent.ok) {
|
|
1355
|
+
try {
|
|
1356
|
+
const pkg = JSON.parse(pkgContent.value);
|
|
1357
|
+
const entries = [];
|
|
1358
|
+
if (pkg["exports"]) {
|
|
1359
|
+
const exports2 = pkg["exports"];
|
|
1360
|
+
if (typeof exports2 === "string") {
|
|
1361
|
+
entries.push((0, import_path6.resolve)(rootDir, exports2));
|
|
1362
|
+
} else if (typeof exports2 === "object" && exports2 !== null) {
|
|
1363
|
+
for (const value of Object.values(exports2)) {
|
|
1364
|
+
if (typeof value === "string") {
|
|
1365
|
+
entries.push((0, import_path6.resolve)(rootDir, value));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const main = pkg["main"];
|
|
1371
|
+
if (typeof main === "string" && entries.length === 0) {
|
|
1372
|
+
entries.push((0, import_path6.resolve)(rootDir, main));
|
|
1373
|
+
}
|
|
1374
|
+
const bin = pkg["bin"];
|
|
1375
|
+
if (bin) {
|
|
1376
|
+
if (typeof bin === "string") {
|
|
1377
|
+
entries.push((0, import_path6.resolve)(rootDir, bin));
|
|
1378
|
+
} else if (typeof bin === "object") {
|
|
1379
|
+
for (const value of Object.values(bin)) {
|
|
1380
|
+
if (typeof value === "string") {
|
|
1381
|
+
entries.push((0, import_path6.resolve)(rootDir, value));
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (entries.length > 0) {
|
|
1387
|
+
return Ok(entries);
|
|
1388
|
+
}
|
|
1389
|
+
} catch {
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const conventions = ["src/index.ts", "src/main.ts", "index.ts", "main.ts"];
|
|
1394
|
+
for (const conv of conventions) {
|
|
1395
|
+
const convPath = (0, import_path6.join)(rootDir, conv);
|
|
1396
|
+
if (await fileExists(convPath)) {
|
|
1397
|
+
return Ok([convPath]);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return Err(
|
|
1401
|
+
createEntropyError(
|
|
1402
|
+
"ENTRY_POINT_NOT_FOUND",
|
|
1403
|
+
"Could not resolve entry points",
|
|
1404
|
+
{ reason: "No package.json exports/main and no conventional entry files found" },
|
|
1405
|
+
[
|
|
1406
|
+
'Add "exports" or "main" to package.json',
|
|
1407
|
+
"Create src/index.ts",
|
|
1408
|
+
"Specify entryPoints in config"
|
|
1409
|
+
]
|
|
1410
|
+
)
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
function extractCodeBlocks(content) {
|
|
1414
|
+
const blocks = [];
|
|
1415
|
+
const lines = content.split("\n");
|
|
1416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1417
|
+
const line = lines[i];
|
|
1418
|
+
if (line !== void 0 && line.startsWith("```")) {
|
|
1419
|
+
const langMatch = line.match(/```(\w*)/);
|
|
1420
|
+
const language = langMatch?.[1] || "text";
|
|
1421
|
+
let codeContent = "";
|
|
1422
|
+
let j = i + 1;
|
|
1423
|
+
let currentLine = lines[j];
|
|
1424
|
+
while (j < lines.length && currentLine !== void 0 && !currentLine.startsWith("```")) {
|
|
1425
|
+
codeContent += currentLine + "\n";
|
|
1426
|
+
j++;
|
|
1427
|
+
currentLine = lines[j];
|
|
1428
|
+
}
|
|
1429
|
+
blocks.push({
|
|
1430
|
+
language,
|
|
1431
|
+
content: codeContent.trim(),
|
|
1432
|
+
line: i + 1
|
|
1433
|
+
});
|
|
1434
|
+
i = j;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return blocks;
|
|
1438
|
+
}
|
|
1439
|
+
function extractInlineRefs(content) {
|
|
1440
|
+
const refs = [];
|
|
1441
|
+
const lines = content.split("\n");
|
|
1442
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1443
|
+
const line = lines[i];
|
|
1444
|
+
if (line === void 0) continue;
|
|
1445
|
+
const regex = /`([^`]+)`/g;
|
|
1446
|
+
let match;
|
|
1447
|
+
while ((match = regex.exec(line)) !== null) {
|
|
1448
|
+
const reference = match[1];
|
|
1449
|
+
if (reference === void 0) continue;
|
|
1450
|
+
if (reference.match(/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*(\(.*\))?$/)) {
|
|
1451
|
+
refs.push({
|
|
1452
|
+
reference: reference.replace(/\(.*\)$/, ""),
|
|
1453
|
+
// Remove function parens
|
|
1454
|
+
line: i + 1,
|
|
1455
|
+
column: match.index
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return refs;
|
|
1461
|
+
}
|
|
1462
|
+
async function parseDocumentationFile(path2) {
|
|
1463
|
+
const contentResult = await readFileContent(path2);
|
|
1464
|
+
if (!contentResult.ok) {
|
|
1465
|
+
return Err(
|
|
1466
|
+
createEntropyError(
|
|
1467
|
+
"PARSE_ERROR",
|
|
1468
|
+
`Failed to read documentation file: ${path2}`,
|
|
1469
|
+
{ file: path2 },
|
|
1470
|
+
["Check that the file exists"]
|
|
1471
|
+
)
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
const content = contentResult.value;
|
|
1475
|
+
const type = path2.endsWith(".md") ? "markdown" : "text";
|
|
1476
|
+
return Ok({
|
|
1477
|
+
path: path2,
|
|
1478
|
+
type,
|
|
1479
|
+
content,
|
|
1480
|
+
codeBlocks: extractCodeBlocks(content),
|
|
1481
|
+
inlineRefs: extractInlineRefs(content)
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
function extractInternalSymbols(ast) {
|
|
1485
|
+
const symbols = [];
|
|
1486
|
+
const body = ast.body;
|
|
1487
|
+
if (!body?.body) return symbols;
|
|
1488
|
+
for (const node of body.body) {
|
|
1489
|
+
if (node.type === "FunctionDeclaration" && node.id?.name) {
|
|
1490
|
+
symbols.push({
|
|
1491
|
+
name: node.id.name,
|
|
1492
|
+
type: "function",
|
|
1493
|
+
line: node.loc?.start?.line || 0,
|
|
1494
|
+
references: 0,
|
|
1495
|
+
calledBy: []
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
if (node.type === "VariableDeclaration") {
|
|
1499
|
+
for (const decl of node.declarations || []) {
|
|
1500
|
+
if (decl.id?.name) {
|
|
1501
|
+
symbols.push({
|
|
1502
|
+
name: decl.id.name,
|
|
1503
|
+
type: "variable",
|
|
1504
|
+
line: node.loc?.start?.line || 0,
|
|
1505
|
+
references: 0,
|
|
1506
|
+
calledBy: []
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (node.type === "ClassDeclaration" && node.id?.name) {
|
|
1512
|
+
symbols.push({
|
|
1513
|
+
name: node.id.name,
|
|
1514
|
+
type: "class",
|
|
1515
|
+
line: node.loc?.start?.line || 0,
|
|
1516
|
+
references: 0,
|
|
1517
|
+
calledBy: []
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return symbols;
|
|
1522
|
+
}
|
|
1523
|
+
function extractJSDocComments(ast) {
|
|
1524
|
+
const comments = [];
|
|
1525
|
+
const body = ast.body;
|
|
1526
|
+
if (body?.comments) {
|
|
1527
|
+
for (const comment of body.comments) {
|
|
1528
|
+
if (comment.type === "Block" && comment.value?.startsWith("*")) {
|
|
1529
|
+
const jsDocComment = {
|
|
1530
|
+
content: comment.value,
|
|
1531
|
+
line: comment.loc?.start?.line || 0
|
|
1532
|
+
};
|
|
1533
|
+
comments.push(jsDocComment);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return comments;
|
|
1538
|
+
}
|
|
1539
|
+
function buildExportMap(files) {
|
|
1540
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1541
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1542
|
+
for (const file of files) {
|
|
1543
|
+
byFile.set(file.path, file.exports);
|
|
1544
|
+
for (const exp of file.exports) {
|
|
1545
|
+
const existing = byName.get(exp.name) || [];
|
|
1546
|
+
existing.push({ file: file.path, export: exp });
|
|
1547
|
+
byName.set(exp.name, existing);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return { byFile, byName };
|
|
1551
|
+
}
|
|
1552
|
+
function extractAllCodeReferences(docs) {
|
|
1553
|
+
const refs = [];
|
|
1554
|
+
for (const doc of docs) {
|
|
1555
|
+
for (const inlineRef of doc.inlineRefs) {
|
|
1556
|
+
refs.push({
|
|
1557
|
+
docFile: doc.path,
|
|
1558
|
+
line: inlineRef.line,
|
|
1559
|
+
column: inlineRef.column,
|
|
1560
|
+
reference: inlineRef.reference,
|
|
1561
|
+
context: "inline"
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
for (const block of doc.codeBlocks) {
|
|
1565
|
+
if (block.language === "typescript" || block.language === "ts" || block.language === "javascript" || block.language === "js") {
|
|
1566
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
1567
|
+
let match;
|
|
1568
|
+
while ((match = importRegex.exec(block.content)) !== null) {
|
|
1569
|
+
const matchedGroup = match[1];
|
|
1570
|
+
if (matchedGroup === void 0) continue;
|
|
1571
|
+
const names = matchedGroup.split(",").map((n) => n.trim());
|
|
1572
|
+
for (const name of names) {
|
|
1573
|
+
refs.push({
|
|
1574
|
+
docFile: doc.path,
|
|
1575
|
+
line: block.line,
|
|
1576
|
+
column: 0,
|
|
1577
|
+
reference: name,
|
|
1578
|
+
context: "code-block"
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return refs;
|
|
1586
|
+
}
|
|
1587
|
+
async function buildSnapshot(config) {
|
|
1588
|
+
const startTime = Date.now();
|
|
1589
|
+
const parser = config.parser || new TypeScriptParser();
|
|
1590
|
+
const rootDir = (0, import_path6.resolve)(config.rootDir);
|
|
1591
|
+
const entryPointsResult = await resolveEntryPoints(rootDir, config.entryPoints);
|
|
1592
|
+
if (!entryPointsResult.ok) {
|
|
1593
|
+
return Err(entryPointsResult.error);
|
|
1594
|
+
}
|
|
1595
|
+
const includePatterns = config.include || ["**/*.ts", "**/*.tsx"];
|
|
1596
|
+
const excludePatterns = config.exclude || [
|
|
1597
|
+
"node_modules/**",
|
|
1598
|
+
"dist/**",
|
|
1599
|
+
"**/*.test.ts",
|
|
1600
|
+
"**/*.spec.ts"
|
|
1601
|
+
];
|
|
1602
|
+
let sourceFilePaths = [];
|
|
1603
|
+
for (const pattern of includePatterns) {
|
|
1604
|
+
const files2 = await findFiles(pattern, rootDir);
|
|
1605
|
+
sourceFilePaths.push(...files2);
|
|
1606
|
+
}
|
|
1607
|
+
sourceFilePaths = sourceFilePaths.filter((f) => {
|
|
1608
|
+
const rel = (0, import_path6.relative)(rootDir, f);
|
|
1609
|
+
return !excludePatterns.some((p) => (0, import_minimatch2.minimatch)(rel, p));
|
|
1610
|
+
});
|
|
1611
|
+
const files = [];
|
|
1612
|
+
for (const filePath of sourceFilePaths) {
|
|
1613
|
+
const parseResult = await parser.parseFile(filePath);
|
|
1614
|
+
if (!parseResult.ok) continue;
|
|
1615
|
+
const importsResult = parser.extractImports(parseResult.value);
|
|
1616
|
+
const exportsResult = parser.extractExports(parseResult.value);
|
|
1617
|
+
const internalSymbols = extractInternalSymbols(parseResult.value);
|
|
1618
|
+
const jsDocComments = extractJSDocComments(parseResult.value);
|
|
1619
|
+
files.push({
|
|
1620
|
+
path: filePath,
|
|
1621
|
+
ast: parseResult.value,
|
|
1622
|
+
imports: importsResult.ok ? importsResult.value : [],
|
|
1623
|
+
exports: exportsResult.ok ? exportsResult.value : [],
|
|
1624
|
+
internalSymbols,
|
|
1625
|
+
jsDocComments
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
const graphResult = await buildDependencyGraph(sourceFilePaths, parser);
|
|
1629
|
+
const dependencyGraph = graphResult.ok ? graphResult.value : { nodes: [], edges: [] };
|
|
1630
|
+
const docPatterns = config.docPaths || ["docs/**/*.md", "README.md", "**/README.md"];
|
|
1631
|
+
let docFilePaths = [];
|
|
1632
|
+
for (const pattern of docPatterns) {
|
|
1633
|
+
const docFiles = await findFiles(pattern, rootDir);
|
|
1634
|
+
docFilePaths.push(...docFiles);
|
|
1635
|
+
}
|
|
1636
|
+
docFilePaths = [...new Set(docFilePaths)];
|
|
1637
|
+
const docs = [];
|
|
1638
|
+
for (const docPath of docFilePaths) {
|
|
1639
|
+
const docResult = await parseDocumentationFile(docPath);
|
|
1640
|
+
if (docResult.ok) {
|
|
1641
|
+
docs.push(docResult.value);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const exportMap = buildExportMap(files);
|
|
1645
|
+
const codeReferences = extractAllCodeReferences(docs);
|
|
1646
|
+
const buildTime = Date.now() - startTime;
|
|
1647
|
+
return Ok({
|
|
1648
|
+
files,
|
|
1649
|
+
dependencyGraph,
|
|
1650
|
+
exportMap,
|
|
1651
|
+
docs,
|
|
1652
|
+
codeReferences,
|
|
1653
|
+
entryPoints: entryPointsResult.value,
|
|
1654
|
+
rootDir,
|
|
1655
|
+
config,
|
|
1656
|
+
buildTime
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/entropy/detectors/drift.ts
|
|
1661
|
+
var import_path7 = require("path");
|
|
1662
|
+
function levenshteinDistance(a, b) {
|
|
1663
|
+
const matrix = [];
|
|
1664
|
+
for (let i = 0; i <= b.length; i++) {
|
|
1665
|
+
matrix[i] = [i];
|
|
1666
|
+
}
|
|
1667
|
+
for (let j = 0; j <= a.length; j++) {
|
|
1668
|
+
const row = matrix[0];
|
|
1669
|
+
if (row) {
|
|
1670
|
+
row[j] = j;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
for (let i = 1; i <= b.length; i++) {
|
|
1674
|
+
for (let j = 1; j <= a.length; j++) {
|
|
1675
|
+
const row = matrix[i];
|
|
1676
|
+
const prevRow = matrix[i - 1];
|
|
1677
|
+
if (!row || !prevRow) continue;
|
|
1678
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
1679
|
+
row[j] = prevRow[j - 1] ?? 0;
|
|
1680
|
+
} else {
|
|
1681
|
+
row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const lastRow = matrix[b.length];
|
|
1686
|
+
return lastRow?.[a.length] ?? 0;
|
|
1687
|
+
}
|
|
1688
|
+
function findPossibleMatches(reference, exportNames, maxDistance = 5) {
|
|
1689
|
+
const matches = [];
|
|
1690
|
+
const refLower = reference.toLowerCase();
|
|
1691
|
+
for (const name of exportNames) {
|
|
1692
|
+
const nameLower = name.toLowerCase();
|
|
1693
|
+
if (nameLower === refLower) {
|
|
1694
|
+
matches.push({ name, score: 0 });
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
if (nameLower.includes(refLower) || refLower.includes(nameLower)) {
|
|
1698
|
+
matches.push({ name, score: 1 });
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const distance = levenshteinDistance(refLower, nameLower);
|
|
1702
|
+
if (distance <= maxDistance) {
|
|
1703
|
+
matches.push({ name, score: distance });
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return matches.sort((a, b) => a.score - b.score).slice(0, 3).map((m) => m.name);
|
|
1707
|
+
}
|
|
1708
|
+
var DEFAULT_DRIFT_CONFIG = {
|
|
1709
|
+
docPaths: [],
|
|
1710
|
+
checkApiSignatures: true,
|
|
1711
|
+
checkExamples: true,
|
|
1712
|
+
checkStructure: true,
|
|
1713
|
+
ignorePatterns: []
|
|
1714
|
+
};
|
|
1715
|
+
function checkApiSignatureDrift(snapshot, config) {
|
|
1716
|
+
const drifts = [];
|
|
1717
|
+
const exportNames = Array.from(snapshot.exportMap.byName.keys());
|
|
1718
|
+
for (const ref of snapshot.codeReferences) {
|
|
1719
|
+
if (config.ignorePatterns.some((p) => ref.reference.match(new RegExp(p)))) {
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
if (!snapshot.exportMap.byName.has(ref.reference)) {
|
|
1723
|
+
const possibleMatches = findPossibleMatches(ref.reference, exportNames);
|
|
1724
|
+
const confidence = possibleMatches.length > 0 ? "high" : "medium";
|
|
1725
|
+
const drift = {
|
|
1726
|
+
type: "api-signature",
|
|
1727
|
+
docFile: ref.docFile,
|
|
1728
|
+
line: ref.line,
|
|
1729
|
+
reference: ref.reference,
|
|
1730
|
+
context: ref.context,
|
|
1731
|
+
issue: possibleMatches.length > 0 ? "RENAMED" : "NOT_FOUND",
|
|
1732
|
+
details: possibleMatches.length > 0 ? `Symbol "${ref.reference}" not found. Similar: ${possibleMatches.join(", ")}` : `Symbol "${ref.reference}" not found in codebase`,
|
|
1733
|
+
suggestion: possibleMatches.length > 0 ? `Did you mean "${possibleMatches[0]}"?` : "Remove reference or add the missing export",
|
|
1734
|
+
confidence
|
|
1735
|
+
};
|
|
1736
|
+
if (possibleMatches.length > 0) {
|
|
1737
|
+
drift.possibleMatches = possibleMatches;
|
|
1738
|
+
}
|
|
1739
|
+
drifts.push(drift);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return drifts;
|
|
1743
|
+
}
|
|
1744
|
+
function extractFileLinks(content) {
|
|
1745
|
+
const links = [];
|
|
1746
|
+
const lines = content.split("\n");
|
|
1747
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1748
|
+
const line = lines[i];
|
|
1749
|
+
if (!line) continue;
|
|
1750
|
+
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
1751
|
+
let match;
|
|
1752
|
+
while ((match = linkRegex.exec(line)) !== null) {
|
|
1753
|
+
const linkPath = match[2];
|
|
1754
|
+
if (linkPath && !linkPath.startsWith("http") && !linkPath.startsWith("#") && (linkPath.includes(".") || linkPath.startsWith(".."))) {
|
|
1755
|
+
links.push({ link: linkPath, line: i + 1 });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return links;
|
|
1760
|
+
}
|
|
1761
|
+
async function checkStructureDrift(snapshot, _config) {
|
|
1762
|
+
const drifts = [];
|
|
1763
|
+
for (const doc of snapshot.docs) {
|
|
1764
|
+
const fileLinks = extractFileLinks(doc.content);
|
|
1765
|
+
for (const { link, line } of fileLinks) {
|
|
1766
|
+
const resolvedPath = (0, import_path7.resolve)((0, import_path7.dirname)(doc.path), link);
|
|
1767
|
+
const exists = await fileExists(resolvedPath);
|
|
1768
|
+
if (!exists) {
|
|
1769
|
+
drifts.push({
|
|
1770
|
+
type: "structure",
|
|
1771
|
+
docFile: doc.path,
|
|
1772
|
+
line,
|
|
1773
|
+
reference: link,
|
|
1774
|
+
context: "link",
|
|
1775
|
+
issue: "NOT_FOUND",
|
|
1776
|
+
details: `File "${link}" referenced in documentation does not exist`,
|
|
1777
|
+
suggestion: "Update the link or remove the reference",
|
|
1778
|
+
confidence: "high"
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return drifts;
|
|
1784
|
+
}
|
|
1785
|
+
async function detectDocDrift(snapshot, config) {
|
|
1786
|
+
const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
|
|
1787
|
+
const drifts = [];
|
|
1788
|
+
if (fullConfig.checkApiSignatures) {
|
|
1789
|
+
drifts.push(...checkApiSignatureDrift(snapshot, fullConfig));
|
|
1790
|
+
}
|
|
1791
|
+
if (fullConfig.checkStructure) {
|
|
1792
|
+
drifts.push(...await checkStructureDrift(snapshot, fullConfig));
|
|
1793
|
+
}
|
|
1794
|
+
const apiDrifts = drifts.filter((d) => d.type === "api-signature").length;
|
|
1795
|
+
const exampleDrifts = drifts.filter((d) => d.type === "example-code").length;
|
|
1796
|
+
const structureDrifts = drifts.filter((d) => d.type === "structure").length;
|
|
1797
|
+
const severity = drifts.length === 0 ? "none" : drifts.length <= 3 ? "low" : drifts.length <= 10 ? "medium" : "high";
|
|
1798
|
+
return Ok({
|
|
1799
|
+
drifts,
|
|
1800
|
+
stats: {
|
|
1801
|
+
docsScanned: snapshot.docs.length,
|
|
1802
|
+
referencesChecked: snapshot.codeReferences.length,
|
|
1803
|
+
driftsFound: drifts.length,
|
|
1804
|
+
byType: { api: apiDrifts, example: exampleDrifts, structure: structureDrifts }
|
|
1805
|
+
},
|
|
1806
|
+
severity
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/entropy/detectors/dead-code.ts
|
|
1811
|
+
var import_path8 = require("path");
|
|
1812
|
+
function resolveImportToFile(importSource, fromFile, snapshot) {
|
|
1813
|
+
if (!importSource.startsWith(".")) {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
const fromDir = (0, import_path8.dirname)(fromFile);
|
|
1817
|
+
let resolved = (0, import_path8.resolve)(fromDir, importSource);
|
|
1818
|
+
if (!resolved.endsWith(".ts") && !resolved.endsWith(".tsx")) {
|
|
1819
|
+
const withTs = resolved + ".ts";
|
|
1820
|
+
if (snapshot.files.some((f) => f.path === withTs)) {
|
|
1821
|
+
return withTs;
|
|
1822
|
+
}
|
|
1823
|
+
const withIndex = (0, import_path8.resolve)(resolved, "index.ts");
|
|
1824
|
+
if (snapshot.files.some((f) => f.path === withIndex)) {
|
|
1825
|
+
return withIndex;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (snapshot.files.some((f) => f.path === resolved)) {
|
|
1829
|
+
return resolved;
|
|
1830
|
+
}
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
function buildReachabilityMap(snapshot) {
|
|
1834
|
+
const reachability = /* @__PURE__ */ new Map();
|
|
1835
|
+
for (const file of snapshot.files) {
|
|
1836
|
+
reachability.set(file.path, false);
|
|
1837
|
+
}
|
|
1838
|
+
const queue = [...snapshot.entryPoints];
|
|
1839
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1840
|
+
while (queue.length > 0) {
|
|
1841
|
+
const current = queue.shift();
|
|
1842
|
+
if (visited.has(current)) continue;
|
|
1843
|
+
visited.add(current);
|
|
1844
|
+
reachability.set(current, true);
|
|
1845
|
+
const sourceFile = snapshot.files.find((f) => f.path === current);
|
|
1846
|
+
if (!sourceFile) continue;
|
|
1847
|
+
for (const imp of sourceFile.imports) {
|
|
1848
|
+
const resolved = resolveImportToFile(imp.source, current, snapshot);
|
|
1849
|
+
if (resolved && !visited.has(resolved)) {
|
|
1850
|
+
queue.push(resolved);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
for (const exp of sourceFile.exports) {
|
|
1854
|
+
if (exp.isReExport && exp.source) {
|
|
1855
|
+
const resolved = resolveImportToFile(exp.source, current, snapshot);
|
|
1856
|
+
if (resolved && !visited.has(resolved)) {
|
|
1857
|
+
queue.push(resolved);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return reachability;
|
|
1863
|
+
}
|
|
1864
|
+
function buildExportUsageMap(snapshot) {
|
|
1865
|
+
const usageMap = /* @__PURE__ */ new Map();
|
|
1866
|
+
for (const file of snapshot.files) {
|
|
1867
|
+
for (const exp of file.exports) {
|
|
1868
|
+
const key = `${file.path}:${exp.name}`;
|
|
1869
|
+
usageMap.set(key, { importers: [], isReExported: exp.isReExport });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
for (const file of snapshot.files) {
|
|
1873
|
+
for (const imp of file.imports) {
|
|
1874
|
+
const resolvedFile = resolveImportToFile(imp.source, file.path, snapshot);
|
|
1875
|
+
if (!resolvedFile) continue;
|
|
1876
|
+
const sourceFile = snapshot.files.find((f) => f.path === resolvedFile);
|
|
1877
|
+
if (!sourceFile) continue;
|
|
1878
|
+
for (const specifier of imp.specifiers) {
|
|
1879
|
+
const matchingExport = sourceFile.exports.find(
|
|
1880
|
+
(e) => e.name === specifier || specifier === "default" && e.type === "default"
|
|
1881
|
+
);
|
|
1882
|
+
if (matchingExport) {
|
|
1883
|
+
const key = `${resolvedFile}:${matchingExport.name}`;
|
|
1884
|
+
const usage = usageMap.get(key);
|
|
1885
|
+
if (usage) {
|
|
1886
|
+
usage.importers.push(file.path);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return usageMap;
|
|
1893
|
+
}
|
|
1894
|
+
function findDeadExports(snapshot, usageMap, reachability) {
|
|
1895
|
+
const deadExports = [];
|
|
1896
|
+
for (const file of snapshot.files) {
|
|
1897
|
+
if (snapshot.entryPoints.includes(file.path)) continue;
|
|
1898
|
+
for (const exp of file.exports) {
|
|
1899
|
+
if (exp.isReExport) continue;
|
|
1900
|
+
const key = `${file.path}:${exp.name}`;
|
|
1901
|
+
const usage = usageMap.get(key);
|
|
1902
|
+
if (!usage || usage.importers.length === 0) {
|
|
1903
|
+
deadExports.push({
|
|
1904
|
+
file: file.path,
|
|
1905
|
+
name: exp.name,
|
|
1906
|
+
line: exp.location.line,
|
|
1907
|
+
type: "variable",
|
|
1908
|
+
// Default type since Export doesn't track declaration kind
|
|
1909
|
+
isDefault: exp.type === "default",
|
|
1910
|
+
reason: "NO_IMPORTERS"
|
|
1911
|
+
});
|
|
1912
|
+
} else {
|
|
1913
|
+
const allImportersDead = usage.importers.every((importer) => !reachability.get(importer));
|
|
1914
|
+
if (allImportersDead) {
|
|
1915
|
+
deadExports.push({
|
|
1916
|
+
file: file.path,
|
|
1917
|
+
name: exp.name,
|
|
1918
|
+
line: exp.location.line,
|
|
1919
|
+
type: "variable",
|
|
1920
|
+
// Default type since Export doesn't track declaration kind
|
|
1921
|
+
isDefault: exp.type === "default",
|
|
1922
|
+
reason: "IMPORTERS_ALSO_DEAD"
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return deadExports;
|
|
1929
|
+
}
|
|
1930
|
+
function countLinesFromAST(ast) {
|
|
1931
|
+
if (ast.body && Array.isArray(ast.body)) {
|
|
1932
|
+
let maxLine = 0;
|
|
1933
|
+
const traverse = (node) => {
|
|
1934
|
+
if (node && typeof node === "object") {
|
|
1935
|
+
const n = node;
|
|
1936
|
+
if (n.loc?.end?.line && n.loc.end.line > maxLine) {
|
|
1937
|
+
maxLine = n.loc.end.line;
|
|
1938
|
+
}
|
|
1939
|
+
for (const key of Object.keys(node)) {
|
|
1940
|
+
const value = node[key];
|
|
1941
|
+
if (Array.isArray(value)) {
|
|
1942
|
+
for (const item of value) {
|
|
1943
|
+
traverse(item);
|
|
1944
|
+
}
|
|
1945
|
+
} else if (value && typeof value === "object") {
|
|
1946
|
+
traverse(value);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
traverse(ast);
|
|
1952
|
+
if (maxLine > 0) return maxLine;
|
|
1953
|
+
return Math.max(ast.body.length * 3, 1);
|
|
1954
|
+
}
|
|
1955
|
+
return 1;
|
|
1956
|
+
}
|
|
1957
|
+
function findDeadFiles(snapshot, reachability) {
|
|
1958
|
+
const deadFiles = [];
|
|
1959
|
+
for (const file of snapshot.files) {
|
|
1960
|
+
const isReachable = reachability.get(file.path) ?? false;
|
|
1961
|
+
if (!isReachable) {
|
|
1962
|
+
deadFiles.push({
|
|
1963
|
+
path: file.path,
|
|
1964
|
+
reason: "NO_IMPORTERS",
|
|
1965
|
+
exportCount: file.exports.filter((e) => !e.isReExport).length,
|
|
1966
|
+
lineCount: countLinesFromAST(file.ast)
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return deadFiles;
|
|
1971
|
+
}
|
|
1972
|
+
function isIdentifierUsedInAST(ast, identifier, skipImportDeclaration = true) {
|
|
1973
|
+
const astString = JSON.stringify(ast);
|
|
1974
|
+
const identifierPattern = new RegExp(`"name"\\s*:\\s*"${identifier}"`, "g");
|
|
1975
|
+
const matches = astString.match(identifierPattern);
|
|
1976
|
+
if (!matches) return false;
|
|
1977
|
+
if (skipImportDeclaration) {
|
|
1978
|
+
return matches.length > 2;
|
|
1979
|
+
}
|
|
1980
|
+
return matches.length > 0;
|
|
1981
|
+
}
|
|
1982
|
+
function findUnusedImports(snapshot) {
|
|
1983
|
+
const unusedImports = [];
|
|
1984
|
+
for (const file of snapshot.files) {
|
|
1985
|
+
for (const imp of file.imports) {
|
|
1986
|
+
const unusedSpecifiers = [];
|
|
1987
|
+
for (const specifier of imp.specifiers) {
|
|
1988
|
+
if (!isIdentifierUsedInAST(file.ast, specifier, true)) {
|
|
1989
|
+
unusedSpecifiers.push(specifier);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (unusedSpecifiers.length > 0) {
|
|
1993
|
+
unusedImports.push({
|
|
1994
|
+
file: file.path,
|
|
1995
|
+
line: imp.location.line,
|
|
1996
|
+
source: imp.source,
|
|
1997
|
+
specifiers: unusedSpecifiers,
|
|
1998
|
+
isFullyUnused: unusedSpecifiers.length === imp.specifiers.length
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return unusedImports;
|
|
2004
|
+
}
|
|
2005
|
+
function findDeadInternals(snapshot, _reachability) {
|
|
2006
|
+
const deadInternals = [];
|
|
2007
|
+
for (const file of snapshot.files) {
|
|
2008
|
+
for (const symbol of file.internalSymbols) {
|
|
2009
|
+
if (symbol.type === "type") continue;
|
|
2010
|
+
if (symbol.references === 0 && symbol.calledBy.length === 0) {
|
|
2011
|
+
deadInternals.push({
|
|
2012
|
+
file: file.path,
|
|
2013
|
+
name: symbol.name,
|
|
2014
|
+
line: symbol.line,
|
|
2015
|
+
type: symbol.type,
|
|
2016
|
+
reason: "NEVER_CALLED"
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return deadInternals;
|
|
2022
|
+
}
|
|
2023
|
+
async function detectDeadCode(snapshot) {
|
|
2024
|
+
const reachability = buildReachabilityMap(snapshot);
|
|
2025
|
+
const usageMap = buildExportUsageMap(snapshot);
|
|
2026
|
+
const deadExports = findDeadExports(snapshot, usageMap, reachability);
|
|
2027
|
+
const deadFiles = findDeadFiles(snapshot, reachability);
|
|
2028
|
+
const unusedImports = findUnusedImports(snapshot);
|
|
2029
|
+
const deadInternals = findDeadInternals(snapshot, reachability);
|
|
2030
|
+
const totalExports = snapshot.files.reduce(
|
|
2031
|
+
(acc, file) => acc + file.exports.filter((e) => !e.isReExport).length,
|
|
2032
|
+
0
|
|
2033
|
+
);
|
|
2034
|
+
const estimatedDeadLines = deadFiles.reduce((acc, file) => acc + file.lineCount, 0);
|
|
2035
|
+
const report = {
|
|
2036
|
+
deadExports,
|
|
2037
|
+
deadFiles,
|
|
2038
|
+
deadInternals,
|
|
2039
|
+
unusedImports,
|
|
2040
|
+
stats: {
|
|
2041
|
+
filesAnalyzed: snapshot.files.length,
|
|
2042
|
+
entryPointsUsed: snapshot.entryPoints,
|
|
2043
|
+
totalExports,
|
|
2044
|
+
deadExportCount: deadExports.length,
|
|
2045
|
+
totalFiles: snapshot.files.length,
|
|
2046
|
+
deadFileCount: deadFiles.length,
|
|
2047
|
+
estimatedDeadLines
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
return Ok(report);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// src/entropy/detectors/patterns.ts
|
|
2054
|
+
var import_minimatch3 = require("minimatch");
|
|
2055
|
+
var import_path9 = require("path");
|
|
2056
|
+
function fileMatchesPattern(filePath, pattern, rootDir) {
|
|
2057
|
+
const relativePath = (0, import_path9.relative)(rootDir, filePath);
|
|
2058
|
+
return (0, import_minimatch3.minimatch)(relativePath, pattern);
|
|
2059
|
+
}
|
|
2060
|
+
function checkConfigPattern(pattern, file, rootDir) {
|
|
2061
|
+
const matches = [];
|
|
2062
|
+
const fileMatches = pattern.files.some((glob2) => fileMatchesPattern(file.path, glob2, rootDir));
|
|
2063
|
+
if (!fileMatches) {
|
|
2064
|
+
return matches;
|
|
2065
|
+
}
|
|
2066
|
+
const rule = pattern.rule;
|
|
2067
|
+
switch (rule.type) {
|
|
2068
|
+
case "must-export": {
|
|
2069
|
+
for (const name of rule.names) {
|
|
2070
|
+
const hasExport = file.exports.some((e) => e.name === name);
|
|
2071
|
+
if (!hasExport) {
|
|
2072
|
+
matches.push({
|
|
2073
|
+
line: 1,
|
|
2074
|
+
message: pattern.message || `Missing required export: "${name}"`,
|
|
2075
|
+
suggestion: `Add export for "${name}"`
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
case "must-export-default": {
|
|
2082
|
+
const hasDefault = file.exports.some((e) => e.type === "default");
|
|
2083
|
+
if (!hasDefault) {
|
|
2084
|
+
matches.push({
|
|
2085
|
+
line: 1,
|
|
2086
|
+
message: pattern.message || "File must have a default export",
|
|
2087
|
+
suggestion: "Add a default export"
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
case "no-export": {
|
|
2093
|
+
for (const name of rule.names) {
|
|
2094
|
+
const exp = file.exports.find((e) => e.name === name);
|
|
2095
|
+
if (exp) {
|
|
2096
|
+
matches.push({
|
|
2097
|
+
line: exp.location.line,
|
|
2098
|
+
message: pattern.message || `Forbidden export: "${name}"`,
|
|
2099
|
+
suggestion: `Remove export "${name}"`
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
break;
|
|
2104
|
+
}
|
|
2105
|
+
case "must-import": {
|
|
2106
|
+
const hasImport = file.imports.some(
|
|
2107
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
2108
|
+
);
|
|
2109
|
+
if (!hasImport) {
|
|
2110
|
+
matches.push({
|
|
2111
|
+
line: 1,
|
|
2112
|
+
message: pattern.message || `Missing required import from "${rule.from}"`,
|
|
2113
|
+
suggestion: `Add import from "${rule.from}"`
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
break;
|
|
2117
|
+
}
|
|
2118
|
+
case "no-import": {
|
|
2119
|
+
const forbiddenImport = file.imports.find(
|
|
2120
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
2121
|
+
);
|
|
2122
|
+
if (forbiddenImport) {
|
|
2123
|
+
matches.push({
|
|
2124
|
+
line: forbiddenImport.location.line,
|
|
2125
|
+
message: pattern.message || `Forbidden import from "${rule.from}"`,
|
|
2126
|
+
suggestion: `Remove import from "${rule.from}"`
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
case "naming": {
|
|
2132
|
+
const regex = new RegExp(rule.match);
|
|
2133
|
+
for (const exp of file.exports) {
|
|
2134
|
+
if (!regex.test(exp.name)) {
|
|
2135
|
+
let expected = "";
|
|
2136
|
+
switch (rule.convention) {
|
|
2137
|
+
case "camelCase":
|
|
2138
|
+
expected = "camelCase (e.g., myFunction)";
|
|
2139
|
+
break;
|
|
2140
|
+
case "PascalCase":
|
|
2141
|
+
expected = "PascalCase (e.g., MyClass)";
|
|
2142
|
+
break;
|
|
2143
|
+
case "UPPER_SNAKE":
|
|
2144
|
+
expected = "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)";
|
|
2145
|
+
break;
|
|
2146
|
+
case "kebab-case":
|
|
2147
|
+
expected = "kebab-case (e.g., my-component)";
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2150
|
+
matches.push({
|
|
2151
|
+
line: exp.location.line,
|
|
2152
|
+
message: pattern.message || `"${exp.name}" does not follow ${rule.convention} convention`,
|
|
2153
|
+
suggestion: `Rename to follow ${expected}`
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
case "max-exports": {
|
|
2160
|
+
if (file.exports.length > rule.count) {
|
|
2161
|
+
matches.push({
|
|
2162
|
+
line: 1,
|
|
2163
|
+
message: pattern.message || `File has ${file.exports.length} exports, max is ${rule.count}`,
|
|
2164
|
+
suggestion: `Split into multiple files or reduce exports to ${rule.count}`
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
case "max-lines": {
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
case "require-jsdoc": {
|
|
2173
|
+
if (file.jsDocComments.length === 0 && file.exports.length > 0) {
|
|
2174
|
+
matches.push({
|
|
2175
|
+
line: 1,
|
|
2176
|
+
message: pattern.message || "Exported symbols require JSDoc documentation",
|
|
2177
|
+
suggestion: "Add JSDoc comments to exports"
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return matches;
|
|
2184
|
+
}
|
|
2185
|
+
async function detectPatternViolations(snapshot, config) {
|
|
2186
|
+
const violations = [];
|
|
2187
|
+
const patterns = config?.patterns || [];
|
|
2188
|
+
for (const file of snapshot.files) {
|
|
2189
|
+
for (const pattern of patterns) {
|
|
2190
|
+
const matches = checkConfigPattern(pattern, file, snapshot.rootDir);
|
|
2191
|
+
for (const match of matches) {
|
|
2192
|
+
violations.push({
|
|
2193
|
+
pattern: pattern.name,
|
|
2194
|
+
file: file.path,
|
|
2195
|
+
line: match.line,
|
|
2196
|
+
message: match.message,
|
|
2197
|
+
suggestion: match.suggestion || "Review and fix this pattern violation",
|
|
2198
|
+
severity: pattern.severity
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
2204
|
+
const warningCount = violations.filter((v) => v.severity === "warning").length;
|
|
2205
|
+
const totalChecks = snapshot.files.length * patterns.length;
|
|
2206
|
+
const passRate = totalChecks > 0 ? (totalChecks - violations.length) / totalChecks : 1;
|
|
2207
|
+
return Ok({
|
|
2208
|
+
violations,
|
|
2209
|
+
stats: {
|
|
2210
|
+
filesChecked: snapshot.files.length,
|
|
2211
|
+
patternsApplied: patterns.length,
|
|
2212
|
+
violationCount: violations.length,
|
|
2213
|
+
errorCount,
|
|
2214
|
+
warningCount
|
|
2215
|
+
},
|
|
2216
|
+
passRate
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// src/entropy/fixers/suggestions.ts
|
|
2221
|
+
function generateDeadCodeSuggestions(report) {
|
|
2222
|
+
const suggestions = [];
|
|
2223
|
+
for (const file of report.deadFiles) {
|
|
2224
|
+
suggestions.push({
|
|
2225
|
+
type: "delete",
|
|
2226
|
+
priority: "high",
|
|
2227
|
+
source: "dead-code",
|
|
2228
|
+
relatedIssues: [`dead-file:${file.path}`],
|
|
2229
|
+
title: `Remove dead file: ${file.path.split("/").pop()}`,
|
|
2230
|
+
description: `This file is not imported by any other file and can be safely removed.`,
|
|
2231
|
+
files: [file.path],
|
|
2232
|
+
steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
|
|
2233
|
+
whyManual: "File deletion requires verification that no dynamic imports exist"
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
for (const exp of report.deadExports) {
|
|
2237
|
+
suggestions.push({
|
|
2238
|
+
type: "refactor",
|
|
2239
|
+
priority: "medium",
|
|
2240
|
+
source: "dead-code",
|
|
2241
|
+
relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
|
|
2242
|
+
title: `Remove unused export: ${exp.name}`,
|
|
2243
|
+
description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
|
|
2244
|
+
files: [exp.file],
|
|
2245
|
+
steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
|
|
2246
|
+
whyManual: "Export removal may affect external consumers not in scope"
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
for (const imp of report.unusedImports) {
|
|
2250
|
+
suggestions.push({
|
|
2251
|
+
type: "delete",
|
|
2252
|
+
priority: "medium",
|
|
2253
|
+
source: "dead-code",
|
|
2254
|
+
relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
|
|
2255
|
+
title: `Remove unused import${imp.specifiers.length > 1 ? "s" : ""}: ${imp.specifiers.join(", ")}`,
|
|
2256
|
+
description: `The import${imp.specifiers.length > 1 ? "s" : ""} from "${imp.source}" ${imp.specifiers.length > 1 ? "are" : "is"} not used.`,
|
|
2257
|
+
files: [imp.file],
|
|
2258
|
+
steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
|
|
2259
|
+
whyManual: "Import removal can be auto-fixed"
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
return suggestions;
|
|
2263
|
+
}
|
|
2264
|
+
function generateDriftSuggestions(report) {
|
|
2265
|
+
const suggestions = [];
|
|
2266
|
+
for (const drift of report.drifts) {
|
|
2267
|
+
const priority = drift.confidence === "high" ? "high" : "medium";
|
|
2268
|
+
suggestions.push({
|
|
2269
|
+
type: "update-docs",
|
|
2270
|
+
priority,
|
|
2271
|
+
source: "drift",
|
|
2272
|
+
relatedIssues: [`drift:${drift.docFile}:${drift.reference}`],
|
|
2273
|
+
title: `Fix documentation drift: ${drift.reference}`,
|
|
2274
|
+
description: drift.details,
|
|
2275
|
+
files: [drift.docFile],
|
|
2276
|
+
steps: [
|
|
2277
|
+
drift.suggestion || "Review and update documentation",
|
|
2278
|
+
"Review documentation for accuracy"
|
|
2279
|
+
],
|
|
2280
|
+
whyManual: "Documentation updates require human judgment for accuracy"
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
return suggestions;
|
|
2284
|
+
}
|
|
2285
|
+
function generatePatternSuggestions(report) {
|
|
2286
|
+
const suggestions = [];
|
|
2287
|
+
for (const violation of report.violations) {
|
|
2288
|
+
suggestions.push({
|
|
2289
|
+
type: "refactor",
|
|
2290
|
+
priority: violation.severity === "error" ? "high" : "low",
|
|
2291
|
+
source: "pattern",
|
|
2292
|
+
relatedIssues: [`pattern:${violation.pattern}:${violation.file}`],
|
|
2293
|
+
title: `Fix pattern violation: ${violation.pattern}`,
|
|
2294
|
+
description: violation.message,
|
|
2295
|
+
files: [violation.file],
|
|
2296
|
+
steps: [violation.suggestion || "Follow pattern guidelines"],
|
|
2297
|
+
whyManual: "Pattern violations often require architectural decisions"
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return suggestions;
|
|
2301
|
+
}
|
|
2302
|
+
function generateSuggestions(deadCode, drift, patterns) {
|
|
2303
|
+
const suggestions = [];
|
|
2304
|
+
if (deadCode) {
|
|
2305
|
+
suggestions.push(...generateDeadCodeSuggestions(deadCode));
|
|
2306
|
+
}
|
|
2307
|
+
if (drift) {
|
|
2308
|
+
suggestions.push(...generateDriftSuggestions(drift));
|
|
2309
|
+
}
|
|
2310
|
+
if (patterns) {
|
|
2311
|
+
suggestions.push(...generatePatternSuggestions(patterns));
|
|
2312
|
+
}
|
|
2313
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
2314
|
+
suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
2315
|
+
const byPriority = {
|
|
2316
|
+
high: suggestions.filter((s) => s.priority === "high"),
|
|
2317
|
+
medium: suggestions.filter((s) => s.priority === "medium"),
|
|
2318
|
+
low: suggestions.filter((s) => s.priority === "low")
|
|
2319
|
+
};
|
|
2320
|
+
let estimatedEffort;
|
|
2321
|
+
if (suggestions.length === 0) {
|
|
2322
|
+
estimatedEffort = "trivial";
|
|
2323
|
+
} else if (suggestions.length <= 5) {
|
|
2324
|
+
estimatedEffort = "small";
|
|
2325
|
+
} else if (suggestions.length <= 20) {
|
|
2326
|
+
estimatedEffort = "medium";
|
|
2327
|
+
} else {
|
|
2328
|
+
estimatedEffort = "large";
|
|
2329
|
+
}
|
|
2330
|
+
return {
|
|
2331
|
+
suggestions,
|
|
2332
|
+
byPriority,
|
|
2333
|
+
estimatedEffort
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/entropy/analyzer.ts
|
|
2338
|
+
var EntropyAnalyzer = class {
|
|
2339
|
+
config;
|
|
2340
|
+
snapshot;
|
|
2341
|
+
report;
|
|
2342
|
+
constructor(config) {
|
|
2343
|
+
this.config = {
|
|
2344
|
+
...config,
|
|
2345
|
+
parser: config.parser || new TypeScriptParser()
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Run full entropy analysis
|
|
2350
|
+
*/
|
|
2351
|
+
async analyze() {
|
|
2352
|
+
const startTime = Date.now();
|
|
2353
|
+
const snapshotResult = await buildSnapshot(this.config);
|
|
2354
|
+
if (!snapshotResult.ok) {
|
|
2355
|
+
return Err(snapshotResult.error);
|
|
2356
|
+
}
|
|
2357
|
+
this.snapshot = snapshotResult.value;
|
|
2358
|
+
let driftReport;
|
|
2359
|
+
let deadCodeReport;
|
|
2360
|
+
let patternReport;
|
|
2361
|
+
const analysisErrors = [];
|
|
2362
|
+
if (this.config.analyze.drift) {
|
|
2363
|
+
const driftConfig = typeof this.config.analyze.drift === "object" ? this.config.analyze.drift : {};
|
|
2364
|
+
const result = await detectDocDrift(this.snapshot, driftConfig);
|
|
2365
|
+
if (result.ok) {
|
|
2366
|
+
driftReport = result.value;
|
|
2367
|
+
} else {
|
|
2368
|
+
analysisErrors.push({ analyzer: "drift", error: result.error });
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (this.config.analyze.deadCode) {
|
|
2372
|
+
const result = await detectDeadCode(this.snapshot);
|
|
2373
|
+
if (result.ok) {
|
|
2374
|
+
deadCodeReport = result.value;
|
|
2375
|
+
} else {
|
|
2376
|
+
analysisErrors.push({ analyzer: "deadCode", error: result.error });
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (this.config.analyze.patterns) {
|
|
2380
|
+
const patternConfig = typeof this.config.analyze.patterns === "object" ? this.config.analyze.patterns : { patterns: [] };
|
|
2381
|
+
const result = await detectPatternViolations(this.snapshot, patternConfig);
|
|
2382
|
+
if (result.ok) {
|
|
2383
|
+
patternReport = result.value;
|
|
2384
|
+
} else {
|
|
2385
|
+
analysisErrors.push({ analyzer: "patterns", error: result.error });
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
const driftIssues = driftReport?.drifts.length || 0;
|
|
2389
|
+
const deadCodeIssues = (deadCodeReport?.deadExports.length || 0) + (deadCodeReport?.deadFiles.length || 0) + (deadCodeReport?.unusedImports.length || 0);
|
|
2390
|
+
const patternIssues = patternReport?.violations.length || 0;
|
|
2391
|
+
const patternErrors = patternReport?.stats.errorCount || 0;
|
|
2392
|
+
const patternWarnings = patternReport?.stats.warningCount || 0;
|
|
2393
|
+
const totalIssues = driftIssues + deadCodeIssues + patternIssues;
|
|
2394
|
+
const fixableCount = (deadCodeReport?.deadFiles.length || 0) + (deadCodeReport?.unusedImports.length || 0);
|
|
2395
|
+
const suggestions = generateSuggestions(deadCodeReport, driftReport, patternReport);
|
|
2396
|
+
const duration = Date.now() - startTime;
|
|
2397
|
+
const report = {
|
|
2398
|
+
snapshot: this.snapshot,
|
|
2399
|
+
analysisErrors,
|
|
2400
|
+
summary: {
|
|
2401
|
+
totalIssues,
|
|
2402
|
+
errors: patternErrors,
|
|
2403
|
+
warnings: patternWarnings + driftIssues,
|
|
2404
|
+
fixableCount,
|
|
2405
|
+
suggestionCount: suggestions.suggestions.length
|
|
2406
|
+
},
|
|
2407
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2408
|
+
duration
|
|
2409
|
+
};
|
|
2410
|
+
if (driftReport) {
|
|
2411
|
+
report.drift = driftReport;
|
|
2412
|
+
}
|
|
2413
|
+
if (deadCodeReport) {
|
|
2414
|
+
report.deadCode = deadCodeReport;
|
|
2415
|
+
}
|
|
2416
|
+
if (patternReport) {
|
|
2417
|
+
report.patterns = patternReport;
|
|
2418
|
+
}
|
|
2419
|
+
this.report = report;
|
|
2420
|
+
return Ok(report);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Get the built snapshot (must call analyze first)
|
|
2424
|
+
*/
|
|
2425
|
+
getSnapshot() {
|
|
2426
|
+
return this.snapshot;
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* Get the last report (must call analyze first)
|
|
2430
|
+
*/
|
|
2431
|
+
getReport() {
|
|
2432
|
+
return this.report;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Generate suggestions from the last analysis
|
|
2436
|
+
*/
|
|
2437
|
+
getSuggestions() {
|
|
2438
|
+
if (!this.report) {
|
|
2439
|
+
return {
|
|
2440
|
+
suggestions: [],
|
|
2441
|
+
byPriority: { high: [], medium: [], low: [] },
|
|
2442
|
+
estimatedEffort: "trivial"
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
return generateSuggestions(this.report.deadCode, this.report.drift, this.report.patterns);
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Build snapshot without running analysis
|
|
2449
|
+
*/
|
|
2450
|
+
async buildSnapshot() {
|
|
2451
|
+
const result = await buildSnapshot(this.config);
|
|
2452
|
+
if (result.ok) {
|
|
2453
|
+
this.snapshot = result.value;
|
|
2454
|
+
}
|
|
2455
|
+
return result;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Ensure snapshot is built, returning the snapshot or an error
|
|
2459
|
+
*/
|
|
2460
|
+
async ensureSnapshot() {
|
|
2461
|
+
if (this.snapshot) {
|
|
2462
|
+
return Ok(this.snapshot);
|
|
2463
|
+
}
|
|
2464
|
+
return this.buildSnapshot();
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Run drift detection only (snapshot must be built first)
|
|
2468
|
+
*/
|
|
2469
|
+
async detectDrift(config) {
|
|
2470
|
+
const snapshotResult = await this.ensureSnapshot();
|
|
2471
|
+
if (!snapshotResult.ok) {
|
|
2472
|
+
return Err(snapshotResult.error);
|
|
2473
|
+
}
|
|
2474
|
+
return detectDocDrift(snapshotResult.value, config || {});
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Run dead code detection only (snapshot must be built first)
|
|
2478
|
+
*/
|
|
2479
|
+
async detectDeadCode() {
|
|
2480
|
+
const snapshotResult = await this.ensureSnapshot();
|
|
2481
|
+
if (!snapshotResult.ok) {
|
|
2482
|
+
return Err(snapshotResult.error);
|
|
2483
|
+
}
|
|
2484
|
+
return detectDeadCode(snapshotResult.value);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Run pattern detection only (snapshot must be built first)
|
|
2488
|
+
*/
|
|
2489
|
+
async detectPatterns(config) {
|
|
2490
|
+
const snapshotResult = await this.ensureSnapshot();
|
|
2491
|
+
if (!snapshotResult.ok) {
|
|
2492
|
+
return Err(snapshotResult.error);
|
|
2493
|
+
}
|
|
2494
|
+
return detectPatternViolations(snapshotResult.value, config);
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2498
|
+
// src/entropy/fixers/safe-fixes.ts
|
|
2499
|
+
var fs = __toESM(require("fs"));
|
|
2500
|
+
var import_util2 = require("util");
|
|
2501
|
+
var import_path10 = require("path");
|
|
2502
|
+
var readFile3 = (0, import_util2.promisify)(fs.readFile);
|
|
2503
|
+
var writeFile2 = (0, import_util2.promisify)(fs.writeFile);
|
|
2504
|
+
var unlink2 = (0, import_util2.promisify)(fs.unlink);
|
|
2505
|
+
var mkdir2 = (0, import_util2.promisify)(fs.mkdir);
|
|
2506
|
+
var copyFile2 = (0, import_util2.promisify)(fs.copyFile);
|
|
2507
|
+
var DEFAULT_FIX_CONFIG = {
|
|
2508
|
+
dryRun: false,
|
|
2509
|
+
fixTypes: ["unused-imports", "dead-files"],
|
|
2510
|
+
createBackup: true,
|
|
2511
|
+
backupDir: ".entropy-backups"
|
|
2512
|
+
};
|
|
2513
|
+
function createDeadFileFixes(deadCodeReport) {
|
|
2514
|
+
return deadCodeReport.deadFiles.map((file) => ({
|
|
2515
|
+
type: "dead-files",
|
|
2516
|
+
file: file.path,
|
|
2517
|
+
description: `Delete dead file (${file.reason}): ${(0, import_path10.basename)(file.path)}`,
|
|
2518
|
+
action: "delete-file",
|
|
2519
|
+
safe: true,
|
|
2520
|
+
reversible: true
|
|
2521
|
+
}));
|
|
2522
|
+
}
|
|
2523
|
+
function createUnusedImportFixes(deadCodeReport) {
|
|
2524
|
+
return deadCodeReport.unusedImports.map((imp) => ({
|
|
2525
|
+
type: "unused-imports",
|
|
2526
|
+
file: imp.file,
|
|
2527
|
+
description: `Remove unused import: ${imp.specifiers.join(", ")} from ${imp.source}`,
|
|
2528
|
+
action: "delete-lines",
|
|
2529
|
+
line: imp.line,
|
|
2530
|
+
safe: true,
|
|
2531
|
+
reversible: true
|
|
2532
|
+
}));
|
|
2533
|
+
}
|
|
2534
|
+
function createFixes(deadCodeReport, config) {
|
|
2535
|
+
const fullConfig = { ...DEFAULT_FIX_CONFIG, ...config };
|
|
2536
|
+
const fixes = [];
|
|
2537
|
+
if (fullConfig.fixTypes.includes("dead-files")) {
|
|
2538
|
+
fixes.push(...createDeadFileFixes(deadCodeReport));
|
|
2539
|
+
}
|
|
2540
|
+
if (fullConfig.fixTypes.includes("unused-imports")) {
|
|
2541
|
+
fixes.push(...createUnusedImportFixes(deadCodeReport));
|
|
2542
|
+
}
|
|
2543
|
+
return fixes;
|
|
2544
|
+
}
|
|
2545
|
+
function previewFix(fix) {
|
|
2546
|
+
switch (fix.action) {
|
|
2547
|
+
case "delete-file":
|
|
2548
|
+
return `Would delete file: ${fix.file}`;
|
|
2549
|
+
case "delete-lines":
|
|
2550
|
+
return `Would delete line ${fix.line} in ${fix.file}: ${fix.description}`;
|
|
2551
|
+
case "replace":
|
|
2552
|
+
return `Would replace in ${fix.file}:
|
|
2553
|
+
- ${fix.oldContent}
|
|
2554
|
+
+ ${fix.newContent}`;
|
|
2555
|
+
case "insert":
|
|
2556
|
+
return `Would insert at line ${fix.line} in ${fix.file}:
|
|
2557
|
+
+ ${fix.newContent}`;
|
|
2558
|
+
default:
|
|
2559
|
+
return `Would apply fix: ${fix.description}`;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
async function createBackup(filePath, backupDir) {
|
|
2563
|
+
const backupPath = (0, import_path10.join)(backupDir, `${Date.now()}-${(0, import_path10.basename)(filePath)}`);
|
|
2564
|
+
try {
|
|
2565
|
+
await mkdir2((0, import_path10.dirname)(backupPath), { recursive: true });
|
|
2566
|
+
await copyFile2(filePath, backupPath);
|
|
2567
|
+
return Ok(backupPath);
|
|
2568
|
+
} catch (e) {
|
|
2569
|
+
return Err(
|
|
2570
|
+
createEntropyError(
|
|
2571
|
+
"BACKUP_FAILED",
|
|
2572
|
+
`Failed to create backup: ${filePath}`,
|
|
2573
|
+
{ file: filePath, originalError: e },
|
|
2574
|
+
["Check file permissions", "Ensure backup directory is writable"]
|
|
2575
|
+
)
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
async function applySingleFix(fix, config) {
|
|
2580
|
+
if (config.dryRun) {
|
|
2581
|
+
return Ok(fix);
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
switch (fix.action) {
|
|
2585
|
+
case "delete-file":
|
|
2586
|
+
if (config.createBackup && config.backupDir) {
|
|
2587
|
+
const backupResult = await createBackup(fix.file, config.backupDir);
|
|
2588
|
+
if (!backupResult.ok) {
|
|
2589
|
+
return Err({ fix, error: backupResult.error.message });
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
await unlink2(fix.file);
|
|
2593
|
+
break;
|
|
2594
|
+
case "delete-lines":
|
|
2595
|
+
if (fix.line !== void 0) {
|
|
2596
|
+
const content = await readFile3(fix.file, "utf-8");
|
|
2597
|
+
const lines = content.split("\n");
|
|
2598
|
+
lines.splice(fix.line - 1, 1);
|
|
2599
|
+
await writeFile2(fix.file, lines.join("\n"));
|
|
2600
|
+
}
|
|
2601
|
+
break;
|
|
2602
|
+
case "replace":
|
|
2603
|
+
if (fix.oldContent && fix.newContent !== void 0) {
|
|
2604
|
+
const content = await readFile3(fix.file, "utf-8");
|
|
2605
|
+
const newContent = content.replace(fix.oldContent, fix.newContent);
|
|
2606
|
+
await writeFile2(fix.file, newContent);
|
|
2607
|
+
}
|
|
2608
|
+
break;
|
|
2609
|
+
case "insert":
|
|
2610
|
+
if (fix.line !== void 0 && fix.newContent) {
|
|
2611
|
+
const content = await readFile3(fix.file, "utf-8");
|
|
2612
|
+
const lines = content.split("\n");
|
|
2613
|
+
lines.splice(fix.line - 1, 0, fix.newContent);
|
|
2614
|
+
await writeFile2(fix.file, lines.join("\n"));
|
|
2615
|
+
}
|
|
2616
|
+
break;
|
|
2617
|
+
}
|
|
2618
|
+
return Ok(fix);
|
|
2619
|
+
} catch (e) {
|
|
2620
|
+
return Err({ fix, error: e.message });
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
async function applyFixes(fixes, config) {
|
|
2624
|
+
const fullConfig = { ...DEFAULT_FIX_CONFIG, ...config };
|
|
2625
|
+
const applied = [];
|
|
2626
|
+
const skipped = [];
|
|
2627
|
+
const errors = [];
|
|
2628
|
+
let filesModified = 0;
|
|
2629
|
+
let filesDeleted = 0;
|
|
2630
|
+
let linesRemoved = 0;
|
|
2631
|
+
for (const fix of fixes) {
|
|
2632
|
+
if (!fullConfig.fixTypes.includes(fix.type)) {
|
|
2633
|
+
skipped.push(fix);
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
const result = await applySingleFix(fix, fullConfig);
|
|
2637
|
+
if (result.ok) {
|
|
2638
|
+
applied.push(result.value);
|
|
2639
|
+
if (fix.action === "delete-file") {
|
|
2640
|
+
filesDeleted++;
|
|
2641
|
+
} else {
|
|
2642
|
+
filesModified++;
|
|
2643
|
+
}
|
|
2644
|
+
if (fix.action === "delete-lines") {
|
|
2645
|
+
linesRemoved++;
|
|
2646
|
+
}
|
|
2647
|
+
} else {
|
|
2648
|
+
errors.push(result.error);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return Ok({
|
|
2652
|
+
applied,
|
|
2653
|
+
skipped,
|
|
2654
|
+
errors,
|
|
2655
|
+
stats: {
|
|
2656
|
+
filesModified,
|
|
2657
|
+
filesDeleted,
|
|
2658
|
+
linesRemoved
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// src/entropy/config/schema.ts
|
|
2664
|
+
var import_zod = require("zod");
|
|
2665
|
+
var MustExportRuleSchema = import_zod.z.object({
|
|
2666
|
+
type: import_zod.z.literal("must-export"),
|
|
2667
|
+
names: import_zod.z.array(import_zod.z.string())
|
|
2668
|
+
});
|
|
2669
|
+
var MustExportDefaultRuleSchema = import_zod.z.object({
|
|
2670
|
+
type: import_zod.z.literal("must-export-default"),
|
|
2671
|
+
kind: import_zod.z.enum(["class", "function", "object"]).optional()
|
|
2672
|
+
});
|
|
2673
|
+
var NoExportRuleSchema = import_zod.z.object({
|
|
2674
|
+
type: import_zod.z.literal("no-export"),
|
|
2675
|
+
names: import_zod.z.array(import_zod.z.string())
|
|
2676
|
+
});
|
|
2677
|
+
var MustImportRuleSchema = import_zod.z.object({
|
|
2678
|
+
type: import_zod.z.literal("must-import"),
|
|
2679
|
+
from: import_zod.z.string(),
|
|
2680
|
+
names: import_zod.z.array(import_zod.z.string()).optional()
|
|
2681
|
+
});
|
|
2682
|
+
var NoImportRuleSchema = import_zod.z.object({
|
|
2683
|
+
type: import_zod.z.literal("no-import"),
|
|
2684
|
+
from: import_zod.z.string()
|
|
2685
|
+
});
|
|
2686
|
+
var NamingRuleSchema = import_zod.z.object({
|
|
2687
|
+
type: import_zod.z.literal("naming"),
|
|
2688
|
+
match: import_zod.z.string(),
|
|
2689
|
+
convention: import_zod.z.enum(["camelCase", "PascalCase", "UPPER_SNAKE", "kebab-case"])
|
|
2690
|
+
});
|
|
2691
|
+
var MaxExportsRuleSchema = import_zod.z.object({
|
|
2692
|
+
type: import_zod.z.literal("max-exports"),
|
|
2693
|
+
count: import_zod.z.number().positive()
|
|
2694
|
+
});
|
|
2695
|
+
var MaxLinesRuleSchema = import_zod.z.object({
|
|
2696
|
+
type: import_zod.z.literal("max-lines"),
|
|
2697
|
+
count: import_zod.z.number().positive()
|
|
2698
|
+
});
|
|
2699
|
+
var RequireJSDocRuleSchema = import_zod.z.object({
|
|
2700
|
+
type: import_zod.z.literal("require-jsdoc"),
|
|
2701
|
+
for: import_zod.z.array(import_zod.z.enum(["function", "class", "export"]))
|
|
2702
|
+
});
|
|
2703
|
+
var RuleSchema = import_zod.z.discriminatedUnion("type", [
|
|
2704
|
+
MustExportRuleSchema,
|
|
2705
|
+
MustExportDefaultRuleSchema,
|
|
2706
|
+
NoExportRuleSchema,
|
|
2707
|
+
MustImportRuleSchema,
|
|
2708
|
+
NoImportRuleSchema,
|
|
2709
|
+
NamingRuleSchema,
|
|
2710
|
+
MaxExportsRuleSchema,
|
|
2711
|
+
MaxLinesRuleSchema,
|
|
2712
|
+
RequireJSDocRuleSchema
|
|
2713
|
+
]);
|
|
2714
|
+
var ConfigPatternSchema = import_zod.z.object({
|
|
2715
|
+
name: import_zod.z.string().min(1),
|
|
2716
|
+
description: import_zod.z.string(),
|
|
2717
|
+
severity: import_zod.z.enum(["error", "warning"]),
|
|
2718
|
+
files: import_zod.z.array(import_zod.z.string()),
|
|
2719
|
+
rule: RuleSchema,
|
|
2720
|
+
message: import_zod.z.string().optional()
|
|
2721
|
+
});
|
|
2722
|
+
var PatternConfigSchema = import_zod.z.object({
|
|
2723
|
+
patterns: import_zod.z.array(ConfigPatternSchema),
|
|
2724
|
+
customPatterns: import_zod.z.array(import_zod.z.any()).optional(),
|
|
2725
|
+
// Code patterns are functions, can't validate
|
|
2726
|
+
ignoreFiles: import_zod.z.array(import_zod.z.string()).optional()
|
|
2727
|
+
});
|
|
2728
|
+
var DriftConfigSchema = import_zod.z.object({
|
|
2729
|
+
docPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2730
|
+
checkApiSignatures: import_zod.z.boolean().optional(),
|
|
2731
|
+
checkExamples: import_zod.z.boolean().optional(),
|
|
2732
|
+
checkStructure: import_zod.z.boolean().optional(),
|
|
2733
|
+
ignorePatterns: import_zod.z.array(import_zod.z.string()).optional()
|
|
2734
|
+
});
|
|
2735
|
+
var DeadCodeConfigSchema = import_zod.z.object({
|
|
2736
|
+
entryPoints: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2737
|
+
includeTypes: import_zod.z.boolean().optional(),
|
|
2738
|
+
includeInternals: import_zod.z.boolean().optional(),
|
|
2739
|
+
ignorePatterns: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2740
|
+
treatDynamicImportsAs: import_zod.z.enum(["used", "unknown"]).optional()
|
|
2741
|
+
});
|
|
2742
|
+
var EntropyConfigSchema = import_zod.z.object({
|
|
2743
|
+
rootDir: import_zod.z.string(),
|
|
2744
|
+
parser: import_zod.z.any().optional(),
|
|
2745
|
+
// LanguageParser instance, can't validate
|
|
2746
|
+
entryPoints: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2747
|
+
analyze: import_zod.z.object({
|
|
2748
|
+
drift: import_zod.z.union([import_zod.z.boolean(), DriftConfigSchema]).optional(),
|
|
2749
|
+
deadCode: import_zod.z.union([import_zod.z.boolean(), DeadCodeConfigSchema]).optional(),
|
|
2750
|
+
patterns: import_zod.z.union([import_zod.z.boolean(), PatternConfigSchema]).optional()
|
|
2751
|
+
}),
|
|
2752
|
+
include: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2753
|
+
exclude: import_zod.z.array(import_zod.z.string()).optional(),
|
|
2754
|
+
docPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
2755
|
+
});
|
|
2756
|
+
function validatePatternConfig(config) {
|
|
2757
|
+
const result = PatternConfigSchema.safeParse(config);
|
|
2758
|
+
if (!result.success) {
|
|
2759
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2760
|
+
return Err(
|
|
2761
|
+
createEntropyError(
|
|
2762
|
+
"CONFIG_VALIDATION_ERROR",
|
|
2763
|
+
`Invalid pattern config: ${issues}`,
|
|
2764
|
+
{ issues: result.error.issues },
|
|
2765
|
+
["Check the pattern config matches the schema"]
|
|
2766
|
+
)
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
return Ok(result.data);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/feedback/telemetry/noop.ts
|
|
2773
|
+
var NoOpTelemetryAdapter = class {
|
|
2774
|
+
name = "noop";
|
|
2775
|
+
async health() {
|
|
2776
|
+
return Ok({ available: true, message: "NoOp adapter - no real telemetry" });
|
|
2777
|
+
}
|
|
2778
|
+
async getMetrics() {
|
|
2779
|
+
return Ok([]);
|
|
2780
|
+
}
|
|
2781
|
+
async getTraces() {
|
|
2782
|
+
return Ok([]);
|
|
2783
|
+
}
|
|
2784
|
+
async getLogs() {
|
|
2785
|
+
return Ok([]);
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
|
|
2789
|
+
// src/shared/uuid.ts
|
|
2790
|
+
function generateId() {
|
|
2791
|
+
if (typeof globalThis !== "undefined" && "crypto" in globalThis && typeof globalThis.crypto.randomUUID === "function") {
|
|
2792
|
+
return globalThis.crypto.randomUUID();
|
|
2793
|
+
}
|
|
2794
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
|
2795
|
+
const r = Math.random() * 16 | 0;
|
|
2796
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
2797
|
+
return v.toString(16);
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// src/feedback/executor/noop.ts
|
|
2802
|
+
var NoOpExecutor = class {
|
|
2803
|
+
name = "noop";
|
|
2804
|
+
processes = /* @__PURE__ */ new Map();
|
|
2805
|
+
async health() {
|
|
2806
|
+
return Ok({ available: true, message: "NoOp executor - no real agent spawning" });
|
|
2807
|
+
}
|
|
2808
|
+
async spawn(config) {
|
|
2809
|
+
const id = generateId();
|
|
2810
|
+
const process2 = {
|
|
2811
|
+
id,
|
|
2812
|
+
status: "completed",
|
|
2813
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2814
|
+
config
|
|
2815
|
+
};
|
|
2816
|
+
this.processes.set(id, process2);
|
|
2817
|
+
return Ok(process2);
|
|
2818
|
+
}
|
|
2819
|
+
async status(processId) {
|
|
2820
|
+
const process2 = this.processes.get(processId);
|
|
2821
|
+
if (!process2) {
|
|
2822
|
+
return Err({
|
|
2823
|
+
code: "AGENT_SPAWN_ERROR",
|
|
2824
|
+
message: "Process not found",
|
|
2825
|
+
details: { agentId: processId },
|
|
2826
|
+
suggestions: ["Check if the process ID is correct"]
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
return Ok(process2);
|
|
2830
|
+
}
|
|
2831
|
+
async wait(processId) {
|
|
2832
|
+
const process2 = this.processes.get(processId);
|
|
2833
|
+
if (!process2) {
|
|
2834
|
+
return Err({
|
|
2835
|
+
code: "AGENT_SPAWN_ERROR",
|
|
2836
|
+
message: "Process not found",
|
|
2837
|
+
details: { agentId: processId },
|
|
2838
|
+
suggestions: ["Check if the process ID is correct"]
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
return Ok({
|
|
2842
|
+
agentId: processId,
|
|
2843
|
+
agentType: process2.config.type,
|
|
2844
|
+
approved: true,
|
|
2845
|
+
comments: [],
|
|
2846
|
+
suggestions: [],
|
|
2847
|
+
duration: 0,
|
|
2848
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
async kill(processId) {
|
|
2852
|
+
this.processes.delete(processId);
|
|
2853
|
+
return Ok(void 0);
|
|
2854
|
+
}
|
|
2855
|
+
};
|
|
2856
|
+
|
|
2857
|
+
// src/feedback/logging/console-sink.ts
|
|
2858
|
+
var ConsoleSink = class {
|
|
2859
|
+
name = "console";
|
|
2860
|
+
options;
|
|
2861
|
+
constructor(options = {}) {
|
|
2862
|
+
this.options = {
|
|
2863
|
+
level: options.level ?? "info",
|
|
2864
|
+
format: options.format ?? "pretty",
|
|
2865
|
+
verbose: options.verbose ?? false
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
async write(action) {
|
|
2869
|
+
const output = this.options.format === "json" ? JSON.stringify(action) : this.formatPretty(action);
|
|
2870
|
+
console.log(output);
|
|
2871
|
+
return Ok(void 0);
|
|
2872
|
+
}
|
|
2873
|
+
formatPretty(action) {
|
|
2874
|
+
const status = action.status === "completed" ? "\u2713" : action.status === "failed" ? "\u2717" : "\u2192";
|
|
2875
|
+
const duration = action.duration ? ` (${action.duration}ms)` : "";
|
|
2876
|
+
return `[${status}] ${action.type}${duration}: ${action.result?.summary ?? action.status}`;
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2880
|
+
// src/feedback/config.ts
|
|
2881
|
+
function getDefaults() {
|
|
2882
|
+
return {
|
|
2883
|
+
telemetry: new NoOpTelemetryAdapter(),
|
|
2884
|
+
executor: new NoOpExecutor(),
|
|
2885
|
+
sinks: [new ConsoleSink()],
|
|
2886
|
+
emitEvents: true,
|
|
2887
|
+
defaultTimeout: 3e5,
|
|
2888
|
+
rootDir: process.cwd()
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
var feedbackConfig = null;
|
|
2892
|
+
function ensureConfig() {
|
|
2893
|
+
if (!feedbackConfig) {
|
|
2894
|
+
feedbackConfig = getDefaults();
|
|
2895
|
+
}
|
|
2896
|
+
return feedbackConfig;
|
|
2897
|
+
}
|
|
2898
|
+
function configureFeedback(config) {
|
|
2899
|
+
feedbackConfig = { ...ensureConfig(), ...config };
|
|
2900
|
+
}
|
|
2901
|
+
function getFeedbackConfig() {
|
|
2902
|
+
return Object.freeze({ ...ensureConfig() });
|
|
2903
|
+
}
|
|
2904
|
+
function resetFeedbackConfig() {
|
|
2905
|
+
feedbackConfig = null;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/feedback/review/diff-analyzer.ts
|
|
2909
|
+
function parseDiff(diff) {
|
|
2910
|
+
try {
|
|
2911
|
+
if (!diff.trim()) {
|
|
2912
|
+
return Ok({ diff, files: [] });
|
|
2913
|
+
}
|
|
2914
|
+
const files = [];
|
|
2915
|
+
const newFileRegex = /new file mode/;
|
|
2916
|
+
const deletedFileRegex = /deleted file mode/;
|
|
2917
|
+
const additionRegex = /^\+(?!\+\+)/gm;
|
|
2918
|
+
const deletionRegex = /^-(?!--)/gm;
|
|
2919
|
+
const diffParts = diff.split(/(?=diff --git)/);
|
|
2920
|
+
for (const part of diffParts) {
|
|
2921
|
+
if (!part.trim()) continue;
|
|
2922
|
+
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
2923
|
+
if (!headerMatch || !headerMatch[2]) continue;
|
|
2924
|
+
const filePath = headerMatch[2];
|
|
2925
|
+
let status = "modified";
|
|
2926
|
+
if (newFileRegex.test(part)) {
|
|
2927
|
+
status = "added";
|
|
2928
|
+
} else if (deletedFileRegex.test(part)) {
|
|
2929
|
+
status = "deleted";
|
|
2930
|
+
} else if (part.includes("rename from")) {
|
|
2931
|
+
status = "renamed";
|
|
2932
|
+
}
|
|
2933
|
+
const additions = (part.match(additionRegex) || []).length;
|
|
2934
|
+
const deletions = (part.match(deletionRegex) || []).length;
|
|
2935
|
+
files.push({
|
|
2936
|
+
path: filePath,
|
|
2937
|
+
status,
|
|
2938
|
+
additions,
|
|
2939
|
+
deletions
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
return Ok({ diff, files });
|
|
2943
|
+
} catch (error) {
|
|
2944
|
+
return Err({
|
|
2945
|
+
code: "DIFF_PARSE_ERROR",
|
|
2946
|
+
message: "Failed to parse git diff",
|
|
2947
|
+
details: { reason: String(error) },
|
|
2948
|
+
suggestions: ["Ensure diff is in valid git diff format"]
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
async function analyzeDiff(changes, options) {
|
|
2953
|
+
if (!options?.enabled) {
|
|
2954
|
+
return Ok([]);
|
|
2955
|
+
}
|
|
2956
|
+
const items = [];
|
|
2957
|
+
let itemId = 0;
|
|
2958
|
+
if (options.forbiddenPatterns) {
|
|
2959
|
+
for (const forbidden of options.forbiddenPatterns) {
|
|
2960
|
+
const pattern = typeof forbidden.pattern === "string" ? new RegExp(forbidden.pattern, "g") : forbidden.pattern;
|
|
2961
|
+
if (pattern.test(changes.diff)) {
|
|
2962
|
+
items.push({
|
|
2963
|
+
id: `diff-${++itemId}`,
|
|
2964
|
+
category: "diff",
|
|
2965
|
+
check: `Forbidden pattern: ${forbidden.pattern}`,
|
|
2966
|
+
passed: false,
|
|
2967
|
+
severity: forbidden.severity,
|
|
2968
|
+
details: forbidden.message,
|
|
2969
|
+
suggestion: `Remove occurrences of ${forbidden.pattern}`
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (options.maxChangedFiles && changes.files.length > options.maxChangedFiles) {
|
|
2975
|
+
items.push({
|
|
2976
|
+
id: `diff-${++itemId}`,
|
|
2977
|
+
category: "diff",
|
|
2978
|
+
check: `PR size: ${changes.files.length} files changed`,
|
|
2979
|
+
passed: false,
|
|
2980
|
+
severity: "warning",
|
|
2981
|
+
details: `This PR changes ${changes.files.length} files, which exceeds the recommended maximum of ${options.maxChangedFiles}`,
|
|
2982
|
+
suggestion: "Consider breaking this into smaller PRs"
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
if (options.maxFileSize) {
|
|
2986
|
+
for (const file of changes.files) {
|
|
2987
|
+
const totalLines = file.additions + file.deletions;
|
|
2988
|
+
if (totalLines > options.maxFileSize) {
|
|
2989
|
+
items.push({
|
|
2990
|
+
id: `diff-${++itemId}`,
|
|
2991
|
+
category: "diff",
|
|
2992
|
+
check: `File size: ${file.path}`,
|
|
2993
|
+
passed: false,
|
|
2994
|
+
severity: "warning",
|
|
2995
|
+
details: `File has ${totalLines} lines changed, exceeding limit of ${options.maxFileSize}`,
|
|
2996
|
+
file: file.path,
|
|
2997
|
+
suggestion: "Consider splitting this file into smaller modules"
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
if (options.checkTestCoverage) {
|
|
3003
|
+
const addedSourceFiles = changes.files.filter(
|
|
3004
|
+
(f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
|
|
3005
|
+
);
|
|
3006
|
+
const testFiles = changes.files.filter((f) => f.path.includes(".test."));
|
|
3007
|
+
for (const sourceFile of addedSourceFiles) {
|
|
3008
|
+
const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
|
|
3009
|
+
const hasTest = testFiles.some(
|
|
3010
|
+
(t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
|
|
3011
|
+
);
|
|
3012
|
+
if (!hasTest) {
|
|
3013
|
+
items.push({
|
|
3014
|
+
id: `diff-${++itemId}`,
|
|
3015
|
+
category: "diff",
|
|
3016
|
+
check: `Test coverage: ${sourceFile.path}`,
|
|
3017
|
+
passed: false,
|
|
3018
|
+
severity: "warning",
|
|
3019
|
+
details: "New source file added without corresponding test file",
|
|
3020
|
+
file: sourceFile.path,
|
|
3021
|
+
suggestion: `Add tests in ${expectedTestPath}`
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return Ok(items);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/feedback/review/checklist.ts
|
|
3030
|
+
var ChecklistBuilder = class {
|
|
3031
|
+
rootDir;
|
|
3032
|
+
harnessOptions;
|
|
3033
|
+
customRules = [];
|
|
3034
|
+
diffOptions;
|
|
3035
|
+
constructor(rootDir) {
|
|
3036
|
+
this.rootDir = rootDir;
|
|
3037
|
+
}
|
|
3038
|
+
withHarnessChecks(options) {
|
|
3039
|
+
this.harnessOptions = options ?? { context: true, constraints: true, entropy: true };
|
|
3040
|
+
return this;
|
|
3041
|
+
}
|
|
3042
|
+
addRule(rule) {
|
|
3043
|
+
this.customRules.push(rule);
|
|
3044
|
+
return this;
|
|
3045
|
+
}
|
|
3046
|
+
addRules(rules) {
|
|
3047
|
+
this.customRules.push(...rules);
|
|
3048
|
+
return this;
|
|
3049
|
+
}
|
|
3050
|
+
withDiffAnalysis(options) {
|
|
3051
|
+
this.diffOptions = options;
|
|
3052
|
+
return this;
|
|
3053
|
+
}
|
|
3054
|
+
async run(changes) {
|
|
3055
|
+
const startTime = Date.now();
|
|
3056
|
+
const items = [];
|
|
3057
|
+
if (this.harnessOptions) {
|
|
3058
|
+
if (this.harnessOptions.context) {
|
|
3059
|
+
items.push({
|
|
3060
|
+
id: "harness-context",
|
|
3061
|
+
category: "harness",
|
|
3062
|
+
check: "Context Engineering (AGENTS.md, doc coverage)",
|
|
3063
|
+
passed: true,
|
|
3064
|
+
severity: "info",
|
|
3065
|
+
details: "Harness context validation not yet integrated. See Module 2 (context/).",
|
|
3066
|
+
suggestion: "Integrate with validateAgentsMap(), checkDocCoverage() from context module"
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
if (this.harnessOptions.constraints) {
|
|
3070
|
+
items.push({
|
|
3071
|
+
id: "harness-constraints",
|
|
3072
|
+
category: "harness",
|
|
3073
|
+
check: "Architectural Constraints (dependencies, boundaries)",
|
|
3074
|
+
passed: true,
|
|
3075
|
+
severity: "info",
|
|
3076
|
+
details: "Harness constraints validation not yet integrated. See Module 3 (constraints/).",
|
|
3077
|
+
suggestion: "Integrate with validateDependencies(), detectCircularDeps() from constraints module"
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
if (this.harnessOptions.entropy) {
|
|
3081
|
+
items.push({
|
|
3082
|
+
id: "harness-entropy",
|
|
3083
|
+
category: "harness",
|
|
3084
|
+
check: "Entropy Management (drift, dead code)",
|
|
3085
|
+
passed: true,
|
|
3086
|
+
severity: "info",
|
|
3087
|
+
details: "Harness entropy validation not yet integrated. See Module 4 (entropy/).",
|
|
3088
|
+
suggestion: "Integrate with EntropyAnalyzer from entropy module"
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
for (const rule of this.customRules) {
|
|
3093
|
+
try {
|
|
3094
|
+
const result = await rule.check(changes, this.rootDir);
|
|
3095
|
+
const item = {
|
|
3096
|
+
id: rule.id,
|
|
3097
|
+
category: "custom",
|
|
3098
|
+
check: rule.name,
|
|
3099
|
+
passed: result.passed,
|
|
3100
|
+
severity: rule.severity,
|
|
3101
|
+
details: result.details
|
|
3102
|
+
};
|
|
3103
|
+
if (result.suggestion !== void 0) {
|
|
3104
|
+
item.suggestion = result.suggestion;
|
|
3105
|
+
}
|
|
3106
|
+
if (result.file !== void 0) {
|
|
3107
|
+
item.file = result.file;
|
|
3108
|
+
}
|
|
3109
|
+
if (result.line !== void 0) {
|
|
3110
|
+
item.line = result.line;
|
|
3111
|
+
}
|
|
3112
|
+
items.push(item);
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
items.push({
|
|
3115
|
+
id: rule.id,
|
|
3116
|
+
category: "custom",
|
|
3117
|
+
check: rule.name,
|
|
3118
|
+
passed: false,
|
|
3119
|
+
severity: "error",
|
|
3120
|
+
details: `Rule execution failed: ${String(error)}`
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
if (this.diffOptions) {
|
|
3125
|
+
const diffResult = await analyzeDiff(changes, this.diffOptions);
|
|
3126
|
+
if (diffResult.ok) {
|
|
3127
|
+
items.push(...diffResult.value);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
const passed = items.filter((i) => i.passed).length;
|
|
3131
|
+
const failed = items.filter((i) => !i.passed).length;
|
|
3132
|
+
const errors = items.filter((i) => !i.passed && i.severity === "error").length;
|
|
3133
|
+
const warnings = items.filter((i) => !i.passed && i.severity === "warning").length;
|
|
3134
|
+
const checklist = {
|
|
3135
|
+
items,
|
|
3136
|
+
passed: failed === 0,
|
|
3137
|
+
// Pass if no failed items
|
|
3138
|
+
summary: {
|
|
3139
|
+
total: items.length,
|
|
3140
|
+
passed,
|
|
3141
|
+
failed,
|
|
3142
|
+
errors,
|
|
3143
|
+
warnings
|
|
3144
|
+
},
|
|
3145
|
+
duration: Date.now() - startTime
|
|
3146
|
+
};
|
|
3147
|
+
return Ok(checklist);
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
|
|
3151
|
+
// src/feedback/review/self-review.ts
|
|
3152
|
+
async function createSelfReview(changes, config) {
|
|
3153
|
+
const builder = new ChecklistBuilder(config.rootDir);
|
|
3154
|
+
if (config.harness) {
|
|
3155
|
+
builder.withHarnessChecks(config.harness);
|
|
3156
|
+
}
|
|
3157
|
+
if (config.customRules) {
|
|
3158
|
+
builder.addRules(config.customRules);
|
|
3159
|
+
}
|
|
3160
|
+
if (config.diffAnalysis) {
|
|
3161
|
+
builder.withDiffAnalysis(config.diffAnalysis);
|
|
3162
|
+
}
|
|
3163
|
+
return builder.run(changes);
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// src/feedback/logging/emitter.ts
|
|
3167
|
+
var AgentActionEmitter = class {
|
|
3168
|
+
listeners = /* @__PURE__ */ new Map();
|
|
3169
|
+
on(eventType, handler) {
|
|
3170
|
+
if (!this.listeners.has(eventType)) {
|
|
3171
|
+
this.listeners.set(eventType, /* @__PURE__ */ new Set());
|
|
3172
|
+
}
|
|
3173
|
+
this.listeners.get(eventType).add(handler);
|
|
3174
|
+
return () => this.off(eventType, handler);
|
|
3175
|
+
}
|
|
3176
|
+
once(eventType, handler) {
|
|
3177
|
+
const wrappedHandler = (event) => {
|
|
3178
|
+
this.off(eventType, wrappedHandler);
|
|
3179
|
+
return handler(event);
|
|
3180
|
+
};
|
|
3181
|
+
return this.on(eventType, wrappedHandler);
|
|
3182
|
+
}
|
|
3183
|
+
off(eventType, handler) {
|
|
3184
|
+
this.listeners.get(eventType)?.delete(handler);
|
|
3185
|
+
}
|
|
3186
|
+
emit(event) {
|
|
3187
|
+
this.listeners.get(event.type)?.forEach((handler) => {
|
|
3188
|
+
try {
|
|
3189
|
+
handler(event);
|
|
3190
|
+
} catch (e) {
|
|
3191
|
+
console.error("Error in action event handler:", e);
|
|
3192
|
+
}
|
|
3193
|
+
});
|
|
3194
|
+
if (event.type !== "action:*") {
|
|
3195
|
+
this.listeners.get("action:*")?.forEach((handler) => {
|
|
3196
|
+
try {
|
|
3197
|
+
handler(event);
|
|
3198
|
+
} catch (e) {
|
|
3199
|
+
console.error("Error in wildcard action event handler:", e);
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
listenerCount(eventType) {
|
|
3205
|
+
return this.listeners.get(eventType)?.size ?? 0;
|
|
3206
|
+
}
|
|
3207
|
+
removeAllListeners() {
|
|
3208
|
+
this.listeners.clear();
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
var globalEmitter = null;
|
|
3212
|
+
function getActionEmitter() {
|
|
3213
|
+
if (!globalEmitter) {
|
|
3214
|
+
globalEmitter = new AgentActionEmitter();
|
|
3215
|
+
}
|
|
3216
|
+
return globalEmitter;
|
|
3217
|
+
}
|
|
3218
|
+
async function logAgentAction(action) {
|
|
3219
|
+
const fullAction = {
|
|
3220
|
+
...action,
|
|
3221
|
+
id: generateId(),
|
|
3222
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3223
|
+
};
|
|
3224
|
+
const config = getFeedbackConfig();
|
|
3225
|
+
if (config.emitEvents) {
|
|
3226
|
+
const eventType = action.status === "completed" ? "action:completed" : action.status === "failed" ? "action:failed" : "action:started";
|
|
3227
|
+
getActionEmitter().emit({
|
|
3228
|
+
type: eventType,
|
|
3229
|
+
action: fullAction,
|
|
3230
|
+
timestamp: fullAction.timestamp
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
if (config.sinks) {
|
|
3234
|
+
for (const sink of config.sinks) {
|
|
3235
|
+
await sink.write(fullAction);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return Ok(fullAction);
|
|
3239
|
+
}
|
|
3240
|
+
function trackAction(type, context) {
|
|
3241
|
+
const startTime = Date.now();
|
|
3242
|
+
const action = {
|
|
3243
|
+
id: generateId(),
|
|
3244
|
+
type,
|
|
3245
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3246
|
+
status: "started",
|
|
3247
|
+
context
|
|
3248
|
+
};
|
|
3249
|
+
const config = getFeedbackConfig();
|
|
3250
|
+
if (config.emitEvents) {
|
|
3251
|
+
getActionEmitter().emit({
|
|
3252
|
+
type: "action:started",
|
|
3253
|
+
action,
|
|
3254
|
+
timestamp: action.timestamp
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
return {
|
|
3258
|
+
get action() {
|
|
3259
|
+
return action;
|
|
3260
|
+
},
|
|
3261
|
+
async complete(result) {
|
|
3262
|
+
action.status = "completed";
|
|
3263
|
+
action.duration = Date.now() - startTime;
|
|
3264
|
+
action.result = result;
|
|
3265
|
+
return logAgentAction(action);
|
|
3266
|
+
},
|
|
3267
|
+
async fail(error) {
|
|
3268
|
+
action.status = "failed";
|
|
3269
|
+
action.duration = Date.now() - startTime;
|
|
3270
|
+
action.error = error;
|
|
3271
|
+
return logAgentAction(action);
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/feedback/review/peer-review.ts
|
|
3277
|
+
async function requestPeerReview(agentType, context, options) {
|
|
3278
|
+
const config = getFeedbackConfig();
|
|
3279
|
+
const executor = config.executor;
|
|
3280
|
+
if (!executor) {
|
|
3281
|
+
return Err({
|
|
3282
|
+
code: "AGENT_SPAWN_ERROR",
|
|
3283
|
+
message: "No agent executor configured",
|
|
3284
|
+
details: {},
|
|
3285
|
+
suggestions: ["Configure an AgentExecutor via configureFeedback()"]
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
const tracker = trackAction("peer-review", {
|
|
3289
|
+
trigger: "agent",
|
|
3290
|
+
files: context.files
|
|
3291
|
+
});
|
|
3292
|
+
try {
|
|
3293
|
+
const spawnConfig = {
|
|
3294
|
+
type: agentType,
|
|
3295
|
+
context
|
|
3296
|
+
};
|
|
3297
|
+
if (options?.customAgentType) {
|
|
3298
|
+
spawnConfig.customType = options.customAgentType;
|
|
3299
|
+
}
|
|
3300
|
+
if (options?.skills) {
|
|
3301
|
+
spawnConfig.skills = options.skills;
|
|
3302
|
+
}
|
|
3303
|
+
if (options?.timeout !== void 0) {
|
|
3304
|
+
spawnConfig.timeout = options.timeout;
|
|
3305
|
+
} else if (config.defaultTimeout !== void 0) {
|
|
3306
|
+
spawnConfig.timeout = config.defaultTimeout;
|
|
3307
|
+
}
|
|
3308
|
+
const spawnResult = await executor.spawn(spawnConfig);
|
|
3309
|
+
if (!spawnResult.ok) {
|
|
3310
|
+
await tracker.fail({ code: spawnResult.error.code, message: spawnResult.error.message });
|
|
3311
|
+
return spawnResult;
|
|
3312
|
+
}
|
|
3313
|
+
if (options?.wait !== false) {
|
|
3314
|
+
const waitResult = await executor.wait(
|
|
3315
|
+
spawnResult.value.id,
|
|
3316
|
+
options?.timeout ?? config.defaultTimeout
|
|
3317
|
+
);
|
|
3318
|
+
if (!waitResult.ok) {
|
|
3319
|
+
await tracker.fail({ code: waitResult.error.code, message: waitResult.error.message });
|
|
3320
|
+
return waitResult;
|
|
3321
|
+
}
|
|
3322
|
+
await tracker.complete({
|
|
3323
|
+
outcome: waitResult.value.approved ? "success" : "failure",
|
|
3324
|
+
summary: waitResult.value.approved ? "Review approved" : `Review rejected: ${waitResult.value.comments.length} comments`,
|
|
3325
|
+
data: waitResult.value
|
|
3326
|
+
});
|
|
3327
|
+
return waitResult;
|
|
3328
|
+
}
|
|
3329
|
+
await tracker.complete({
|
|
3330
|
+
outcome: "success",
|
|
3331
|
+
summary: `Agent spawned: ${spawnResult.value.id}`,
|
|
3332
|
+
data: { processId: spawnResult.value.id }
|
|
3333
|
+
});
|
|
3334
|
+
return Ok({
|
|
3335
|
+
agentId: spawnResult.value.id,
|
|
3336
|
+
agentType,
|
|
3337
|
+
approved: false,
|
|
3338
|
+
// Unknown until wait
|
|
3339
|
+
comments: [],
|
|
3340
|
+
suggestions: [],
|
|
3341
|
+
duration: 0,
|
|
3342
|
+
completedAt: ""
|
|
3343
|
+
});
|
|
3344
|
+
} catch (error) {
|
|
3345
|
+
await tracker.fail({
|
|
3346
|
+
code: "AGENT_SPAWN_ERROR",
|
|
3347
|
+
message: String(error)
|
|
3348
|
+
});
|
|
3349
|
+
return Err({
|
|
3350
|
+
code: "AGENT_SPAWN_ERROR",
|
|
3351
|
+
message: "Failed to request peer review",
|
|
3352
|
+
details: { reason: String(error) },
|
|
3353
|
+
suggestions: ["Check executor configuration", "Verify agent availability"]
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
async function requestMultiplePeerReviews(requests) {
|
|
3358
|
+
if (requests.length === 0) {
|
|
3359
|
+
return Ok([]);
|
|
3360
|
+
}
|
|
3361
|
+
const results = await Promise.all(
|
|
3362
|
+
requests.map(
|
|
3363
|
+
({ agentType, context, options }) => requestPeerReview(agentType, context, options)
|
|
3364
|
+
)
|
|
3365
|
+
);
|
|
3366
|
+
const firstError = results.find((r) => !r.ok);
|
|
3367
|
+
if (firstError && !firstError.ok) {
|
|
3368
|
+
return Err(firstError.error);
|
|
3369
|
+
}
|
|
3370
|
+
return Ok(results.map((r) => r.value));
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/feedback/logging/file-sink.ts
|
|
3374
|
+
var import_fs2 = require("fs");
|
|
3375
|
+
var import_path11 = require("path");
|
|
3376
|
+
var FileSink = class {
|
|
3377
|
+
name = "file";
|
|
3378
|
+
filePath;
|
|
3379
|
+
options;
|
|
3380
|
+
buffer = [];
|
|
3381
|
+
flushTimer;
|
|
3382
|
+
initialized = false;
|
|
3383
|
+
constructor(filePath, options = {}) {
|
|
3384
|
+
this.filePath = filePath;
|
|
3385
|
+
this.options = {
|
|
3386
|
+
mode: options.mode ?? "append",
|
|
3387
|
+
bufferSize: options.bufferSize ?? 1
|
|
3388
|
+
};
|
|
3389
|
+
if (options.flushInterval !== void 0) {
|
|
3390
|
+
this.options.flushInterval = options.flushInterval;
|
|
3391
|
+
this.flushTimer = setInterval(() => {
|
|
3392
|
+
this.flush();
|
|
3393
|
+
}, this.options.flushInterval);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
ensureDirectory() {
|
|
3397
|
+
if (!this.initialized) {
|
|
3398
|
+
const dir = (0, import_path11.dirname)(this.filePath);
|
|
3399
|
+
if (!(0, import_fs2.existsSync)(dir)) {
|
|
3400
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
3401
|
+
}
|
|
3402
|
+
this.initialized = true;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
async write(action) {
|
|
3406
|
+
try {
|
|
3407
|
+
const line = JSON.stringify(action) + "\n";
|
|
3408
|
+
this.buffer.push(line);
|
|
3409
|
+
if (this.buffer.length >= (this.options.bufferSize ?? 1)) {
|
|
3410
|
+
return this.flush();
|
|
3411
|
+
}
|
|
3412
|
+
return Ok(void 0);
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
return Err({
|
|
3415
|
+
code: "SINK_ERROR",
|
|
3416
|
+
message: "Failed to write action to file",
|
|
3417
|
+
details: { reason: String(error) },
|
|
3418
|
+
suggestions: ["Check file permissions", "Verify disk space"]
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
async flush() {
|
|
3423
|
+
if (this.buffer.length === 0) {
|
|
3424
|
+
return Ok(void 0);
|
|
3425
|
+
}
|
|
3426
|
+
try {
|
|
3427
|
+
this.ensureDirectory();
|
|
3428
|
+
const content = this.buffer.join("");
|
|
3429
|
+
this.buffer = [];
|
|
3430
|
+
if (this.options.mode === "overwrite" && !(0, import_fs2.existsSync)(this.filePath)) {
|
|
3431
|
+
(0, import_fs2.writeFileSync)(this.filePath, content);
|
|
3432
|
+
} else {
|
|
3433
|
+
(0, import_fs2.appendFileSync)(this.filePath, content);
|
|
3434
|
+
}
|
|
3435
|
+
return Ok(void 0);
|
|
3436
|
+
} catch (error) {
|
|
3437
|
+
return Err({
|
|
3438
|
+
code: "SINK_ERROR",
|
|
3439
|
+
message: "Failed to flush actions to file",
|
|
3440
|
+
details: { reason: String(error) },
|
|
3441
|
+
suggestions: ["Check file permissions", "Verify disk space"]
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
async close() {
|
|
3446
|
+
if (this.flushTimer) {
|
|
3447
|
+
clearInterval(this.flushTimer);
|
|
3448
|
+
}
|
|
3449
|
+
await this.flush();
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3452
|
+
|
|
3453
|
+
// src/feedback/logging/sink.ts
|
|
3454
|
+
var NoOpSink = class {
|
|
3455
|
+
name = "noop";
|
|
3456
|
+
async write() {
|
|
3457
|
+
return Ok(void 0);
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
|
|
3461
|
+
// src/state/types.ts
|
|
3462
|
+
var import_zod2 = require("zod");
|
|
3463
|
+
var FailureEntrySchema = import_zod2.z.object({
|
|
3464
|
+
date: import_zod2.z.string(),
|
|
3465
|
+
skill: import_zod2.z.string(),
|
|
3466
|
+
type: import_zod2.z.string(),
|
|
3467
|
+
description: import_zod2.z.string()
|
|
3468
|
+
});
|
|
3469
|
+
var HandoffSchema = import_zod2.z.object({
|
|
3470
|
+
timestamp: import_zod2.z.string(),
|
|
3471
|
+
fromSkill: import_zod2.z.string(),
|
|
3472
|
+
phase: import_zod2.z.string(),
|
|
3473
|
+
summary: import_zod2.z.string(),
|
|
3474
|
+
completed: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
3475
|
+
pending: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
3476
|
+
concerns: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
3477
|
+
decisions: import_zod2.z.array(
|
|
3478
|
+
import_zod2.z.object({
|
|
3479
|
+
what: import_zod2.z.string(),
|
|
3480
|
+
why: import_zod2.z.string()
|
|
3481
|
+
})
|
|
3482
|
+
).default([]),
|
|
3483
|
+
blockers: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
3484
|
+
contextKeywords: import_zod2.z.array(import_zod2.z.string()).default([])
|
|
3485
|
+
});
|
|
3486
|
+
var GateCheckSchema = import_zod2.z.object({
|
|
3487
|
+
name: import_zod2.z.string(),
|
|
3488
|
+
passed: import_zod2.z.boolean(),
|
|
3489
|
+
command: import_zod2.z.string(),
|
|
3490
|
+
output: import_zod2.z.string().optional(),
|
|
3491
|
+
duration: import_zod2.z.number().optional()
|
|
3492
|
+
});
|
|
3493
|
+
var GateResultSchema = import_zod2.z.object({
|
|
3494
|
+
passed: import_zod2.z.boolean(),
|
|
3495
|
+
checks: import_zod2.z.array(GateCheckSchema)
|
|
3496
|
+
});
|
|
3497
|
+
var GateConfigSchema = import_zod2.z.object({
|
|
3498
|
+
checks: import_zod2.z.array(
|
|
3499
|
+
import_zod2.z.object({
|
|
3500
|
+
name: import_zod2.z.string(),
|
|
3501
|
+
command: import_zod2.z.string()
|
|
3502
|
+
})
|
|
3503
|
+
).optional(),
|
|
3504
|
+
trace: import_zod2.z.boolean().optional()
|
|
3505
|
+
});
|
|
3506
|
+
var HarnessStateSchema = import_zod2.z.object({
|
|
3507
|
+
schemaVersion: import_zod2.z.literal(1),
|
|
3508
|
+
position: import_zod2.z.object({
|
|
3509
|
+
phase: import_zod2.z.string().optional(),
|
|
3510
|
+
task: import_zod2.z.string().optional()
|
|
3511
|
+
}).default({}),
|
|
3512
|
+
decisions: import_zod2.z.array(
|
|
3513
|
+
import_zod2.z.object({
|
|
3514
|
+
date: import_zod2.z.string(),
|
|
3515
|
+
decision: import_zod2.z.string(),
|
|
3516
|
+
context: import_zod2.z.string()
|
|
3517
|
+
})
|
|
3518
|
+
).default([]),
|
|
3519
|
+
blockers: import_zod2.z.array(
|
|
3520
|
+
import_zod2.z.object({
|
|
3521
|
+
id: import_zod2.z.string(),
|
|
3522
|
+
description: import_zod2.z.string(),
|
|
3523
|
+
status: import_zod2.z.enum(["open", "resolved"])
|
|
3524
|
+
})
|
|
3525
|
+
).default([]),
|
|
3526
|
+
progress: import_zod2.z.record(import_zod2.z.enum(["pending", "in_progress", "complete"])).default({}),
|
|
3527
|
+
lastSession: import_zod2.z.object({
|
|
3528
|
+
date: import_zod2.z.string(),
|
|
3529
|
+
summary: import_zod2.z.string(),
|
|
3530
|
+
lastSkill: import_zod2.z.string().optional(),
|
|
3531
|
+
pendingTasks: import_zod2.z.array(import_zod2.z.string()).optional()
|
|
3532
|
+
}).optional()
|
|
3533
|
+
});
|
|
3534
|
+
var DEFAULT_STATE = {
|
|
3535
|
+
schemaVersion: 1,
|
|
3536
|
+
position: {},
|
|
3537
|
+
decisions: [],
|
|
3538
|
+
blockers: [],
|
|
3539
|
+
progress: {}
|
|
3540
|
+
};
|
|
3541
|
+
|
|
3542
|
+
// src/state/state-manager.ts
|
|
3543
|
+
var fs2 = __toESM(require("fs"));
|
|
3544
|
+
var path = __toESM(require("path"));
|
|
3545
|
+
var import_child_process = require("child_process");
|
|
3546
|
+
var HARNESS_DIR = ".harness";
|
|
3547
|
+
var STATE_FILE = "state.json";
|
|
3548
|
+
var LEARNINGS_FILE = "learnings.md";
|
|
3549
|
+
var FAILURES_FILE = "failures.md";
|
|
3550
|
+
var HANDOFF_FILE = "handoff.json";
|
|
3551
|
+
var GATE_CONFIG_FILE = "gate.json";
|
|
3552
|
+
async function loadState(projectPath) {
|
|
3553
|
+
const statePath = path.join(projectPath, HARNESS_DIR, STATE_FILE);
|
|
3554
|
+
if (!fs2.existsSync(statePath)) {
|
|
3555
|
+
return Ok({ ...DEFAULT_STATE });
|
|
3556
|
+
}
|
|
3557
|
+
try {
|
|
3558
|
+
const raw = fs2.readFileSync(statePath, "utf-8");
|
|
3559
|
+
const parsed = JSON.parse(raw);
|
|
3560
|
+
const result = HarnessStateSchema.safeParse(parsed);
|
|
3561
|
+
if (!result.success) {
|
|
3562
|
+
return Err(new Error(`Invalid state file ${statePath}: ${result.error.message}`));
|
|
3563
|
+
}
|
|
3564
|
+
return Ok(result.data);
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
return Err(
|
|
3567
|
+
new Error(
|
|
3568
|
+
`Failed to load state from ${statePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
3569
|
+
)
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
async function saveState(projectPath, state) {
|
|
3574
|
+
const harnessDir = path.join(projectPath, HARNESS_DIR);
|
|
3575
|
+
const statePath = path.join(harnessDir, STATE_FILE);
|
|
3576
|
+
try {
|
|
3577
|
+
fs2.mkdirSync(harnessDir, { recursive: true });
|
|
3578
|
+
fs2.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
3579
|
+
return Ok(void 0);
|
|
3580
|
+
} catch (error) {
|
|
3581
|
+
return Err(
|
|
3582
|
+
new Error(`Failed to save state: ${error instanceof Error ? error.message : String(error)}`)
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
async function appendLearning(projectPath, learning, skillName, outcome) {
|
|
3587
|
+
const harnessDir = path.join(projectPath, HARNESS_DIR);
|
|
3588
|
+
const learningsPath = path.join(harnessDir, LEARNINGS_FILE);
|
|
3589
|
+
try {
|
|
3590
|
+
fs2.mkdirSync(harnessDir, { recursive: true });
|
|
3591
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3592
|
+
let entry;
|
|
3593
|
+
if (skillName && outcome) {
|
|
3594
|
+
entry = `
|
|
3595
|
+
- **${timestamp} [skill:${skillName}] [outcome:${outcome}]:** ${learning}
|
|
3596
|
+
`;
|
|
3597
|
+
} else if (skillName) {
|
|
3598
|
+
entry = `
|
|
3599
|
+
- **${timestamp} [skill:${skillName}]:** ${learning}
|
|
3600
|
+
`;
|
|
3601
|
+
} else {
|
|
3602
|
+
entry = `
|
|
3603
|
+
- **${timestamp}:** ${learning}
|
|
3604
|
+
`;
|
|
3605
|
+
}
|
|
3606
|
+
if (!fs2.existsSync(learningsPath)) {
|
|
3607
|
+
fs2.writeFileSync(learningsPath, `# Learnings
|
|
3608
|
+
${entry}`);
|
|
3609
|
+
} else {
|
|
3610
|
+
fs2.appendFileSync(learningsPath, entry);
|
|
3611
|
+
}
|
|
3612
|
+
return Ok(void 0);
|
|
3613
|
+
} catch (error) {
|
|
3614
|
+
return Err(
|
|
3615
|
+
new Error(
|
|
3616
|
+
`Failed to append learning: ${error instanceof Error ? error.message : String(error)}`
|
|
3617
|
+
)
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
async function loadRelevantLearnings(projectPath, skillName) {
|
|
3622
|
+
const learningsPath = path.join(projectPath, HARNESS_DIR, LEARNINGS_FILE);
|
|
3623
|
+
if (!fs2.existsSync(learningsPath)) {
|
|
3624
|
+
return Ok([]);
|
|
3625
|
+
}
|
|
3626
|
+
try {
|
|
3627
|
+
const content = fs2.readFileSync(learningsPath, "utf-8");
|
|
3628
|
+
const lines = content.split("\n");
|
|
3629
|
+
const entries = [];
|
|
3630
|
+
let currentBlock = [];
|
|
3631
|
+
for (const line of lines) {
|
|
3632
|
+
if (line.startsWith("# ")) continue;
|
|
3633
|
+
const isDatedBullet = /^- \*\*\d{4}-\d{2}-\d{2}/.test(line);
|
|
3634
|
+
const isHeading = /^## \d{4}-\d{2}-\d{2}/.test(line);
|
|
3635
|
+
if (isDatedBullet || isHeading) {
|
|
3636
|
+
if (currentBlock.length > 0) {
|
|
3637
|
+
entries.push(currentBlock.join("\n"));
|
|
3638
|
+
}
|
|
3639
|
+
currentBlock = [line];
|
|
3640
|
+
} else if (line.trim() !== "" && currentBlock.length > 0) {
|
|
3641
|
+
currentBlock.push(line);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
if (currentBlock.length > 0) {
|
|
3645
|
+
entries.push(currentBlock.join("\n"));
|
|
3646
|
+
}
|
|
3647
|
+
if (!skillName) {
|
|
3648
|
+
return Ok(entries);
|
|
3649
|
+
}
|
|
3650
|
+
const filtered = entries.filter((entry) => entry.includes(`[skill:${skillName}]`));
|
|
3651
|
+
return Ok(filtered);
|
|
3652
|
+
} catch (error) {
|
|
3653
|
+
return Err(
|
|
3654
|
+
new Error(
|
|
3655
|
+
`Failed to load learnings: ${error instanceof Error ? error.message : String(error)}`
|
|
3656
|
+
)
|
|
3657
|
+
);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
var FAILURE_LINE_REGEX = /^- \*\*(\d{4}-\d{2}-\d{2}) \[skill:([^\]]+)\] \[type:([^\]]+)\]:\*\* (.+)$/;
|
|
3661
|
+
async function appendFailure(projectPath, description, skillName, type) {
|
|
3662
|
+
const harnessDir = path.join(projectPath, HARNESS_DIR);
|
|
3663
|
+
const failuresPath = path.join(harnessDir, FAILURES_FILE);
|
|
3664
|
+
try {
|
|
3665
|
+
fs2.mkdirSync(harnessDir, { recursive: true });
|
|
3666
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3667
|
+
const entry = `
|
|
3668
|
+
- **${timestamp} [skill:${skillName}] [type:${type}]:** ${description}
|
|
3669
|
+
`;
|
|
3670
|
+
if (!fs2.existsSync(failuresPath)) {
|
|
3671
|
+
fs2.writeFileSync(failuresPath, `# Failures
|
|
3672
|
+
${entry}`);
|
|
3673
|
+
} else {
|
|
3674
|
+
fs2.appendFileSync(failuresPath, entry);
|
|
3675
|
+
}
|
|
3676
|
+
return Ok(void 0);
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
return Err(
|
|
3679
|
+
new Error(
|
|
3680
|
+
`Failed to append failure: ${error instanceof Error ? error.message : String(error)}`
|
|
3681
|
+
)
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async function loadFailures(projectPath) {
|
|
3686
|
+
const failuresPath = path.join(projectPath, HARNESS_DIR, FAILURES_FILE);
|
|
3687
|
+
if (!fs2.existsSync(failuresPath)) {
|
|
3688
|
+
return Ok([]);
|
|
3689
|
+
}
|
|
3690
|
+
try {
|
|
3691
|
+
const content = fs2.readFileSync(failuresPath, "utf-8");
|
|
3692
|
+
const entries = [];
|
|
3693
|
+
for (const line of content.split("\n")) {
|
|
3694
|
+
const match = line.match(FAILURE_LINE_REGEX);
|
|
3695
|
+
if (match) {
|
|
3696
|
+
entries.push({
|
|
3697
|
+
date: match[1] ?? "",
|
|
3698
|
+
skill: match[2] ?? "",
|
|
3699
|
+
type: match[3] ?? "",
|
|
3700
|
+
description: match[4] ?? ""
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
return Ok(entries);
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
return Err(
|
|
3707
|
+
new Error(
|
|
3708
|
+
`Failed to load failures: ${error instanceof Error ? error.message : String(error)}`
|
|
3709
|
+
)
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
async function archiveFailures(projectPath) {
|
|
3714
|
+
const harnessDir = path.join(projectPath, HARNESS_DIR);
|
|
3715
|
+
const failuresPath = path.join(harnessDir, FAILURES_FILE);
|
|
3716
|
+
if (!fs2.existsSync(failuresPath)) {
|
|
3717
|
+
return Ok(void 0);
|
|
3718
|
+
}
|
|
3719
|
+
try {
|
|
3720
|
+
const archiveDir = path.join(harnessDir, "archive");
|
|
3721
|
+
fs2.mkdirSync(archiveDir, { recursive: true });
|
|
3722
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3723
|
+
let archiveName = `failures-${date}.md`;
|
|
3724
|
+
let counter = 2;
|
|
3725
|
+
while (fs2.existsSync(path.join(archiveDir, archiveName))) {
|
|
3726
|
+
archiveName = `failures-${date}-${counter}.md`;
|
|
3727
|
+
counter++;
|
|
3728
|
+
}
|
|
3729
|
+
fs2.renameSync(failuresPath, path.join(archiveDir, archiveName));
|
|
3730
|
+
return Ok(void 0);
|
|
3731
|
+
} catch (error) {
|
|
3732
|
+
return Err(
|
|
3733
|
+
new Error(
|
|
3734
|
+
`Failed to archive failures: ${error instanceof Error ? error.message : String(error)}`
|
|
3735
|
+
)
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
async function saveHandoff(projectPath, handoff) {
|
|
3740
|
+
const harnessDir = path.join(projectPath, HARNESS_DIR);
|
|
3741
|
+
const handoffPath = path.join(harnessDir, HANDOFF_FILE);
|
|
3742
|
+
try {
|
|
3743
|
+
fs2.mkdirSync(harnessDir, { recursive: true });
|
|
3744
|
+
fs2.writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
|
|
3745
|
+
return Ok(void 0);
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
return Err(
|
|
3748
|
+
new Error(`Failed to save handoff: ${error instanceof Error ? error.message : String(error)}`)
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
async function loadHandoff(projectPath) {
|
|
3753
|
+
const handoffPath = path.join(projectPath, HARNESS_DIR, HANDOFF_FILE);
|
|
3754
|
+
if (!fs2.existsSync(handoffPath)) {
|
|
3755
|
+
return Ok(null);
|
|
3756
|
+
}
|
|
3757
|
+
try {
|
|
3758
|
+
const raw = fs2.readFileSync(handoffPath, "utf-8");
|
|
3759
|
+
const parsed = JSON.parse(raw);
|
|
3760
|
+
const result = HandoffSchema.safeParse(parsed);
|
|
3761
|
+
if (!result.success) {
|
|
3762
|
+
return Err(new Error(`Invalid handoff file: ${result.error.message}`));
|
|
3763
|
+
}
|
|
3764
|
+
return Ok(result.data);
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
return Err(
|
|
3767
|
+
new Error(`Failed to load handoff: ${error instanceof Error ? error.message : String(error)}`)
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
async function runMechanicalGate(projectPath) {
|
|
3772
|
+
const harnessDir = path.join(projectPath, HARNESS_DIR);
|
|
3773
|
+
const gateConfigPath = path.join(harnessDir, GATE_CONFIG_FILE);
|
|
3774
|
+
try {
|
|
3775
|
+
let checks = [];
|
|
3776
|
+
if (fs2.existsSync(gateConfigPath)) {
|
|
3777
|
+
const raw = JSON.parse(fs2.readFileSync(gateConfigPath, "utf-8"));
|
|
3778
|
+
const config = GateConfigSchema.safeParse(raw);
|
|
3779
|
+
if (config.success && config.data.checks) {
|
|
3780
|
+
checks = config.data.checks;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
if (checks.length === 0) {
|
|
3784
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
3785
|
+
if (fs2.existsSync(packageJsonPath)) {
|
|
3786
|
+
const pkg = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
|
|
3787
|
+
const scripts = pkg.scripts || {};
|
|
3788
|
+
if (scripts.test) checks.push({ name: "test", command: "npm test" });
|
|
3789
|
+
if (scripts.lint) checks.push({ name: "lint", command: "npm run lint" });
|
|
3790
|
+
if (scripts.typecheck) checks.push({ name: "typecheck", command: "npm run typecheck" });
|
|
3791
|
+
if (scripts.build) checks.push({ name: "build", command: "npm run build" });
|
|
3792
|
+
}
|
|
3793
|
+
if (fs2.existsSync(path.join(projectPath, "go.mod"))) {
|
|
3794
|
+
checks.push({ name: "test", command: "go test ./..." });
|
|
3795
|
+
checks.push({ name: "build", command: "go build ./..." });
|
|
3796
|
+
}
|
|
3797
|
+
if (fs2.existsSync(path.join(projectPath, "pyproject.toml")) || fs2.existsSync(path.join(projectPath, "setup.py"))) {
|
|
3798
|
+
checks.push({ name: "test", command: "python -m pytest" });
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
const results = [];
|
|
3802
|
+
for (const check of checks) {
|
|
3803
|
+
const start = Date.now();
|
|
3804
|
+
try {
|
|
3805
|
+
(0, import_child_process.execSync)(check.command, {
|
|
3806
|
+
cwd: projectPath,
|
|
3807
|
+
stdio: "pipe",
|
|
3808
|
+
timeout: 12e4
|
|
3809
|
+
});
|
|
3810
|
+
results.push({
|
|
3811
|
+
name: check.name,
|
|
3812
|
+
passed: true,
|
|
3813
|
+
command: check.command,
|
|
3814
|
+
duration: Date.now() - start
|
|
3815
|
+
});
|
|
3816
|
+
} catch (error) {
|
|
3817
|
+
const output = error instanceof Error ? error.stderr?.toString() || error.message : String(error);
|
|
3818
|
+
results.push({
|
|
3819
|
+
name: check.name,
|
|
3820
|
+
passed: false,
|
|
3821
|
+
command: check.command,
|
|
3822
|
+
output: output.slice(0, 2e3),
|
|
3823
|
+
duration: Date.now() - start
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
return Ok({
|
|
3828
|
+
passed: results.length === 0 || results.every((r) => r.passed),
|
|
3829
|
+
checks: results
|
|
3830
|
+
});
|
|
3831
|
+
} catch (error) {
|
|
3832
|
+
return Err(
|
|
3833
|
+
new Error(
|
|
3834
|
+
`Failed to run mechanical gate: ${error instanceof Error ? error.message : String(error)}`
|
|
3835
|
+
)
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// src/workflow/runner.ts
|
|
3841
|
+
async function executeWorkflow(workflow, executor) {
|
|
3842
|
+
const stepResults = [];
|
|
3843
|
+
const startTime = Date.now();
|
|
3844
|
+
let previousArtifact;
|
|
3845
|
+
let stopped = false;
|
|
3846
|
+
for (const step of workflow.steps) {
|
|
3847
|
+
if (stopped) {
|
|
3848
|
+
stepResults.push({
|
|
3849
|
+
step,
|
|
3850
|
+
outcome: "skipped",
|
|
3851
|
+
durationMs: 0
|
|
3852
|
+
});
|
|
3853
|
+
continue;
|
|
3854
|
+
}
|
|
3855
|
+
const stepResult = await executor(step, previousArtifact);
|
|
3856
|
+
stepResults.push(stepResult);
|
|
3857
|
+
if (stepResult.outcome === "pass") {
|
|
3858
|
+
previousArtifact = stepResult.artifact;
|
|
3859
|
+
} else {
|
|
3860
|
+
const gate = step.gate ?? "pass-required";
|
|
3861
|
+
if (gate === "pass-required") {
|
|
3862
|
+
stopped = true;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
const hasFailure = stepResults.some((r) => r.outcome === "fail");
|
|
3867
|
+
return {
|
|
3868
|
+
workflow,
|
|
3869
|
+
stepResults,
|
|
3870
|
+
pass: !hasFailure,
|
|
3871
|
+
totalDurationMs: Date.now() - startTime
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
// src/pipeline/skill-pipeline.ts
|
|
3876
|
+
async function runPipeline(initialContext, executor, options) {
|
|
3877
|
+
const startTime = Date.now();
|
|
3878
|
+
const hooks = options?.hooks;
|
|
3879
|
+
let context = { ...initialContext };
|
|
3880
|
+
if (hooks?.preExecution) {
|
|
3881
|
+
const updated = hooks.preExecution(context);
|
|
3882
|
+
if (updated === null) {
|
|
3883
|
+
return {
|
|
3884
|
+
success: false,
|
|
3885
|
+
context,
|
|
3886
|
+
error: "Pre-execution hook rejected the context",
|
|
3887
|
+
turnsExecuted: 0,
|
|
3888
|
+
durationMs: Date.now() - startTime
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
context = updated;
|
|
3892
|
+
}
|
|
3893
|
+
let result;
|
|
3894
|
+
try {
|
|
3895
|
+
result = await executor(context);
|
|
3896
|
+
} catch (e) {
|
|
3897
|
+
return {
|
|
3898
|
+
success: false,
|
|
3899
|
+
context,
|
|
3900
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3901
|
+
turnsExecuted: 1,
|
|
3902
|
+
durationMs: Date.now() - startTime
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
if (hooks?.postExecution) {
|
|
3906
|
+
try {
|
|
3907
|
+
hooks.postExecution(context, result);
|
|
3908
|
+
} catch {
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
return {
|
|
3912
|
+
success: result.success,
|
|
3913
|
+
context,
|
|
3914
|
+
result,
|
|
3915
|
+
turnsExecuted: 1,
|
|
3916
|
+
durationMs: Date.now() - startTime
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
3920
|
+
const startTime = Date.now();
|
|
3921
|
+
const hooks = options?.hooks;
|
|
3922
|
+
const maxTurns = options?.maxTurns ?? 10;
|
|
3923
|
+
let context = { ...initialContext };
|
|
3924
|
+
if (hooks?.preExecution) {
|
|
3925
|
+
const updated = hooks.preExecution(context);
|
|
3926
|
+
if (updated === null) {
|
|
3927
|
+
return {
|
|
3928
|
+
success: false,
|
|
3929
|
+
context,
|
|
3930
|
+
error: "Pre-execution hook rejected the context",
|
|
3931
|
+
turnsExecuted: 0,
|
|
3932
|
+
durationMs: Date.now() - startTime
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
context = updated;
|
|
3936
|
+
}
|
|
3937
|
+
const previousResults = [];
|
|
3938
|
+
let turnsExecuted = 0;
|
|
3939
|
+
let lastError;
|
|
3940
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
3941
|
+
const turnContext = {
|
|
3942
|
+
...context,
|
|
3943
|
+
turnNumber: turn + 1,
|
|
3944
|
+
previousResults: [...previousResults]
|
|
3945
|
+
};
|
|
3946
|
+
let filteredTurnContext = turnContext;
|
|
3947
|
+
if (hooks?.perTurn) {
|
|
3948
|
+
const updated = hooks.perTurn(turnContext);
|
|
3949
|
+
if (updated === null) {
|
|
3950
|
+
break;
|
|
3951
|
+
}
|
|
3952
|
+
filteredTurnContext = updated;
|
|
3953
|
+
}
|
|
3954
|
+
try {
|
|
3955
|
+
const turnResult = await turnExecutor(filteredTurnContext);
|
|
3956
|
+
previousResults.push(turnResult.result);
|
|
3957
|
+
turnsExecuted++;
|
|
3958
|
+
if (turnResult.done) {
|
|
3959
|
+
break;
|
|
3960
|
+
}
|
|
3961
|
+
} catch (e) {
|
|
3962
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
3963
|
+
turnsExecuted++;
|
|
3964
|
+
break;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
const skillResult = {
|
|
3968
|
+
success: !lastError,
|
|
3969
|
+
artifacts: [],
|
|
3970
|
+
summary: lastError ?? `Completed in ${turnsExecuted} turns`
|
|
3971
|
+
};
|
|
3972
|
+
if (hooks?.postExecution) {
|
|
3973
|
+
try {
|
|
3974
|
+
hooks.postExecution(context, skillResult);
|
|
3975
|
+
} catch {
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
return {
|
|
3979
|
+
success: !lastError,
|
|
3980
|
+
context,
|
|
3981
|
+
result: skillResult,
|
|
3982
|
+
...lastError !== void 0 && { error: lastError },
|
|
3983
|
+
turnsExecuted,
|
|
3984
|
+
durationMs: Date.now() - startTime
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
// src/index.ts
|
|
3989
|
+
var VERSION = "0.5.0";
|
|
3990
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3991
|
+
0 && (module.exports = {
|
|
3992
|
+
AgentActionEmitter,
|
|
3993
|
+
ChecklistBuilder,
|
|
3994
|
+
ConsoleSink,
|
|
3995
|
+
DEFAULT_STATE,
|
|
3996
|
+
EntropyAnalyzer,
|
|
3997
|
+
EntropyConfigSchema,
|
|
3998
|
+
Err,
|
|
3999
|
+
FailureEntrySchema,
|
|
4000
|
+
FileSink,
|
|
4001
|
+
GateConfigSchema,
|
|
4002
|
+
GateResultSchema,
|
|
4003
|
+
HandoffSchema,
|
|
4004
|
+
HarnessStateSchema,
|
|
4005
|
+
NoOpExecutor,
|
|
4006
|
+
NoOpSink,
|
|
4007
|
+
NoOpTelemetryAdapter,
|
|
4008
|
+
Ok,
|
|
4009
|
+
PatternConfigSchema,
|
|
4010
|
+
REQUIRED_SECTIONS,
|
|
4011
|
+
TypeScriptParser,
|
|
4012
|
+
VERSION,
|
|
4013
|
+
analyzeDiff,
|
|
4014
|
+
appendFailure,
|
|
4015
|
+
appendLearning,
|
|
4016
|
+
applyFixes,
|
|
4017
|
+
archiveFailures,
|
|
4018
|
+
buildDependencyGraph,
|
|
4019
|
+
buildReachabilityMap,
|
|
4020
|
+
buildSnapshot,
|
|
4021
|
+
checkConfigPattern,
|
|
4022
|
+
checkDocCoverage,
|
|
4023
|
+
configureFeedback,
|
|
4024
|
+
contextBudget,
|
|
4025
|
+
contextFilter,
|
|
4026
|
+
createBoundaryValidator,
|
|
4027
|
+
createError,
|
|
4028
|
+
createFixes,
|
|
4029
|
+
createParseError,
|
|
4030
|
+
createSelfReview,
|
|
4031
|
+
defineLayer,
|
|
4032
|
+
detectCircularDeps,
|
|
4033
|
+
detectCircularDepsInFiles,
|
|
4034
|
+
detectDeadCode,
|
|
4035
|
+
detectDocDrift,
|
|
4036
|
+
detectPatternViolations,
|
|
4037
|
+
executeWorkflow,
|
|
4038
|
+
extractMarkdownLinks,
|
|
4039
|
+
extractSections,
|
|
4040
|
+
findPossibleMatches,
|
|
4041
|
+
generateAgentsMap,
|
|
4042
|
+
generateSuggestions,
|
|
4043
|
+
getActionEmitter,
|
|
4044
|
+
getFeedbackConfig,
|
|
4045
|
+
getPhaseCategories,
|
|
4046
|
+
isErr,
|
|
4047
|
+
isOk,
|
|
4048
|
+
levenshteinDistance,
|
|
4049
|
+
loadFailures,
|
|
4050
|
+
loadHandoff,
|
|
4051
|
+
loadRelevantLearnings,
|
|
4052
|
+
loadState,
|
|
4053
|
+
logAgentAction,
|
|
4054
|
+
parseDiff,
|
|
4055
|
+
parseDocumentationFile,
|
|
4056
|
+
previewFix,
|
|
4057
|
+
requestMultiplePeerReviews,
|
|
4058
|
+
requestPeerReview,
|
|
4059
|
+
resetFeedbackConfig,
|
|
4060
|
+
resolveEntryPoints,
|
|
4061
|
+
resolveFileToLayer,
|
|
4062
|
+
runMechanicalGate,
|
|
4063
|
+
runMultiTurnPipeline,
|
|
4064
|
+
runPipeline,
|
|
4065
|
+
saveHandoff,
|
|
4066
|
+
saveState,
|
|
4067
|
+
trackAction,
|
|
4068
|
+
validateAgentsMap,
|
|
4069
|
+
validateBoundaries,
|
|
4070
|
+
validateCommitMessage,
|
|
4071
|
+
validateConfig,
|
|
4072
|
+
validateDependencies,
|
|
4073
|
+
validateFileStructure,
|
|
4074
|
+
validateKnowledgeMap,
|
|
4075
|
+
validatePatternConfig,
|
|
4076
|
+
...require("@harness-engineering/types")
|
|
4077
|
+
});
|