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.
@@ -0,0 +1,495 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+ const { DEFAULT_WORKDIR, DEFAULT_PROXY, DEFAULT_STORAGE_DIR } = require("./constants");
4
+
5
+ function createRepairContext(overrides = {}) {
6
+ const rootDir = path.resolve(overrides.rootDir || process.cwd());
7
+ const fingerprintsRoot = path.resolve(overrides.fingerprintsRoot || path.join(rootDir, DEFAULT_STORAGE_DIR));
8
+ return {
9
+ rootDir,
10
+ fingerprintsRoot,
11
+ workdir: overrides.workdir || DEFAULT_WORKDIR,
12
+ proxy: resolveCodexProxy(overrides.proxy),
13
+ };
14
+ }
15
+
16
+ function formatRepairVersion(date) {
17
+ const year = String(date.getFullYear());
18
+ const month = String(date.getMonth() + 1).padStart(2, "0");
19
+ const day = String(date.getDate()).padStart(2, "0");
20
+ const hour = String(date.getHours()).padStart(2, "0");
21
+ const minute = String(date.getMinutes()).padStart(2, "0");
22
+ const second = String(date.getSeconds()).padStart(2, "0");
23
+ return `${year}${month}${day}-${hour}${minute}${second}`;
24
+ }
25
+
26
+ function toSafeAbsolute(rootDir, relativePath) {
27
+ const safeRelative = String(relativePath || "").replace(/[\\/]+/g, path.sep);
28
+ const targetPath = path.resolve(rootDir, safeRelative);
29
+ if (targetPath !== rootDir && !targetPath.startsWith(rootDir + path.sep)) {
30
+ throw new Error("Invalid path");
31
+ }
32
+ return targetPath;
33
+ }
34
+
35
+ async function pathExists(targetPath) {
36
+ try {
37
+ await fs.access(targetPath);
38
+ return true;
39
+ } catch (error) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function getFingerprintPathFromReportPath(relativeReportPath) {
45
+ const normalized = String(relativeReportPath || "").replace(/[\\/]+/g, "/").trim();
46
+ if (!normalized) {
47
+ throw new Error("Invalid report path");
48
+ }
49
+ const parts = normalized.split("/").filter(Boolean);
50
+ const reportsIndex = parts.lastIndexOf("reports");
51
+ if (reportsIndex > 0) {
52
+ return parts.slice(0, reportsIndex).join(path.sep);
53
+ }
54
+ if (parts.length >= 2) {
55
+ return parts.slice(0, -1).join(path.sep);
56
+ }
57
+ return parts[0];
58
+ }
59
+
60
+ function getRepairVersionRelativeDir(relativeReportPath, repairVersion) {
61
+ return path.join(getFingerprintPathFromReportPath(relativeReportPath), "repair", repairVersion);
62
+ }
63
+
64
+ function getRepairVersionFromRepairPlanPath(repairPlanPath) {
65
+ const normalized = String(repairPlanPath || "").replace(/[\\/]+/g, "/").trim();
66
+ if (!normalized) {
67
+ return "";
68
+ }
69
+ const parts = normalized.split("/").filter(Boolean);
70
+ const repairIndex = parts.lastIndexOf("repair");
71
+ if (repairIndex >= 0 && parts.length > repairIndex + 1) {
72
+ return parts[repairIndex + 1];
73
+ }
74
+ return "";
75
+ }
76
+
77
+ async function resolveNewRepairVersion(relativeReportPath, context) {
78
+ const baseVersion = formatRepairVersion(new Date());
79
+ const repairRootRelative = path.join(getFingerprintPathFromReportPath(relativeReportPath), "repair");
80
+ const repairRootAbsolute = toSafeAbsolute(context.fingerprintsRoot, repairRootRelative);
81
+ let suffix = 0;
82
+ while (true) {
83
+ const repairVersion = suffix === 0 ? baseVersion : `${baseVersion}-${String(suffix).padStart(2, "0")}`;
84
+ const candidateDir = path.join(repairRootAbsolute, repairVersion);
85
+ if (!(await pathExists(candidateDir))) {
86
+ return repairVersion;
87
+ }
88
+ suffix += 1;
89
+ }
90
+ }
91
+
92
+ async function resolveRepairVersionForApply(relativeReportPath, repairVersion, repairPlanPath, context) {
93
+ const normalized = String(repairVersion || "").trim();
94
+ if (normalized) {
95
+ return normalized;
96
+ }
97
+ const planVersion = getRepairVersionFromRepairPlanPath(repairPlanPath);
98
+ if (planVersion) {
99
+ return planVersion;
100
+ }
101
+ const repairRootRelative = path.join(getFingerprintPathFromReportPath(relativeReportPath), "repair");
102
+ const repairRootAbsolute = toSafeAbsolute(context.fingerprintsRoot, repairRootRelative);
103
+ const entries = await fs.readdir(repairRootAbsolute, { withFileTypes: true }).catch(() => []);
104
+ const versions = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().reverse();
105
+ if (versions.length > 0) {
106
+ return versions[0];
107
+ }
108
+ return formatRepairVersion(new Date());
109
+ }
110
+
111
+ async function ensureRepairVersionDir(relativeReportPath, repairVersion, context) {
112
+ const relativeDir = getRepairVersionRelativeDir(relativeReportPath, repairVersion);
113
+ const absoluteDir = toSafeAbsolute(context.fingerprintsRoot, relativeDir);
114
+ await fs.mkdir(absoluteDir, { recursive: true });
115
+ return { relativeDir, absoluteDir };
116
+ }
117
+
118
+ async function writeRepairArtifact(relativeReportPath, repairVersion, fileName, text, context) {
119
+ const versionDir = await ensureRepairVersionDir(relativeReportPath, repairVersion, context);
120
+ const relativePath = path.join(versionDir.relativeDir, fileName);
121
+ const absolutePath = path.join(versionDir.absoluteDir, fileName);
122
+ await fs.writeFile(absolutePath, String(text || ""), "utf8");
123
+ return { repairVersion, relativeDir: versionDir.relativeDir, relativePath, absolutePath };
124
+ }
125
+
126
+ function collectModifiedSourceCandidates(text) {
127
+ const matches = String(text || "").match(/(?:[A-Za-z]:)?[A-Za-z0-9_./\\-]+\.(?:js|jsx|ts|tsx|json|css|scss|less|html|vue|java|kt|go|py|rb|php|cs|cpp|c|h|hpp|m|mm|swift|xml|yml|yaml)/g) || [];
128
+ return Array.from(new Set(matches.map((item) => String(item || "").replace(/\\/g, "/").replace(/^\.\//, "").trim()).filter(Boolean)));
129
+ }
130
+
131
+ async function archiveModifiedSources(repairVersionRelativeDir, text, context) {
132
+ const candidates = collectModifiedSourceCandidates(text);
133
+ const archived = [];
134
+ for (const candidate of candidates) {
135
+ /* eslint-disable no-await-in-loop */
136
+ const workspaceAbsolute = path.resolve(context.rootDir, candidate);
137
+ if (workspaceAbsolute !== context.rootDir && !workspaceAbsolute.startsWith(context.rootDir + path.sep)) {
138
+ continue;
139
+ }
140
+ const stats = await fs.stat(workspaceAbsolute).catch(() => null);
141
+ if (!stats || !stats.isFile()) {
142
+ continue;
143
+ }
144
+ const sourceRelativePath = path.relative(context.rootDir, workspaceAbsolute);
145
+ if (!sourceRelativePath || sourceRelativePath.startsWith("fingerprints" + path.sep)) {
146
+ continue;
147
+ }
148
+ const targetRelativePath = path.join(repairVersionRelativeDir, "sources", sourceRelativePath);
149
+ const targetAbsolutePath = toSafeAbsolute(context.fingerprintsRoot, targetRelativePath);
150
+ await fs.mkdir(path.dirname(targetAbsolutePath), { recursive: true });
151
+ await fs.copyFile(workspaceAbsolute, targetAbsolutePath);
152
+ archived.push(targetRelativePath);
153
+ /* eslint-enable no-await-in-loop */
154
+ }
155
+ return archived;
156
+ }
157
+
158
+ async function writeRepairStatus(relativeReportPath, payload, context) {
159
+ const reportsRelativeDir = path.dirname(relativeReportPath);
160
+ const statusPath = toSafeAbsolute(context.fingerprintsRoot, path.join(reportsRelativeDir, ".repair-status.json"));
161
+ await fs.writeFile(statusPath, JSON.stringify(payload, null, 2), "utf8");
162
+ return path.relative(context.fingerprintsRoot, statusPath);
163
+ }
164
+
165
+ async function readFilePreview(rootDir, relativePath) {
166
+ const targetPath = toSafeAbsolute(rootDir, relativePath);
167
+ const content = await fs.readFile(targetPath, "utf8");
168
+ return {
169
+ path: relativePath,
170
+ absolutePath: targetPath,
171
+ content,
172
+ };
173
+ }
174
+
175
+ async function getCodex(proxyUrl) {
176
+ const mod = await import("@openai/codex-sdk");
177
+ const env = Object.assign({}, process.env, {
178
+ HTTP_PROXY: proxyUrl,
179
+ HTTPS_PROXY: proxyUrl,
180
+ ALL_PROXY: proxyUrl,
181
+ });
182
+ return new mod.Codex({ env });
183
+ }
184
+
185
+ function resolveCodexProxy(proxyValue) {
186
+ return String(proxyValue || process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.ALL_PROXY || DEFAULT_PROXY || "").trim();
187
+ }
188
+
189
+ async function withHeartbeat(scope, stage, context, task) {
190
+ return task();
191
+ }
192
+
193
+ function truncateConsoleText(value, maxLength) {
194
+ const text = String(value || "").replace(/\r\n/g, "\n");
195
+ if (text.length <= maxLength) {
196
+ return text;
197
+ }
198
+ return `${text.slice(0, maxLength)}\n...[truncated]`;
199
+ }
200
+
201
+ function extractCodexEventText(event) {
202
+ if (!event || typeof event !== "object") {
203
+ return "";
204
+ }
205
+ if (typeof event.text === "string" && event.text.trim()) {
206
+ return event.text;
207
+ }
208
+ if (typeof event.delta === "string" && event.delta.trim()) {
209
+ return event.delta;
210
+ }
211
+ if (event.item && typeof event.item.text === "string" && event.item.text.trim()) {
212
+ return event.item.text;
213
+ }
214
+ if (event.item && typeof event.item.delta === "string" && event.item.delta.trim()) {
215
+ return event.item.delta;
216
+ }
217
+ if (typeof event.message === "string" && event.message.trim()) {
218
+ return event.message;
219
+ }
220
+ return "";
221
+ }
222
+
223
+ function normalizePlanString(value) {
224
+ return String(value || "").replace(/\r\n/g, "\n").trim();
225
+ }
226
+
227
+ function extractJsonCodeFence(text) {
228
+ const normalized = normalizePlanString(text);
229
+ const fencedMatch = normalized.match(/```json\s*([\s\S]*?)```/i);
230
+ if (fencedMatch && fencedMatch[1]) {
231
+ return fencedMatch[1].trim();
232
+ }
233
+ return "";
234
+ }
235
+
236
+ function buildFallbackStructuredPlan(rawText, parseError, reportPath) {
237
+ return {
238
+ schemaVersion: 1,
239
+ reportPath,
240
+ summary: "",
241
+ rootCause: "",
242
+ changes: [],
243
+ risks: [],
244
+ verification: [],
245
+ notes: [],
246
+ parseError,
247
+ rawText: normalizePlanString(rawText),
248
+ };
249
+ }
250
+
251
+ function extractStructuredRepairPlan(rawText, reportPath) {
252
+ const normalized = normalizePlanString(rawText);
253
+ const jsonText = extractJsonCodeFence(normalized) || normalized;
254
+ try {
255
+ const parsed = JSON.parse(jsonText);
256
+ return {
257
+ schemaVersion: Number(parsed.schemaVersion || 1),
258
+ reportPath: parsed.reportPath || reportPath,
259
+ summary: String(parsed.summary || ""),
260
+ rootCause: String(parsed.rootCause || ""),
261
+ changes: Array.isArray(parsed.changes) ? parsed.changes : [],
262
+ risks: Array.isArray(parsed.risks) ? parsed.risks : [],
263
+ verification: Array.isArray(parsed.verification) ? parsed.verification : [],
264
+ notes: Array.isArray(parsed.notes) ? parsed.notes : [],
265
+ };
266
+ } catch (error) {
267
+ return buildFallbackStructuredPlan(
268
+ rawText,
269
+ error instanceof Error ? error.message : String(error),
270
+ reportPath,
271
+ );
272
+ }
273
+ }
274
+
275
+ function logCodexEvent(scope, stage, context, event) {
276
+ const text = extractCodexEventText(event);
277
+ if (text) {
278
+ console.log(truncateConsoleText(text, 4000));
279
+ }
280
+ }
281
+
282
+ async function runCodexTurnWithStreaming(thread, prompt, scope, stage, context) {
283
+ let lastEventAt = Date.now();
284
+ let finalResponse = "";
285
+ let usage = null;
286
+ let waitingLogged = false;
287
+ const timer = setInterval(() => {
288
+ if (!waitingLogged && Date.now() - lastEventAt >= 15000) {
289
+ waitingLogged = true;
290
+ console.log(`${scope} ${stage} waiting`);
291
+ }
292
+ }, 5000);
293
+ if (typeof timer.unref === "function") {
294
+ timer.unref();
295
+ }
296
+ try {
297
+ const stream = await thread.runStreamed(prompt);
298
+ for await (const event of stream.events) {
299
+ lastEventAt = Date.now();
300
+ waitingLogged = false;
301
+ logCodexEvent(scope, stage, context, event);
302
+ if (event.type === "item.completed" && event.item && event.item.type === "agent_message") {
303
+ finalResponse = event.item.text || finalResponse;
304
+ } else if (event.type === "turn.completed") {
305
+ usage = event.usage || null;
306
+ } else if (event.type === "turn.failed") {
307
+ throw new Error(event.error && event.error.message ? event.error.message : "Codex turn failed");
308
+ }
309
+ }
310
+ return {
311
+ finalResponse,
312
+ usage,
313
+ };
314
+ } finally {
315
+ clearInterval(timer);
316
+ }
317
+ }
318
+
319
+ async function runRepairPlan(reportText, reportPath, options) {
320
+ const proxy = resolveCodexProxy(options.proxy);
321
+ const codex = await withHeartbeat("[backtrace-repair][generate]", "create-client", { reportPath, proxy }, () => getCodex(proxy));
322
+ const thread = codex.startThread({
323
+ workingDirectory: options.workdir,
324
+ skipGitRepoCheck: true,
325
+ sandboxMode: "read-only",
326
+ approvalPolicy: "never",
327
+ modelReasoningEffort: "high",
328
+ });
329
+ const prompt = [
330
+ "你是一名资深工程师。当前只输出修复方案,不执行任何代码修改。",
331
+ "请先输出一个严格可解析的 JSON 代码块,然后再输出一份人类可读的 Markdown 修复说明。",
332
+ "重要要求:JSON 的键名保持英文不变,但 JSON 的值内容和后续 Markdown 说明必须全部使用中文。",
333
+ "第一部分必须是一个合法的 JSON 代码块,结构如下:",
334
+ "{",
335
+ ' "schemaVersion": 1,',
336
+ ' "reportPath": "string",',
337
+ ' "summary": "一句话总结",',
338
+ ' "rootCause": "根因判断",',
339
+ ' "changes": [',
340
+ " {",
341
+ ' "file": "工作区相对路径",',
342
+ ' "searchAnchor": "稳定定位锚点文本",',
343
+ ' "lineHint": 0,',
344
+ ' "action": "replace_block | insert_before | insert_after | update_condition",',
345
+ ' "before": "尽量小的修改前代码片段",',
346
+ ' "after": "尽量小的修改后代码片段",',
347
+ ' "reason": "为什么这样修改",',
348
+ ' "risk": "该修改的风险",',
349
+ ' "verify": ["验证步骤1", "验证步骤2"]',
350
+ " }",
351
+ " ],",
352
+ ' "risks": ["整体风险"],',
353
+ ' "verification": ["整体验证步骤"],',
354
+ ' "notes": ["补充说明"]',
355
+ "}",
356
+ "Rules:",
357
+ "1. The JSON must be valid and parseable.",
358
+ "2. Prefer to output non-empty changes whenever there is any plausible actionable fix.",
359
+ "3. If you cannot identify an exact file, still output candidate changes using the most likely file type or location, such as Config/DefaultEngine.ini, a Blueprint default, construction logic, runtime guard code, or a C++ validation path.",
360
+ "4. Only return an empty changes array when you truly cannot infer any file type, any protective code change, or any temporary mitigation.",
361
+ "5. For common guard scenarios such as zero scale, invalid transform, null pointer, or bounds validation, you must provide at least one candidate protective change and explain uncertainty in risk.",
362
+ "报告路径: " + reportPath,
363
+ "",
364
+ reportText,
365
+ ].join("\n");
366
+ const turn = await runCodexTurnWithStreaming(thread, prompt, "[backtrace-repair][generate]", "run-prompt", { reportPath, threadId: thread.id });
367
+ return {
368
+ threadId: thread.id,
369
+ planText: turn.finalResponse || "Codex 未返回修复方案。",
370
+ };
371
+ }
372
+
373
+ async function runApplyRepairPlan(reportText, planText, reportPath, options) {
374
+ const proxy = resolveCodexProxy(options.proxy);
375
+ const codex = await withHeartbeat("[backtrace-repair][apply]", "create-client", { reportPath, proxy }, () => getCodex(proxy));
376
+ const thread = codex.startThread({
377
+ workingDirectory: options.workdir,
378
+ skipGitRepoCheck: true,
379
+ sandboxMode: "workspace-write",
380
+ approvalPolicy: "never",
381
+ modelReasoningEffort: "high",
382
+ });
383
+ const prompt = [
384
+ "你是一名资深工程师,现在根据下面已经确认的修复方案,直接在工作目录中实施代码修改。",
385
+ "请实际修改文件,不要只给建议。",
386
+ "修改完成后,说明你改了哪些文件、每个文件做了什么修改,以及你进行了哪些验证。",
387
+ "最后请单独列出所有实际修改过的源码文件路径,每行一个。",
388
+ "",
389
+ `分析报告路径: ${reportPath}`,
390
+ "",
391
+ "原始分析报告:",
392
+ reportText,
393
+ "",
394
+ "已确认的修复方案:",
395
+ planText,
396
+ ].join("\n");
397
+ const turn = await runCodexTurnWithStreaming(thread, prompt, "[backtrace-repair][apply]", "run-prompt", { reportPath, threadId: thread.id });
398
+ return {
399
+ threadId: thread.id,
400
+ resultText: turn.finalResponse || "Codex 未返回应用结果。",
401
+ };
402
+ }
403
+
404
+ async function generateRepairPlan(input, overrides = {}) {
405
+ const context = createRepairContext(overrides);
406
+ const reportPath = String(input.reportPath || "").trim();
407
+ if (!reportPath) {
408
+ throw new Error("reportPath is required");
409
+ }
410
+ const repairVersion = await resolveNewRepairVersion(reportPath, context);
411
+ const file = await readFilePreview(context.fingerprintsRoot, reportPath);
412
+ const result = await runRepairPlan(file.content, reportPath, {
413
+ workdir: overrides.workdir || context.workdir,
414
+ proxy: overrides.proxy || context.proxy,
415
+ });
416
+ const structuredPlan = extractStructuredRepairPlan(result.planText, reportPath);
417
+ const repairPlanFile = await writeRepairArtifact(reportPath, repairVersion, "repair-plan.md", result.planText, context);
418
+ const repairPlanJsonFile = await writeRepairArtifact(
419
+ reportPath,
420
+ repairVersion,
421
+ "repair-plan.json",
422
+ JSON.stringify(structuredPlan, null, 2),
423
+ context,
424
+ );
425
+ return {
426
+ ok: true,
427
+ reportPath,
428
+ plan: result.planText,
429
+ structuredPlan,
430
+ threadId: result.threadId,
431
+ repairVersion,
432
+ repairPlanPath: repairPlanFile.relativePath,
433
+ repairPlanJsonPath: repairPlanJsonFile.relativePath,
434
+ };
435
+ }
436
+
437
+ async function applyRepairPlan(input, overrides = {}) {
438
+ const context = createRepairContext(overrides);
439
+ const reportPath = String(input.reportPath || "").trim();
440
+ const planText = String(input.planText || "").trim();
441
+ if (!reportPath || !planText) {
442
+ throw new Error("reportPath and planText are required");
443
+ }
444
+ const repairVersion = await resolveRepairVersionForApply(reportPath, input.repairVersion, input.repairPlanPath, context);
445
+ const file = await readFilePreview(context.fingerprintsRoot, reportPath);
446
+ const result = await runApplyRepairPlan(file.content, planText, reportPath, {
447
+ workdir: overrides.workdir || context.workdir,
448
+ proxy: overrides.proxy || context.proxy,
449
+ });
450
+ const repairResultFile = await writeRepairArtifact(reportPath, repairVersion, "apply-result.md", result.resultText, context);
451
+ const archivedSources = await archiveModifiedSources(path.dirname(repairResultFile.relativePath), `${result.resultText}\n${planText}`, context);
452
+ const repairStatusPath = await writeRepairStatus(reportPath, {
453
+ completed: true,
454
+ appliedAt: new Date().toISOString(),
455
+ reportPath,
456
+ repairVersion,
457
+ repairResultPath: repairResultFile.relativePath,
458
+ archivedSources,
459
+ threadId: result.threadId,
460
+ resultText: result.resultText,
461
+ }, context);
462
+ return {
463
+ ok: true,
464
+ reportPath,
465
+ resultText: result.resultText,
466
+ threadId: result.threadId,
467
+ repairStatusPath,
468
+ repairVersion,
469
+ repairResultPath: repairResultFile.relativePath,
470
+ archivedSources,
471
+ status: "已完成",
472
+ };
473
+ }
474
+
475
+ async function readRepairPlanText(input, overrides = {}) {
476
+ const context = createRepairContext(overrides);
477
+ if (input.planText) {
478
+ return String(input.planText);
479
+ }
480
+ const repairPlanPath = String(input.repairPlanPath || "").trim();
481
+ if (!repairPlanPath) {
482
+ throw new Error("planText or repairPlanPath is required");
483
+ }
484
+ const file = await readFilePreview(context.fingerprintsRoot, repairPlanPath);
485
+ return file.content;
486
+ }
487
+
488
+ module.exports = {
489
+ createRepairContext,
490
+ formatRepairVersion,
491
+ resolveCodexProxy,
492
+ generateRepairPlan,
493
+ applyRepairPlan,
494
+ readRepairPlanText,
495
+ };