backtrace-console 0.0.1
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/README.md +22 -0
- package/app.js +22 -0
- package/bin/backtrace-cli.js +22 -0
- package/bin/www +90 -0
- package/lib/BacktraceCodexTool.js +32 -0
- package/lib/backtrace/analysis.js +356 -0
- package/lib/backtrace/constants.js +23 -0
- package/lib/backtrace/options.js +278 -0
- package/lib/backtrace/query.js +940 -0
- package/lib/backtrace/repair-fingerprint.js +405 -0
- package/lib/backtrace/repair.js +495 -0
- package/lib/backtrace/tool.js +333 -0
- package/lib/backtrace/utils.js +297 -0
- package/lib/cli/args.js +177 -0
- package/lib/cli/run.js +191 -0
- package/package.json +29 -0
- package/public/__inline_check__.js +451 -0
- package/public/index.html +642 -0
- package/public/stylesheets/style.css +186 -0
- package/routes/backtrace.js +864 -0
- package/routes/index.js +9 -0
- package/routes/users.js +9 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { normalizeFingerprint, areFingerprintsRelated, listExistingFingerprintDirectories } = require("./utils");
|
|
4
|
+
const { createRepairContext, generateRepairPlan, applyRepairPlan } = require("./repair");
|
|
5
|
+
|
|
6
|
+
const MAX_LOG_PROMPT_CHARS = 180000;
|
|
7
|
+
const MAX_SECTION_CHARS = 60000;
|
|
8
|
+
const MAX_FALLBACK_HEAD_TAIL_CHARS = 12000;
|
|
9
|
+
const CONTEXT_RADIUS = 3;
|
|
10
|
+
const REPAIR_PLAN_LOG_SAMPLE_SIZE = 5;
|
|
11
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
12
|
+
".ini",
|
|
13
|
+
".txt",
|
|
14
|
+
".md",
|
|
15
|
+
".json",
|
|
16
|
+
".js",
|
|
17
|
+
".jsx",
|
|
18
|
+
".ts",
|
|
19
|
+
".tsx",
|
|
20
|
+
".c",
|
|
21
|
+
".cc",
|
|
22
|
+
".cpp",
|
|
23
|
+
".cxx",
|
|
24
|
+
".h",
|
|
25
|
+
".hh",
|
|
26
|
+
".hpp",
|
|
27
|
+
".hxx",
|
|
28
|
+
".cs",
|
|
29
|
+
".java",
|
|
30
|
+
".kt",
|
|
31
|
+
".py",
|
|
32
|
+
".rb",
|
|
33
|
+
".php",
|
|
34
|
+
".go",
|
|
35
|
+
".swift",
|
|
36
|
+
".xml",
|
|
37
|
+
".yml",
|
|
38
|
+
".yaml",
|
|
39
|
+
".cfg",
|
|
40
|
+
".conf",
|
|
41
|
+
".usf",
|
|
42
|
+
".ush",
|
|
43
|
+
]);
|
|
44
|
+
const KEYWORD_PATTERNS = [
|
|
45
|
+
/error/i,
|
|
46
|
+
/exception/i,
|
|
47
|
+
/fatal/i,
|
|
48
|
+
/ensure/i,
|
|
49
|
+
/assert/i,
|
|
50
|
+
/stack/i,
|
|
51
|
+
/callstack/i,
|
|
52
|
+
/crash/i,
|
|
53
|
+
/Unhandled/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
async function collectFingerprintLogCandidates(logsDir, relativeDir = "") {
|
|
57
|
+
const currentDir = path.join(logsDir, relativeDir);
|
|
58
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true }).catch(() => []);
|
|
59
|
+
const files = [];
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
/* eslint-disable no-await-in-loop */
|
|
63
|
+
const nextRelativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
|
|
64
|
+
const absolutePath = path.join(logsDir, nextRelativePath);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
files.push(...await collectFingerprintLogCandidates(logsDir, nextRelativePath));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!entry.isFile() || entry.name.toLowerCase() !== "aboveland.log") {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const stats = await fs.stat(absolutePath).catch(() => null);
|
|
73
|
+
if (!stats || !stats.isFile()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
files.push({
|
|
77
|
+
relativePath: nextRelativePath,
|
|
78
|
+
absolutePath,
|
|
79
|
+
size: stats.size,
|
|
80
|
+
});
|
|
81
|
+
/* eslint-enable no-await-in-loop */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return files;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function findFingerprintLogs(fingerprint, context) {
|
|
88
|
+
const normalizedFingerprint = normalizeFingerprint(fingerprint);
|
|
89
|
+
const existingDirectories = await listExistingFingerprintDirectories(context.fingerprintsRoot);
|
|
90
|
+
const matchedDirectory = existingDirectories
|
|
91
|
+
.filter((entry) => areFingerprintsRelated(entry.name, normalizedFingerprint))
|
|
92
|
+
.sort((left, right) => left.name.length - right.name.length || left.name.localeCompare(right.name))[0];
|
|
93
|
+
const resolvedFingerprint = matchedDirectory ? matchedDirectory.name : normalizedFingerprint;
|
|
94
|
+
const logsDir = path.join(context.fingerprintsRoot, resolvedFingerprint, "logs");
|
|
95
|
+
const entries = await collectFingerprintLogCandidates(logsDir);
|
|
96
|
+
if (entries.length === 0) {
|
|
97
|
+
throw new Error(`No AboveLand.LOG found under fingerprint logs: ${resolvedFingerprint}`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
fingerprint: resolvedFingerprint,
|
|
101
|
+
logs: entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function pickRandomLogs(logs, maxCount) {
|
|
106
|
+
if (!Array.isArray(logs) || logs.length <= maxCount) {
|
|
107
|
+
return Array.isArray(logs) ? [...logs] : [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const shuffled = [...logs];
|
|
111
|
+
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
|
112
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
113
|
+
[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return shuffled
|
|
117
|
+
.slice(0, maxCount)
|
|
118
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function clampText(text, maxChars) {
|
|
122
|
+
const normalized = String(text || "");
|
|
123
|
+
if (normalized.length <= maxChars) {
|
|
124
|
+
return normalized;
|
|
125
|
+
}
|
|
126
|
+
return `${normalized.slice(0, maxChars)}\n...[truncated]`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildKeywordExcerpt(logContent) {
|
|
130
|
+
const lines = String(logContent || "").split(/\r?\n/);
|
|
131
|
+
const selectedIndexes = new Set();
|
|
132
|
+
|
|
133
|
+
lines.forEach((line, index) => {
|
|
134
|
+
if (!KEYWORD_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const start = Math.max(0, index - CONTEXT_RADIUS);
|
|
138
|
+
const end = Math.min(lines.length - 1, index + CONTEXT_RADIUS);
|
|
139
|
+
for (let cursor = start; cursor <= end; cursor += 1) {
|
|
140
|
+
selectedIndexes.add(cursor);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const excerpt = Array.from(selectedIndexes)
|
|
145
|
+
.sort((left, right) => left - right)
|
|
146
|
+
.map((index) => `${index + 1}: ${lines[index]}`)
|
|
147
|
+
.join("\n");
|
|
148
|
+
|
|
149
|
+
return clampText(excerpt, MAX_SECTION_CHARS);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildHeadTailExcerpt(logContent) {
|
|
153
|
+
const text = String(logContent || "");
|
|
154
|
+
if (text.length <= MAX_FALLBACK_HEAD_TAIL_CHARS) {
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
const half = Math.floor(MAX_FALLBACK_HEAD_TAIL_CHARS / 2);
|
|
158
|
+
return [
|
|
159
|
+
text.slice(0, half),
|
|
160
|
+
"\n...[middle truncated]...\n",
|
|
161
|
+
text.slice(-half),
|
|
162
|
+
].join("");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function writeFingerprintPlanInput(fingerprint, sampledLogs, context) {
|
|
166
|
+
const reportsDir = path.join(context.fingerprintsRoot, fingerprint, "reports");
|
|
167
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
168
|
+
const inputRelativePath = path.join(fingerprint, "reports", "fix-plan-input.md");
|
|
169
|
+
const inputAbsolutePath = path.join(context.fingerprintsRoot, inputRelativePath);
|
|
170
|
+
const wrappedContentParts = [
|
|
171
|
+
"# Fingerprint",
|
|
172
|
+
fingerprint,
|
|
173
|
+
"",
|
|
174
|
+
"# Sampled Logs",
|
|
175
|
+
`Count: ${sampledLogs.length}`,
|
|
176
|
+
"",
|
|
177
|
+
].join("\n");
|
|
178
|
+
let wrappedContent = wrappedContentParts;
|
|
179
|
+
|
|
180
|
+
for (const logEntry of sampledLogs) {
|
|
181
|
+
/* eslint-disable no-await-in-loop */
|
|
182
|
+
const relativeLogPath = path.join(fingerprint, "logs", logEntry.relativePath).replace(/\\/g, "/");
|
|
183
|
+
const logContent = await fs.readFile(logEntry.absolutePath, "utf8");
|
|
184
|
+
const keywordExcerpt = buildKeywordExcerpt(logContent);
|
|
185
|
+
wrappedContent += [
|
|
186
|
+
"## Log Path",
|
|
187
|
+
relativeLogPath,
|
|
188
|
+
"",
|
|
189
|
+
"## Log Size",
|
|
190
|
+
String(logEntry.size),
|
|
191
|
+
"",
|
|
192
|
+
"## Key Error Excerpt",
|
|
193
|
+
"```text",
|
|
194
|
+
keywordExcerpt || "No explicit error keywords matched in log.",
|
|
195
|
+
"```",
|
|
196
|
+
"",
|
|
197
|
+
].join("\n");
|
|
198
|
+
if (!keywordExcerpt) {
|
|
199
|
+
const headTailExcerpt = buildHeadTailExcerpt(logContent);
|
|
200
|
+
wrappedContent += [
|
|
201
|
+
"## Fallback Head Tail Excerpt",
|
|
202
|
+
"```text",
|
|
203
|
+
headTailExcerpt,
|
|
204
|
+
"```",
|
|
205
|
+
"",
|
|
206
|
+
].join("\n");
|
|
207
|
+
}
|
|
208
|
+
/* eslint-enable no-await-in-loop */
|
|
209
|
+
}
|
|
210
|
+
const trimmedContent = clampText(wrappedContent, MAX_LOG_PROMPT_CHARS);
|
|
211
|
+
const finalContent = trimmedContent.length === wrappedContent.length
|
|
212
|
+
? trimmedContent
|
|
213
|
+
: `${trimmedContent}\n\n# Note\nInput truncated to stay within Codex prompt limits.\n`;
|
|
214
|
+
await fs.writeFile(inputAbsolutePath, finalContent, "utf8");
|
|
215
|
+
return {
|
|
216
|
+
inputRelativePath,
|
|
217
|
+
relativeLogPaths: sampledLogs.map((logEntry) => path.join(fingerprint, "logs", logEntry.relativePath).replace(/\\/g, "/")),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function generateRepairPlanFromFingerprint(input, overrides = {}) {
|
|
222
|
+
const context = createRepairContext(overrides);
|
|
223
|
+
const requestedFingerprint = normalizeFingerprint(input.fingerprint);
|
|
224
|
+
if (!requestedFingerprint || requestedFingerprint === "unknown-fingerprint") {
|
|
225
|
+
throw new Error("fingerprint is required");
|
|
226
|
+
}
|
|
227
|
+
const resolved = await findFingerprintLogs(requestedFingerprint, context);
|
|
228
|
+
const fingerprint = resolved.fingerprint;
|
|
229
|
+
const sampledLogs = pickRandomLogs(resolved.logs, REPAIR_PLAN_LOG_SAMPLE_SIZE);
|
|
230
|
+
const preparedInput = await writeFingerprintPlanInput(fingerprint, sampledLogs, context);
|
|
231
|
+
const result = await generateRepairPlan({ reportPath: preparedInput.inputRelativePath }, overrides);
|
|
232
|
+
return {
|
|
233
|
+
...result,
|
|
234
|
+
fingerprint,
|
|
235
|
+
logPaths: preparedInput.relativeLogPaths,
|
|
236
|
+
logCount: sampledLogs.length,
|
|
237
|
+
logSizes: sampledLogs.map((logEntry) => logEntry.size),
|
|
238
|
+
inputReportPath: preparedInput.inputRelativePath,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function loadLatestRepairPlanFromFingerprint(input, overrides = {}) {
|
|
243
|
+
const context = createRepairContext(overrides);
|
|
244
|
+
const requestedFingerprint = normalizeFingerprint(input.fingerprint);
|
|
245
|
+
if (!requestedFingerprint || requestedFingerprint === "unknown-fingerprint") {
|
|
246
|
+
throw new Error("fingerprint is required");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const resolved = await findFingerprintLogs(requestedFingerprint, context);
|
|
250
|
+
const fingerprint = resolved.fingerprint;
|
|
251
|
+
const repairRoot = path.join(context.fingerprintsRoot, fingerprint, "repair");
|
|
252
|
+
const repairEntries = await fs.readdir(repairRoot, { withFileTypes: true }).catch(() => []);
|
|
253
|
+
const versions = repairEntries
|
|
254
|
+
.filter((entry) => entry.isDirectory())
|
|
255
|
+
.map((entry) => entry.name)
|
|
256
|
+
.sort()
|
|
257
|
+
.reverse();
|
|
258
|
+
|
|
259
|
+
for (const repairVersion of versions) {
|
|
260
|
+
/* eslint-disable no-await-in-loop */
|
|
261
|
+
const relativeJsonPath = path.join(fingerprint, "repair", repairVersion, "repair-plan.json");
|
|
262
|
+
const absoluteJsonPath = path.join(context.fingerprintsRoot, relativeJsonPath);
|
|
263
|
+
const rawText = await fs.readFile(absoluteJsonPath, "utf8").catch(() => "");
|
|
264
|
+
if (!rawText) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
let structuredPlan = null;
|
|
268
|
+
try {
|
|
269
|
+
structuredPlan = JSON.parse(rawText);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
structuredPlan = null;
|
|
272
|
+
}
|
|
273
|
+
if (!structuredPlan || typeof structuredPlan !== "object") {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
fingerprint,
|
|
278
|
+
repairVersion,
|
|
279
|
+
repairPlanJsonPath: relativeJsonPath,
|
|
280
|
+
structuredPlan,
|
|
281
|
+
};
|
|
282
|
+
/* eslint-enable no-await-in-loop */
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw new Error(`No repair-plan.json found for fingerprint: ${fingerprint}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isSupportedTextFile(filePath) {
|
|
289
|
+
return TEXT_FILE_EXTENSIONS.has(path.extname(String(filePath || "")).toLowerCase());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resolveWorkspacePath(rootDir, relativePath) {
|
|
293
|
+
const absolutePath = path.resolve(rootDir, String(relativePath || "").replace(/[\\/]+/g, path.sep));
|
|
294
|
+
if (absolutePath !== rootDir && !absolutePath.startsWith(rootDir + path.sep)) {
|
|
295
|
+
throw new Error("Invalid workspace path");
|
|
296
|
+
}
|
|
297
|
+
return absolutePath;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function backupTargetFile(fingerprint, repairVersion, relativeFilePath, absoluteFilePath, context) {
|
|
301
|
+
const backupRelativePath = path.join(
|
|
302
|
+
fingerprint,
|
|
303
|
+
"repair",
|
|
304
|
+
repairVersion,
|
|
305
|
+
"apply-backups",
|
|
306
|
+
String(relativeFilePath || "").replace(/[\\/]+/g, path.sep),
|
|
307
|
+
);
|
|
308
|
+
const backupAbsolutePath = path.join(context.fingerprintsRoot, backupRelativePath);
|
|
309
|
+
await fs.mkdir(path.dirname(backupAbsolutePath), { recursive: true });
|
|
310
|
+
await fs.copyFile(absoluteFilePath, backupAbsolutePath);
|
|
311
|
+
return backupRelativePath;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function applyReplaceBlock(content, change) {
|
|
315
|
+
const anchor = String(change.searchAnchor || "");
|
|
316
|
+
const before = String(change.before || "");
|
|
317
|
+
const after = String(change.after || "");
|
|
318
|
+
if (anchor && !content.includes(anchor)) {
|
|
319
|
+
return { ok: false, reason: "searchAnchor not found" };
|
|
320
|
+
}
|
|
321
|
+
if (!before) {
|
|
322
|
+
return { ok: false, reason: "before is required for replace_block/update_condition" };
|
|
323
|
+
}
|
|
324
|
+
if (!content.includes(before)) {
|
|
325
|
+
return { ok: false, reason: "before snippet not found" };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
ok: true,
|
|
329
|
+
content: content.replace(before, after),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function applyInsertAfter(content, change) {
|
|
334
|
+
const anchor = String(change.searchAnchor || "");
|
|
335
|
+
const after = String(change.after || "");
|
|
336
|
+
if (!anchor) {
|
|
337
|
+
return { ok: false, reason: "searchAnchor is required for insert_after" };
|
|
338
|
+
}
|
|
339
|
+
const anchorIndex = content.indexOf(anchor);
|
|
340
|
+
if (anchorIndex < 0) {
|
|
341
|
+
return { ok: false, reason: "searchAnchor not found" };
|
|
342
|
+
}
|
|
343
|
+
const insertIndex = anchorIndex + anchor.length;
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
content: `${content.slice(0, insertIndex)}\n${after}\n${content.slice(insertIndex)}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function applyPlannedChange(content, change) {
|
|
351
|
+
const action = String(change.action || "").trim();
|
|
352
|
+
if (action === "replace_block" || action === "update_condition") {
|
|
353
|
+
return applyReplaceBlock(content, change);
|
|
354
|
+
}
|
|
355
|
+
if (action === "insert_after") {
|
|
356
|
+
return applyInsertAfter(content, change);
|
|
357
|
+
}
|
|
358
|
+
return { ok: false, reason: `unsupported action: ${action || "unknown"}` };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function applyFixFromFingerprint(input, overrides = {}) {
|
|
362
|
+
const loaded = await loadLatestRepairPlanFromFingerprint(input, overrides);
|
|
363
|
+
const changes = Array.isArray(loaded.structuredPlan?.changes) ? loaded.structuredPlan.changes : [];
|
|
364
|
+
if (changes.length === 0) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
command: "apply-fix",
|
|
368
|
+
fingerprint: loaded.fingerprint,
|
|
369
|
+
repairVersion: loaded.repairVersion,
|
|
370
|
+
repairPlanJsonPath: loaded.repairPlanJsonPath,
|
|
371
|
+
actionable: false,
|
|
372
|
+
reason: "repair-plan.json has no changes; there is nothing safe to apply automatically.",
|
|
373
|
+
structuredPlan: loaded.structuredPlan,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = await applyRepairPlan({
|
|
378
|
+
reportPath: loaded.structuredPlan.reportPath,
|
|
379
|
+
planText: JSON.stringify(loaded.structuredPlan, null, 2),
|
|
380
|
+
repairVersion: loaded.repairVersion,
|
|
381
|
+
repairPlanPath: loaded.repairPlanJsonPath,
|
|
382
|
+
}, overrides);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
command: "apply-fix",
|
|
387
|
+
fingerprint: loaded.fingerprint,
|
|
388
|
+
repairVersion: loaded.repairVersion,
|
|
389
|
+
repairPlanJsonPath: loaded.repairPlanJsonPath,
|
|
390
|
+
actionable: true,
|
|
391
|
+
reason: "Codex apply-fix executed.",
|
|
392
|
+
changeCount: changes.length,
|
|
393
|
+
applyResultPath: result.repairResultPath,
|
|
394
|
+
archivedSources: result.archivedSources,
|
|
395
|
+
repairStatusPath: result.repairStatusPath,
|
|
396
|
+
resultText: result.resultText,
|
|
397
|
+
structuredPlan: loaded.structuredPlan,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
module.exports = {
|
|
402
|
+
applyFixFromFingerprint,
|
|
403
|
+
generateRepairPlanFromFingerprint,
|
|
404
|
+
loadLatestRepairPlanFromFingerprint,
|
|
405
|
+
};
|