opencode-metrics-plugin 0.1.5

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,550 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { getSummaryFile } from "./dirs.js";
5
+ import { upsertSessionSnapshot } from "./summary-store.js";
6
+ // ─── 工具函数 ────────────────────────────────────────────────────────────────
7
+ // 状态文件不带 .json 扩展名,避免被会话快照扫描器(session-viewer 等)误读
8
+ const STATE_FILE = ".backfill-state";
9
+ function zeroTokens() {
10
+ return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
11
+ }
12
+ function addTokens(a, b) {
13
+ return {
14
+ input: a.input + b.input,
15
+ output: a.output + b.output,
16
+ reasoning: a.reasoning + b.reasoning,
17
+ cacheRead: a.cacheRead + b.cacheRead,
18
+ cacheWrite: a.cacheWrite + b.cacheWrite,
19
+ total: a.total + b.total,
20
+ };
21
+ }
22
+ /** message.data.tokens 结构 {input,output,reasoning,cache:{read,write}} → 扁平 TokenUsage */
23
+ function msgTokens(raw) {
24
+ const t = (raw ?? {});
25
+ const cache = (t.cache ?? {});
26
+ const n = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
27
+ const usage = {
28
+ input: n(t.input), output: n(t.output), reasoning: n(t.reasoning),
29
+ cacheRead: n(cache.read), cacheWrite: n(cache.write), total: 0,
30
+ };
31
+ usage.total = usage.input + usage.output + usage.reasoning;
32
+ return usage;
33
+ }
34
+ function sessionTokens(row) {
35
+ const n = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
36
+ const usage = {
37
+ input: n(row.tokens_input), output: n(row.tokens_output), reasoning: n(row.tokens_reasoning),
38
+ cacheRead: n(row.tokens_cache_read), cacheWrite: n(row.tokens_cache_write), total: 0,
39
+ };
40
+ usage.total = usage.input + usage.output + usage.reasoning;
41
+ return usage;
42
+ }
43
+ /** 目录前缀匹配(统一正斜杠 + 大小写不敏感,兼容 Windows 反斜杠配置) */
44
+ function dirMatches(directory, prefixes) {
45
+ if (!directory || !prefixes || prefixes.length === 0)
46
+ return false;
47
+ const dir = directory.replace(/\\/g, "/").toLowerCase();
48
+ return prefixes.some((p) => dir.startsWith(p.replace(/\\/g, "/").toLowerCase()));
49
+ }
50
+ function parseModelDisplay(modelJson) {
51
+ if (!modelJson)
52
+ return "";
53
+ try {
54
+ const m = JSON.parse(modelJson);
55
+ // 只存模型名(如 deepseek-v4-flash),不带 provider 前缀(与 live 的 model.id 口径一致)
56
+ return m.id ?? modelJson;
57
+ }
58
+ catch {
59
+ return modelJson;
60
+ }
61
+ }
62
+ function readWatermark(metricsDir) {
63
+ try {
64
+ const raw = fs.readFileSync(path.join(metricsDir, STATE_FILE), "utf-8");
65
+ const v = JSON.parse(raw).watermark;
66
+ return typeof v === "number" ? v : 0;
67
+ }
68
+ catch {
69
+ return 0;
70
+ }
71
+ }
72
+ function writeWatermark(metricsDir, watermark) {
73
+ try {
74
+ fs.mkdirSync(metricsDir, { recursive: true });
75
+ fs.writeFileSync(path.join(metricsDir, STATE_FILE), JSON.stringify({ watermark }, null, 2));
76
+ }
77
+ catch { }
78
+ }
79
+ /** 从 message/part 行重建单会话(或子会话)的 steps/skills/工具统计 */
80
+ function buildFromRows(messages, parts) {
81
+ const partsByMsg = new Map();
82
+ for (const p of parts) {
83
+ if (!p.message_id)
84
+ continue;
85
+ const list = partsByMsg.get(p.message_id);
86
+ if (list)
87
+ list.push(p);
88
+ else
89
+ partsByMsg.set(p.message_id, [p]);
90
+ }
91
+ const steps = [];
92
+ const skills = [];
93
+ let responseLength = 0;
94
+ // 内部累加器(含 totalDuration),输出时换算为 avgDuration
95
+ const accDistribution = new Map();
96
+ let totalCalls = 0;
97
+ let totalErrors = 0;
98
+ let slowest = null;
99
+ for (const msg of messages) {
100
+ let md;
101
+ try {
102
+ md = JSON.parse(msg.data);
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ const role = md.role;
108
+ if (role === "assistant") {
109
+ const msgParts = (partsByMsg.get(msg.id) ?? []);
110
+ let text = "";
111
+ let reasoning = "";
112
+ const tools = [];
113
+ for (const p of msgParts) {
114
+ let pd;
115
+ try {
116
+ pd = JSON.parse(p.data);
117
+ }
118
+ catch {
119
+ continue;
120
+ }
121
+ const state = (pd.state ?? {});
122
+ const status = state.status;
123
+ if (pd.type === "text") {
124
+ text += pd.text ?? "";
125
+ }
126
+ else if (pd.type === "reasoning") {
127
+ reasoning += pd.text ?? "";
128
+ }
129
+ else if (pd.type === "tool" && status && status !== "running") {
130
+ const toolName = pd.tool ?? "unknown";
131
+ const time = (state.time ?? {});
132
+ const start = typeof time.start === "number" ? time.start : p.time_created;
133
+ const end = typeof time.end === "number" ? time.end : p.time_updated;
134
+ const durationMs = end > start ? end - start : 0;
135
+ const output = state.output ?? state.raw ?? "";
136
+ tools.push({
137
+ tool: toolName,
138
+ callID: pd.callID ?? p.id,
139
+ status: status === "error" ? "error" : "completed",
140
+ input: (state.input ?? pd.input ?? {}),
141
+ output,
142
+ durationMs,
143
+ });
144
+ // 工具分布统计
145
+ const stat = accDistribution.get(toolName) ?? { calls: 0, errors: 0, totalDuration: 0, maxDuration: 0 };
146
+ stat.calls++;
147
+ if (status === "error") {
148
+ stat.errors++;
149
+ totalErrors++;
150
+ }
151
+ stat.totalDuration += durationMs;
152
+ stat.maxDuration = Math.max(stat.maxDuration, durationMs);
153
+ accDistribution.set(toolName, stat);
154
+ totalCalls++;
155
+ if (!slowest || durationMs > slowest.duration)
156
+ slowest = { tool: toolName, duration: durationMs };
157
+ // skill 调用(与运行时 collectSkills 同口径:tool==='skill',名称取 state.input.name)
158
+ if (toolName === "skill") {
159
+ const input = (state.input ?? {});
160
+ skills.push({
161
+ skillName: input.name ?? "skill",
162
+ status: status === "error" ? "failed" : "completed",
163
+ });
164
+ }
165
+ }
166
+ }
167
+ const tokens = msgTokens(md.tokens);
168
+ const hasContent = tokens.total > 0 || tools.length > 0 || text.length > 0;
169
+ if (hasContent) {
170
+ responseLength += text.length;
171
+ steps.push({
172
+ index: steps.length,
173
+ startTime: msg.time_created,
174
+ endTime: msg.time_updated > msg.time_created ? msg.time_updated : msg.time_created,
175
+ durationMs: msg.time_updated > msg.time_created ? msg.time_updated - msg.time_created : 0,
176
+ tokens,
177
+ cost: typeof md.cost === "number" ? md.cost : 0,
178
+ tools,
179
+ textLength: text.length,
180
+ text,
181
+ reasoning,
182
+ });
183
+ }
184
+ }
185
+ }
186
+ // 汇总分布(avg 由 totalDuration 换算)
187
+ const dist = {};
188
+ for (const [tool, s] of accDistribution) {
189
+ dist[tool] = {
190
+ calls: s.calls, errors: s.errors,
191
+ avgDuration: s.calls > 0 ? s.totalDuration / s.calls : 0,
192
+ maxDuration: s.maxDuration,
193
+ };
194
+ }
195
+ return {
196
+ steps,
197
+ skills,
198
+ responseLength,
199
+ toolStats: {
200
+ totalCalls,
201
+ invalidCalls: 0,
202
+ successRate: totalCalls > 0 ? (totalCalls - totalErrors) / totalCalls : 0,
203
+ distribution: dist,
204
+ slowestCall: slowest,
205
+ },
206
+ };
207
+ }
208
+ function emptyPlanning() {
209
+ return {
210
+ planningRounds: 0, totalPlanningDuration: 0, avgPlanningDuration: 0,
211
+ totalTasks: 0, completedTasks: 0, inProgressTasks: 0, pendingTasks: 0,
212
+ completionRate: 0, priorityDistribution: {}, calls: [],
213
+ };
214
+ }
215
+ /** 从消息时间线按真实对话轮次生成 rounds:user 消息开轮,其后连续 assistant 消息归属该轮 */
216
+ function roundsFromTimeline(messages, parts) {
217
+ const partsByMsg = new Map();
218
+ for (const p of parts) {
219
+ if (!p.message_id)
220
+ continue;
221
+ const list = partsByMsg.get(p.message_id);
222
+ if (list)
223
+ list.push(p);
224
+ else
225
+ partsByMsg.set(p.message_id, [p]);
226
+ }
227
+ const zeroTokens = () => ({ input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 });
228
+ const rounds = [];
229
+ let current = null;
230
+ const pushRound = () => {
231
+ // 与 live 同口径:空轮(无 tokens 且无工具调用)不生成快照
232
+ if (current && (current.tokens.total > 0 || current.toolCalls > 0))
233
+ rounds.push(current);
234
+ current = null;
235
+ };
236
+ const ordered = [...messages].sort((a, b) => a.time_created - b.time_created);
237
+ for (const msg of ordered) {
238
+ let md;
239
+ try {
240
+ md = JSON.parse(msg.data);
241
+ }
242
+ catch {
243
+ continue;
244
+ }
245
+ const role = md.role;
246
+ if (role === "user") {
247
+ pushRound();
248
+ const texts = [];
249
+ for (const p of partsByMsg.get(msg.id) ?? []) {
250
+ try {
251
+ const pd = JSON.parse(p.data);
252
+ if (pd.type === "text" && typeof pd.text === "string" && pd.text.length > 0)
253
+ texts.push(pd.text);
254
+ }
255
+ catch { /* 跳过坏行 */ }
256
+ }
257
+ current = {
258
+ userMessage: texts.length > 0 ? texts : undefined,
259
+ roundStart: msg.time_created,
260
+ firstTextAt: 0,
261
+ tokens: zeroTokens(),
262
+ toolCalls: 0,
263
+ errors: 0,
264
+ duration: 0,
265
+ };
266
+ continue;
267
+ }
268
+ if (role !== "assistant")
269
+ continue;
270
+ if (!current) {
271
+ current = { userMessage: undefined, roundStart: msg.time_created, firstTextAt: 0, tokens: zeroTokens(), toolCalls: 0, errors: 0, duration: 0 };
272
+ }
273
+ const t = msgTokens(md.tokens);
274
+ current.tokens.input += t.input;
275
+ current.tokens.output += t.output;
276
+ current.tokens.reasoning += t.reasoning;
277
+ current.tokens.cacheRead += t.cacheRead;
278
+ current.tokens.cacheWrite += t.cacheWrite;
279
+ current.tokens.total += t.total;
280
+ for (const p of partsByMsg.get(msg.id) ?? []) {
281
+ try {
282
+ const pd = JSON.parse(p.data);
283
+ if (pd.type === "text") {
284
+ if (!current.firstTextAt)
285
+ current.firstTextAt = p.time_created;
286
+ }
287
+ else if (pd.type === "tool") {
288
+ const status = (pd.state ?? {}).status;
289
+ if (status && status !== "running") {
290
+ current.toolCalls++;
291
+ if (status === "error")
292
+ current.errors++;
293
+ }
294
+ }
295
+ }
296
+ catch { /* 跳过坏行 */ }
297
+ }
298
+ current.duration += msg.time_updated > msg.time_created ? msg.time_updated - msg.time_created : 0;
299
+ }
300
+ pushRound();
301
+ return rounds.map((r, i) => ({
302
+ roundIndex: i,
303
+ duration: r.duration,
304
+ firstTokenLatency: r.firstTextAt > r.roundStart ? r.firstTextAt - r.roundStart : 0,
305
+ tokens: { ...r.tokens },
306
+ toolCalls: r.toolCalls,
307
+ errors: r.errors,
308
+ userMessage: r.userMessage,
309
+ }));
310
+ }
311
+ // ─── 主流程 ──────────────────────────────────────────────────────────────────
312
+ /** 从 opencode.db 回填历史会话统计:按归属规则路由写入各引擎目录(快照带 source:"backfill")。 */
313
+ export async function backfillFromOpencode(opts) {
314
+ const dbPath = opts.dbPath ?? path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
315
+ const skipExisting = opts.skipExisting !== false;
316
+ const { DatabaseSync } = await import("node:sqlite");
317
+ const db = new DatabaseSync(dbPath, { readOnly: true });
318
+ try {
319
+ const sessions = db.prepare("SELECT id,parent_id,directory,title,agent,model,cost,tokens_input,tokens_output,tokens_reasoning,tokens_cache_read,tokens_cache_write,time_created,time_updated,time_compacting FROM session ORDER BY time_created").all();
320
+ const childrenByParent = new Map();
321
+ for (const s of sessions) {
322
+ if (s.parent_id) {
323
+ const list = childrenByParent.get(s.parent_id);
324
+ if (list)
325
+ list.push(s);
326
+ else
327
+ childrenByParent.set(s.parent_id, [s]);
328
+ }
329
+ }
330
+ const msgCountStmt = db.prepare("SELECT COUNT(*) AS n FROM message WHERE session_id = ?");
331
+ const agentStmt = db.prepare("SELECT data FROM message WHERE session_id = ? ORDER BY time_created");
332
+ const msgStmt = db.prepare("SELECT id,time_created,time_updated,data FROM message WHERE session_id = ? ORDER BY time_created");
333
+ const partStmt = db.prepare("SELECT id,message_id,time_created,time_updated,data FROM part WHERE session_id = ? ORDER BY time_created");
334
+ /** agent 归属链:session.agent → 首条含 agent 的 message */
335
+ function resolveAgent(row) {
336
+ if (row.agent)
337
+ return row.agent;
338
+ try {
339
+ for (const m of agentStmt.all(row.id)) {
340
+ try {
341
+ const d = JSON.parse(m.data);
342
+ if (typeof d.agent === "string" && d.agent)
343
+ return d.agent;
344
+ }
345
+ catch { }
346
+ }
347
+ }
348
+ catch { }
349
+ return null;
350
+ }
351
+ function attribute(row, agent) {
352
+ for (const e of opts.engines) {
353
+ if (e.agents && agent && e.agents.includes(agent))
354
+ return e;
355
+ if (dirMatches(row.directory, e.cwdPrefixes))
356
+ return e;
357
+ }
358
+ return opts.fallback ? "fallback" : null;
359
+ }
360
+ const stats = new Map();
361
+ const statOf = (label, dir) => {
362
+ let s = stats.get(label + "\u0000" + dir);
363
+ if (!s) {
364
+ s = { label, metricsDir: dir, written: 0, skippedExisting: 0, skippedEmpty: 0 };
365
+ stats.set(label + "\u0000" + dir, s);
366
+ }
367
+ return s;
368
+ };
369
+ for (const e of opts.engines)
370
+ statOf(e.label, e.metricsDir);
371
+ if (opts.fallback)
372
+ statOf("fallback", opts.fallback.metricsDir);
373
+ const watermarks = new Map();
374
+ const newWatermarks = new Map();
375
+ const wmOf = (dir) => {
376
+ let w = watermarks.get(dir);
377
+ if (w === undefined) {
378
+ w = opts.incremental ? readWatermark(dir) : 0;
379
+ watermarks.set(dir, w);
380
+ }
381
+ return w;
382
+ };
383
+ let skippedByFilter = 0;
384
+ let unattributed = 0;
385
+ let total = 0;
386
+ for (const row of sessions) {
387
+ if (row.parent_id !== null)
388
+ continue; // 子会话并入父快照
389
+ total++;
390
+ const f = opts.filter;
391
+ if (f) {
392
+ if (f.sessionIds && !f.sessionIds.includes(row.id)) {
393
+ skippedByFilter++;
394
+ continue;
395
+ }
396
+ if (typeof f.since === "number" && row.time_created <= f.since) {
397
+ skippedByFilter++;
398
+ continue;
399
+ }
400
+ }
401
+ // cwdPrefixes 前置(纯字符串比较零开销):不匹配行先跳过,避免为大批
402
+ // 目录外会话支付 resolveAgent 的全消息扫描代价(agent 留到 attribute 前解析)
403
+ if (f?.cwdPrefixes && f.cwdPrefixes.length > 0 && !dirMatches(row.directory, f.cwdPrefixes)) {
404
+ skippedByFilter++;
405
+ continue;
406
+ }
407
+ const agent = resolveAgent(row);
408
+ if (f?.agents && f.agents.length > 0 && (!agent || !f.agents.includes(agent))) {
409
+ skippedByFilter++;
410
+ continue;
411
+ }
412
+ // 空会话(父+子均无消息)无回填价值
413
+ const parentMsgs = msgCountStmt.get(row.id).n;
414
+ const children = childrenByParent.get(row.id) ?? [];
415
+ const childMsgs = children.reduce((sum, c) => sum + msgCountStmt.get(c.id).n, 0);
416
+ const target = attribute(row, agent);
417
+ if (!target) {
418
+ unattributed++;
419
+ continue;
420
+ }
421
+ const label = target === "fallback" ? "fallback" : target.label;
422
+ const metricsDir = target === "fallback" ? opts.fallback.metricsDir : target.metricsDir;
423
+ // 摘要库落点:rule/fallback 显式指定,否则全局共享库(与 live flush 同库,多宿主聚合)
424
+ const summaryFile = (target === "fallback" ? opts.fallback.summaryFile : target.summaryFile) ?? getSummaryFile();
425
+ const stat = statOf(label, metricsDir);
426
+ if (parentMsgs === 0 && childMsgs === 0) {
427
+ stat.skippedEmpty++;
428
+ continue;
429
+ }
430
+ // 增量水位(按目标目录)
431
+ if (opts.incremental && row.time_created <= wmOf(metricsDir))
432
+ continue;
433
+ // live 快照优先,不覆盖;force 全量重跑时仅保护 source:"live"
434
+ //(backfill 源与无标记 legacy 均可覆盖——标记缺失视为历史产物,需可重建以治愈存量摘要行)
435
+ {
436
+ const snapPath = path.join(metricsDir, `${row.id}.json`);
437
+ if (fs.existsSync(snapPath)) {
438
+ let protectedLive = false;
439
+ if (!skipExisting) {
440
+ try {
441
+ protectedLive = JSON.parse(fs.readFileSync(snapPath, "utf-8")).source === "live";
442
+ }
443
+ catch { /* 坏文件视为可覆盖 */ }
444
+ }
445
+ if (skipExisting || protectedLive) {
446
+ stat.skippedExisting++;
447
+ continue;
448
+ }
449
+ }
450
+ }
451
+ // 构建快照
452
+ const parentMessages = msgStmt.all(row.id);
453
+ const parentParts = partStmt.all(row.id);
454
+ const built = buildFromRows(parentMessages, parentParts);
455
+ const childrenBuilt = [];
456
+ for (const c of children) {
457
+ const cMsgs = msgStmt.all(c.id);
458
+ if (cMsgs.length === 0)
459
+ continue;
460
+ childrenBuilt.push({ row: c, built: buildFromRows(cMsgs, partStmt.all(c.id)) });
461
+ }
462
+ let tokens = sessionTokens(row);
463
+ for (const cb of childrenBuilt)
464
+ tokens = addTokens(tokens, sessionTokens(cb.row));
465
+ const denom = tokens.input + tokens.cacheRead;
466
+ const stepCount = built.steps.length;
467
+ const output = {
468
+ sessionId: row.id,
469
+ startTime: row.time_created,
470
+ endTime: row.time_updated > row.time_created ? row.time_updated : row.time_created,
471
+ duration: row.time_updated > row.time_created ? row.time_updated - row.time_created : 0,
472
+ systemPrompts: {},
473
+ rounds: roundsFromTimeline(parentMessages, parentParts),
474
+ tokens: {
475
+ ...tokens,
476
+ cacheHitRate: denom > 0 ? tokens.cacheRead / denom : 0,
477
+ avgTokensPerStep: stepCount > 0 ? tokens.total / stepCount : 0,
478
+ },
479
+ tools: {
480
+ ...built.toolStats,
481
+ slowestCall: built.toolStats.slowestCall ?? { tool: "", duration: 0 },
482
+ },
483
+ compactions: row.time_compacting ? 1 : 0,
484
+ anomaly: { triggered: false, events: [] },
485
+ stages: [],
486
+ header: {
487
+ sessionId: row.id,
488
+ workingDirectory: row.directory ?? "",
489
+ startTime: new Date(row.time_created).toISOString(),
490
+ model: parseModelDisplay(row.model),
491
+ agent: agent ?? "",
492
+ agentSwitches: 0,
493
+ agentUsage: agent ? { [agent]: 0 } : {},
494
+ modelSwitches: 0,
495
+ modelTokenDistribution: {},
496
+ },
497
+ codeStats: {
498
+ etsLines: 0, buildSuccess: false, fixCompileCount: 0, totalCompileErrors: 0,
499
+ firstBuildPerRound: [], firstBuildPassRate: 0, errorCodes: [],
500
+ warnings: { total: 0, byType: {}, entries: [] },
501
+ fixCycles: { successRate: 0, avgAttempts: 0, cycles: [] },
502
+ moduleTimings: [],
503
+ },
504
+ responseLength: built.responseLength,
505
+ skills: built.skills,
506
+ skillSearches: [],
507
+ planning: emptyPlanning(),
508
+ steps: built.steps,
509
+ subagents: childrenBuilt.map((cb) => ({
510
+ sessionId: cb.row.id,
511
+ agent: cb.row.agent ?? "",
512
+ title: cb.row.title ?? "",
513
+ steps: cb.built.steps,
514
+ tokens: sessionTokens(cb.row),
515
+ tools: {
516
+ ...cb.built.toolStats,
517
+ slowestCall: cb.built.toolStats.slowestCall ?? { tool: "", duration: 0 },
518
+ },
519
+ })),
520
+ source: "backfill",
521
+ };
522
+ if (!opts.dryRun) {
523
+ fs.mkdirSync(metricsDir, { recursive: true });
524
+ const filePath = path.join(metricsDir, `${row.id}.json`);
525
+ const tmpPath = filePath + ".tmp";
526
+ fs.writeFileSync(tmpPath, JSON.stringify(output, null, 2));
527
+ fs.renameSync(tmpPath, filePath);
528
+ // 摘要/详情双表联动入库(增量 soft:live 已有非零数据不覆盖;force 重跑强制刷新)
529
+ upsertSessionSnapshot(summaryFile, output, filePath, skipExisting ? "soft" : "force");
530
+ }
531
+ stat.written++;
532
+ const prev = newWatermarks.get(metricsDir) ?? 0;
533
+ if (row.time_created > prev)
534
+ newWatermarks.set(metricsDir, row.time_created);
535
+ opts.onSessionBackfilled?.(row.id, label, metricsDir);
536
+ }
537
+ if (!opts.dryRun) {
538
+ for (const [dir, w] of newWatermarks) {
539
+ const prev = opts.incremental ? wmOf(dir) : 0;
540
+ writeWatermark(dir, Math.max(prev, w));
541
+ }
542
+ }
543
+ const perEngine = opts.engines.map((e) => stats.get(e.label + "\u0000" + e.metricsDir)).filter(Boolean);
544
+ const fb = opts.fallback ? stats.get("fallback\u0000" + opts.fallback.metricsDir) ?? null : null;
545
+ return { perEngine, fallback: fb, skippedByFilter, unattributed, total };
546
+ }
547
+ finally {
548
+ db.close();
549
+ }
550
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * compile-analyzer.ts — Pure-function module for parsing hvigorw build output
3
+ * and extracting compile metrics (errors, warnings, module timings).
4
+ */
5
+ export interface CompileResult {
6
+ success: boolean;
7
+ duration: number;
8
+ errorCount: number;
9
+ warnCount: number;
10
+ errors: ErrorCodeEntry[];
11
+ warnings: WarningEntry[];
12
+ moduleTimings: Map<string, ModuleTiming>;
13
+ }
14
+ export interface ErrorCodeEntry {
15
+ code: string;
16
+ type: string;
17
+ message: string;
18
+ file: string;
19
+ line: number;
20
+ col: number;
21
+ }
22
+ export interface WarningEntry {
23
+ type: "deprecated_api" | "resource_conflict" | "signing" | "obfuscation" | "other";
24
+ message: string;
25
+ file?: string;
26
+ line?: number;
27
+ }
28
+ export interface ModuleTiming {
29
+ totalDuration: number;
30
+ taskCount: number;
31
+ slowestTask: {
32
+ name: string;
33
+ duration: number;
34
+ };
35
+ }
36
+ export declare function stripAnsi(raw: string): string;
37
+ export declare function extractErrorCodes(stripped: string): ErrorCodeEntry[];
38
+ export declare function extractWarnings(stripped: string): WarningEntry[];
39
+ export declare function extractModuleTimings(stripped: string): Map<string, ModuleTiming>;
40
+ export declare function parseHvigorwOutput(raw: string): CompileResult;