lumencode 1.0.0 → 1.1.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/index.js +74 -63
- package/lib/git.js +51 -18
- package/lib/parsers/claude.js +321 -316
- package/lib/parsers/codex.js +360 -316
- package/lib/parsers/opencode.js +236 -216
- package/lib/record-utils.js +36 -35
- package/lib/report.js +41 -4
- package/lib/server.js +573 -523
- package/package.json +1 -1
- package/public/app.js +827 -809
- package/public/style.css +3 -2
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadConfig, initConfig, getConfigPath } from './lib/config.js';
|
|
3
3
|
import { collectAllRecords, computeUsageStats, filterRecordsByPeriod, normalizeProjectPath, computeTrendData, computePrevPeriodRange, groupBySessions } from './lib/aggregate.js';
|
|
4
|
-
import { getGitStatsForMultipleReposAsync,
|
|
4
|
+
import { getGitStatsForMultipleReposAsync, invalidateGitCache, finalizeGitStats, computeCommitTypes, computeFileHotspots } from './lib/git.js';
|
|
5
5
|
import { invalidateFileCache } from './lib/cache.js';
|
|
6
6
|
import { generateReport, generateWorkReport } from './lib/report.js';
|
|
7
7
|
import { startServer } from './lib/server.js';
|
|
@@ -97,54 +97,60 @@ async function buildReportData(period, dateArg, config, effectiveIncludeProjects
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// 其余逻辑保持不变
|
|
101
100
|
const { filtered, start, end } = filterRecordsByPeriod(toolRecords, period, dateArg, { customStart: options.customStart, customEnd: options.customEnd });
|
|
102
|
-
const
|
|
103
|
-
const sessions = groupBySessions(filtered);
|
|
101
|
+
const reposConfigured = !!(config.repos && config.repos.length > 0);
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
// ── 第一层并发:三个独立的同步计算 ──
|
|
104
|
+
const [usageStats, sessions, billingBlocks] = [
|
|
105
|
+
computeUsageStats(filtered, config.scenarioKeywords, config.costMode),
|
|
106
|
+
groupBySessions(filtered),
|
|
107
|
+
identifyBillingBlocks(filtered, config.blockQuota ? 5 : 5, config.costMode),
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// ── 第二层并发:gitStats(async) + trendData + prevStats ──
|
|
111
|
+
const gitStatsPromise = (async () => {
|
|
112
|
+
if (!reposConfigured) return null;
|
|
108
113
|
const coveredBases = new Set(filtered.map(r => {
|
|
109
114
|
const p = r.project || '';
|
|
110
115
|
return p.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
111
116
|
}).filter(Boolean));
|
|
112
|
-
|
|
113
|
-
if (toolRepos.length
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// 但提交类型和热点只基于窗口内
|
|
131
|
-
gitStats.commitTypes = computeCommitTypes(inWindow);
|
|
132
|
-
gitStats.fileHotspots = computeFileHotspots(inWindow, 10);
|
|
133
|
-
}
|
|
117
|
+
let toolRepos = config.repos.filter(r => coveredBases.has(r.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()));
|
|
118
|
+
if (toolRepos.length === 0) toolRepos = config.repos;
|
|
119
|
+
if (toolRepos.length === 0) return null;
|
|
120
|
+
const extendedEnd = new Date(end);
|
|
121
|
+
extendedEnd.setDate(extendedEnd.getDate() + 2);
|
|
122
|
+
const extendedEndStr = extendedEnd.toISOString().slice(0, 10) + 'T23:59:59';
|
|
123
|
+
let gs = await getGitStatsForMultipleReposAsync(toolRepos, start, extendedEndStr);
|
|
124
|
+
gs = finalizeGitStats(gs, sessions);
|
|
125
|
+
if (gs.commitList) {
|
|
126
|
+
const windowStart = start;
|
|
127
|
+
const windowEnd = end + 'T23:59:59';
|
|
128
|
+
const inWindow = gs.commitList.filter(c => (c.date || '') >= windowStart && (c.date || '') <= windowEnd);
|
|
129
|
+
gs.commits = inWindow.length;
|
|
130
|
+
gs.linesAdded = inWindow.reduce((s, c) => s + (c.linesAdded || 0), 0);
|
|
131
|
+
gs.linesDeleted = inWindow.reduce((s, c) => s + (c.linesDeleted || 0), 0);
|
|
132
|
+
gs.filesChanged = new Set(inWindow.flatMap(c => (c.files || []).map(f => f.path))).size;
|
|
133
|
+
gs.commitTypes = computeCommitTypes(inWindow);
|
|
134
|
+
gs.fileHotspots = computeFileHotspots(inWindow, 10);
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
+
return gs;
|
|
137
|
+
})();
|
|
136
138
|
|
|
137
|
-
const
|
|
139
|
+
const trendDataPromise = Promise.resolve(computeTrendData(toolRecords, period, dateArg));
|
|
138
140
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
const prevStatsPromise = (async () => {
|
|
142
|
+
const prevRange = computePrevPeriodRange(period, dateArg, { customStart: options.customStart, customEnd: options.customEnd });
|
|
143
|
+
const prevFiltered = toolRecords.filter(r => {
|
|
144
|
+
if (!r.timestamp) return false;
|
|
145
|
+
const date = r.timestamp.slice(0, 10);
|
|
146
|
+
return date >= prevRange.start && date <= prevRange.end;
|
|
147
|
+
});
|
|
148
|
+
return prevFiltered.length > 0 ? computeUsageStats(prevFiltered, config.scenarioKeywords, config.costMode) : null;
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
const [gitStats, trendData, prevStats] = await Promise.all([gitStatsPromise, trendDataPromise, prevStatsPromise]);
|
|
147
152
|
|
|
153
|
+
// ── 第三层:依赖 usageStats 的同步派生 ──
|
|
148
154
|
const slimSessions = sessions.map(s => ({
|
|
149
155
|
id: s.id,
|
|
150
156
|
project: s.project,
|
|
@@ -154,14 +160,8 @@ async function buildReportData(period, dateArg, config, effectiveIncludeProjects
|
|
|
154
160
|
commits: s.commits || [],
|
|
155
161
|
}));
|
|
156
162
|
|
|
157
|
-
const billingBlocks = identifyBillingBlocks(filtered, config.blockQuota ? 5 : 5, config.costMode);
|
|
158
|
-
|
|
159
|
-
const reposConfigured = !!(config.repos && config.repos.length > 0);
|
|
160
|
-
|
|
161
|
-
// 合并 toolBreakdown:usageStats 内含 token 粒度数据,parsers 提供 sessionCount
|
|
162
163
|
const statsTB = usageStats.toolBreakdown || {};
|
|
163
164
|
const mergedBreakdown = {};
|
|
164
|
-
// 以 parsers toolBreakdown 为基础(包含所有已启用工具),补充 stats 中的 token 数据
|
|
165
165
|
for (const [name, base] of Object.entries(toolBreakdown)) {
|
|
166
166
|
const s = statsTB[name] || {};
|
|
167
167
|
mergedBreakdown[name] = {
|
|
@@ -173,7 +173,6 @@ async function buildReportData(period, dateArg, config, effectiveIncludeProjects
|
|
|
173
173
|
sessionCount: base.sessionCount || 0,
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
-
// 补充 stats 中有但 parsers 无的极端情况
|
|
177
176
|
for (const [name, data] of Object.entries(statsTB)) {
|
|
178
177
|
if (!mergedBreakdown[name]) {
|
|
179
178
|
mergedBreakdown[name] = {
|
|
@@ -187,31 +186,43 @@ async function buildReportData(period, dateArg, config, effectiveIncludeProjects
|
|
|
187
186
|
}
|
|
188
187
|
}
|
|
189
188
|
|
|
190
|
-
//
|
|
189
|
+
// ── 第四层:projectDetails(从 commitList 按 repo 分组派生,无需再次 git 调用)──
|
|
191
190
|
const projectDetails = {};
|
|
192
191
|
const projEntries = Object.entries(usageStats.projects || {}).sort((a, b) => b[1].requests - a[1].requests);
|
|
193
|
-
if (reposConfigured && gitStats) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
192
|
+
if (reposConfigured && gitStats?.commitList?.length) {
|
|
193
|
+
const windowEnd = end + 'T23:59:59';
|
|
194
|
+
const inWindow = gitStats.commitList.filter(c => (c.date || '') >= start && (c.date || '') <= windowEnd);
|
|
195
|
+
const repoGroups = new Map();
|
|
196
|
+
for (const c of inWindow) {
|
|
197
|
+
const base = (c.repo || '').replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
198
|
+
if (!base) continue;
|
|
199
|
+
if (!repoGroups.has(base)) repoGroups.set(base, []);
|
|
200
|
+
repoGroups.get(base).push(c);
|
|
201
|
+
}
|
|
201
202
|
for (const [projName, projStats] of projEntries) {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
const repoCommits = repoGroups.get(projName) || [];
|
|
204
|
+
if (repoCommits.length === 0) {
|
|
205
|
+
projectDetails[projName] = { usage: projStats, git: null, topCommits: [] };
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const uniqueFiles = new Set();
|
|
209
|
+
let linesAdded = 0, linesDeleted = 0;
|
|
210
|
+
for (const c of repoCommits) {
|
|
211
|
+
linesAdded += c.linesAdded || 0;
|
|
212
|
+
linesDeleted += c.linesDeleted || 0;
|
|
213
|
+
for (const f of c.files || []) uniqueFiles.add(f.path);
|
|
214
|
+
}
|
|
215
|
+
const topCommits = repoCommits
|
|
205
216
|
.filter(c => c.type === 'feat' || c.type === 'fix')
|
|
206
217
|
.slice(0, 5)
|
|
207
218
|
.map(c => ({ type: c.type, subject: c.subject, scope: c.scope }));
|
|
208
219
|
projectDetails[projName] = {
|
|
209
220
|
usage: projStats,
|
|
210
|
-
git:
|
|
211
|
-
commits:
|
|
212
|
-
filesChanged:
|
|
213
|
-
fileHotspots: (
|
|
214
|
-
}
|
|
221
|
+
git: {
|
|
222
|
+
commits: repoCommits.length, linesAdded, linesDeleted,
|
|
223
|
+
filesChanged: uniqueFiles.size,
|
|
224
|
+
fileHotspots: computeFileHotspots(repoCommits, 5),
|
|
225
|
+
},
|
|
215
226
|
topCommits,
|
|
216
227
|
};
|
|
217
228
|
}
|
package/lib/git.js
CHANGED
|
@@ -157,6 +157,13 @@ const AI_CONFIDENCE = {
|
|
|
157
157
|
HIGH: 'high',
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
const CONFIDENCE_WEIGHTS = {
|
|
161
|
+
[AI_CONFIDENCE.HIGH]: 1.0,
|
|
162
|
+
[AI_CONFIDENCE.MEDIUM]: 0.7,
|
|
163
|
+
[AI_CONFIDENCE.LOW]: 0.2,
|
|
164
|
+
[AI_CONFIDENCE.NONE]: 0,
|
|
165
|
+
};
|
|
166
|
+
|
|
160
167
|
function isCountedAIConfidence(confidence) {
|
|
161
168
|
return confidence === AI_CONFIDENCE.HIGH || confidence === AI_CONFIDENCE.MEDIUM;
|
|
162
169
|
}
|
|
@@ -285,6 +292,8 @@ export function detectAICommit(subject = '', author = '', body = '') {
|
|
|
285
292
|
|
|
286
293
|
export function computeAIContribution(commits, toolFilter = null) {
|
|
287
294
|
let aiCommits = 0, aiLinesAdded = 0, aiLinesDeleted = 0;
|
|
295
|
+
let possibleAICommits = 0, possibleAILinesAdded = 0, possibleAILinesDeleted = 0;
|
|
296
|
+
let weightedAILinesAdded = 0, weightedAILinesDeleted = 0;
|
|
288
297
|
let aiCommitLinesAdded = 0, aiCommitLinesDeleted = 0;
|
|
289
298
|
let aiFileLinesAdded = 0, aiFileLinesDeleted = 0;
|
|
290
299
|
let highConfidenceCommits = 0, mediumConfidenceCommits = 0, lowConfidenceCommits = 0;
|
|
@@ -303,27 +312,39 @@ export function computeAIContribution(commits, toolFilter = null) {
|
|
|
303
312
|
else if (confidence === AI_CONFIDENCE.MEDIUM) mediumConfidenceCommits++;
|
|
304
313
|
else if (confidence === AI_CONFIDENCE.LOW) lowConfidenceCommits++;
|
|
305
314
|
|
|
315
|
+
// 计算文件级行数(用于 HIGH/MEDIUM/LOW 各自统计)
|
|
316
|
+
const matchedFiles = new Set((c.aiEvidenceDetails?.matchedFiles || []).map(normalizeCommitFilePath));
|
|
317
|
+
const useMatchedFiles = matchedFiles.size > 0;
|
|
318
|
+
let fileAdded = 0;
|
|
319
|
+
let fileDeleted = 0;
|
|
320
|
+
for (const f of c.files || []) {
|
|
321
|
+
const filePath = normalizeCommitFilePath(f.path);
|
|
322
|
+
if (useMatchedFiles && !matchedFiles.has(filePath)) continue;
|
|
323
|
+
fileAdded += f.added || 0;
|
|
324
|
+
fileDeleted += f.deleted || 0;
|
|
325
|
+
}
|
|
326
|
+
if (!useMatchedFiles && (c.attributionType === 'explicit' || c.attributionType?.startsWith('session_'))) {
|
|
327
|
+
fileAdded = c.linesAdded || 0;
|
|
328
|
+
fileDeleted = c.linesDeleted || 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
306
331
|
if (isCountedAIConfidence(confidence)) {
|
|
307
332
|
aiCommits++;
|
|
308
333
|
aiCommitLinesAdded += c.linesAdded || 0;
|
|
309
334
|
aiCommitLinesDeleted += c.linesDeleted || 0;
|
|
310
|
-
|
|
311
|
-
const matchedFiles = new Set((c.aiEvidenceDetails?.matchedFiles || []).map(normalizeCommitFilePath));
|
|
312
|
-
const useMatchedFiles = matchedFiles.size > 0;
|
|
313
|
-
let fileAdded = 0;
|
|
314
|
-
let fileDeleted = 0;
|
|
315
|
-
for (const f of c.files || []) {
|
|
316
|
-
const filePath = normalizeCommitFilePath(f.path);
|
|
317
|
-
if (useMatchedFiles && !matchedFiles.has(filePath)) continue;
|
|
318
|
-
fileAdded += f.added || 0;
|
|
319
|
-
fileDeleted += f.deleted || 0;
|
|
320
|
-
}
|
|
321
|
-
if (!useMatchedFiles && (c.attributionType === 'explicit' || c.attributionType?.startsWith('session_'))) {
|
|
322
|
-
fileAdded = c.linesAdded || 0;
|
|
323
|
-
fileDeleted = c.linesDeleted || 0;
|
|
324
|
-
}
|
|
325
335
|
aiFileLinesAdded += fileAdded;
|
|
326
336
|
aiFileLinesDeleted += fileDeleted;
|
|
337
|
+
} else if (confidence === AI_CONFIDENCE.LOW) {
|
|
338
|
+
possibleAICommits++;
|
|
339
|
+
possibleAILinesAdded += fileAdded;
|
|
340
|
+
possibleAILinesDeleted += fileDeleted;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 加权计算:所有归因的 commit 都参与(包括 LOW)
|
|
344
|
+
const weight = CONFIDENCE_WEIGHTS[confidence] || 0;
|
|
345
|
+
if (weight > 0) {
|
|
346
|
+
weightedAILinesAdded += fileAdded * weight;
|
|
347
|
+
weightedAILinesDeleted += fileDeleted * weight;
|
|
327
348
|
}
|
|
328
349
|
}
|
|
329
350
|
aiLinesAdded = aiFileLinesAdded;
|
|
@@ -331,20 +352,32 @@ export function computeAIContribution(commits, toolFilter = null) {
|
|
|
331
352
|
const total = allCommits.length;
|
|
332
353
|
const totalLinesChanged = totalLinesAdded + totalLinesDeleted;
|
|
333
354
|
const aiLinesChanged = aiLinesAdded + aiLinesDeleted;
|
|
355
|
+
const possibleAILinesChanged = possibleAILinesAdded + possibleAILinesDeleted;
|
|
356
|
+
const weightedAILinesChanged = Math.round(weightedAILinesAdded + weightedAILinesDeleted);
|
|
334
357
|
return {
|
|
335
358
|
aiCommits,
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
possibleAICommits,
|
|
360
|
+
nonToolCommits: total - aiCommits - possibleAICommits,
|
|
361
|
+
humanCommits: total - aiCommits - possibleAICommits,
|
|
338
362
|
aiCommitRatio: total > 0 ? aiCommits / total : 0,
|
|
363
|
+
possibleAICommitRatio: total > 0 ? possibleAICommits / total : 0,
|
|
339
364
|
aiRatio: totalLinesChanged > 0 ? aiLinesChanged / totalLinesChanged : 0,
|
|
365
|
+
aiLineRatio: totalLinesChanged > 0 ? aiLinesChanged / totalLinesChanged : 0,
|
|
366
|
+
possibleAILineRatio: totalLinesChanged > 0 ? possibleAILinesChanged / totalLinesChanged : 0,
|
|
367
|
+
weightedAILineRatio: totalLinesChanged > 0 ? weightedAILinesChanged / totalLinesChanged : 0,
|
|
340
368
|
toolFilter: toolFilter || null,
|
|
341
369
|
aiLinesAdded,
|
|
342
370
|
aiLinesDeleted,
|
|
343
371
|
aiLinesChanged,
|
|
372
|
+
possibleAILinesAdded,
|
|
373
|
+
possibleAILinesDeleted,
|
|
374
|
+
possibleAILinesChanged,
|
|
375
|
+
weightedAILinesAdded: Math.round(weightedAILinesAdded),
|
|
376
|
+
weightedAILinesDeleted: Math.round(weightedAILinesDeleted),
|
|
377
|
+
weightedAILinesChanged,
|
|
344
378
|
totalLinesAdded,
|
|
345
379
|
totalLinesDeleted,
|
|
346
380
|
totalLinesChanged,
|
|
347
|
-
aiLineRatio: totalLinesChanged > 0 ? aiLinesChanged / totalLinesChanged : 0,
|
|
348
381
|
aiCommitLinesAdded,
|
|
349
382
|
aiCommitLinesDeleted,
|
|
350
383
|
aiFileLinesAdded,
|