lumencode 1.1.0 → 1.3.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/README.md +41 -0
- package/hooks/claude-post-tool-batch.js +51 -0
- package/hooks/codex-hook.js +56 -0
- package/hooks/init-steps.js +10 -0
- package/hooks/install-codex.js +9 -0
- package/hooks/install.js +14 -0
- package/hooks/opencode-hook.js +45 -0
- package/hooks/post-tool-use.js +42 -0
- package/index.js +236 -22
- package/lib/aggregate.js +27 -9
- package/lib/attribution.js +13 -0
- package/lib/capture-recorder.js +141 -0
- package/lib/config.js +26 -2
- package/lib/git-attribution-candidates.js +37 -0
- package/lib/git-attribution-options.js +105 -0
- package/lib/git-paths.js +41 -0
- package/lib/git.js +581 -129
- package/lib/hooks-manager.js +379 -0
- package/lib/line-blame.js +140 -0
- package/lib/parser.js +40 -18
- package/lib/parsers/base.js +69 -67
- package/lib/parsers/claude.js +51 -53
- package/lib/parsers/codex.js +21 -9
- package/lib/parsers/index.js +153 -151
- package/lib/parsers/opencode.js +28 -20
- package/lib/report.js +3 -3
- package/lib/server.js +242 -29
- package/lib/step-schema.js +217 -0
- package/lib/step-tracker.js +323 -0
- package/package.json +8 -2
- package/public/api.js +21 -0
- package/public/app.js +127 -2
- package/public/config.js +2 -0
- package/public/git-insights.js +19 -0
- package/public/index.html +69 -0
- package/public/style.css +85 -1
package/lib/aggregate.js
CHANGED
|
@@ -20,7 +20,7 @@ function getProjectBaseName(projectPath) {
|
|
|
20
20
|
return parts[parts.length - 1] || '';
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function encodeProjectPath(projectPath) {
|
|
23
|
+
export function encodeProjectPath(projectPath) {
|
|
24
24
|
return projectPath
|
|
25
25
|
.replace(/:[\\/]/, '--') // D:/ → D--
|
|
26
26
|
.replace(/[\\/]/g, '-'); // 剩余 / 或 \ → -
|
|
@@ -47,7 +47,7 @@ export function collectAllRecords(claudeDir, excludeProjects = [], includeProjec
|
|
|
47
47
|
|
|
48
48
|
for (const projDir of dirs) {
|
|
49
49
|
const projPath = join(projectsDir, projDir);
|
|
50
|
-
const projName =
|
|
50
|
+
const projName = getProjectDisplayName(projDir);
|
|
51
51
|
|
|
52
52
|
if (encodedIncludes) {
|
|
53
53
|
if (!encodedIncludes.has(projDir)) {
|
|
@@ -74,7 +74,7 @@ export function collectAllRecords(claudeDir, excludeProjects = [], includeProjec
|
|
|
74
74
|
const sessionRecords = records.filter(r => r.sessionId);
|
|
75
75
|
sessionRecords.forEach(r => projSessions.add(r.sessionId));
|
|
76
76
|
projRequests += records.filter(r => isAssistantRecord(r)).length;
|
|
77
|
-
} catch {}
|
|
77
|
+
} catch (e) { console.warn(`[aggregate] 读取文件记录失败: ${filePath}`, e.message); }
|
|
78
78
|
|
|
79
79
|
// 解析子 agent 日志
|
|
80
80
|
try {
|
|
@@ -87,7 +87,7 @@ export function collectAllRecords(claudeDir, excludeProjects = [], includeProjec
|
|
|
87
87
|
}
|
|
88
88
|
projRequests += subRecords.filter(r => isAssistantRecord(r)).length;
|
|
89
89
|
subRecords.filter(r => r.sessionId).forEach(r => projSessions.add(r.sessionId));
|
|
90
|
-
} catch {}
|
|
90
|
+
} catch (e) { console.warn(`[aggregate] 解析子agent失败: ${filePath}`, e.message); }
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// 当无主 JSONL 文件时,扫描 sessions-index.json 和 UUID 子目录
|
|
@@ -127,7 +127,7 @@ export function collectAllRecords(claudeDir, excludeProjects = [], includeProjec
|
|
|
127
127
|
projRequests++;
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
} catch {}
|
|
130
|
+
} catch (e) { console.warn(`[aggregate] 解析sessions-index失败: ${indexPath}`, e.message); }
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// 扫描 UUID 子目录中的 subagent 文件
|
|
@@ -146,7 +146,7 @@ export function collectAllRecords(claudeDir, excludeProjects = [], includeProjec
|
|
|
146
146
|
}
|
|
147
147
|
projRequests += subRecords.filter(r => isAssistantRecord(r)).length;
|
|
148
148
|
subRecords.filter(r => r.sessionId).forEach(r => projSessions.add(r.sessionId));
|
|
149
|
-
} catch {}
|
|
149
|
+
} catch (e) { console.warn(`[aggregate] 扫描UUID子目录失败: ${uuidDir}`, e.message); }
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -187,7 +187,10 @@ export function deduplicateRecords(records) {
|
|
|
187
187
|
return deduped;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// 有损解码:encode 中 / 和 \ 都映射为 -,decode 无法区分 - 是路径分隔符还是项目名原字符
|
|
191
|
+
// 因此 decode 结果可能不准确(如 ccusage-report → ccusage/report)
|
|
192
|
+
// 内部匹配用原始目录名,此函数仅用于展示
|
|
193
|
+
export function decodeProjectName(dirName) {
|
|
191
194
|
let decoded = dirName
|
|
192
195
|
.replace(/^([A-Z])-/, '$1:/')
|
|
193
196
|
.replace(/--/g, '/')
|
|
@@ -197,7 +200,6 @@ function decodeProjectName(dirName) {
|
|
|
197
200
|
decoded = '.../' + decoded.slice(3);
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
// 去掉尾部多余斜杠
|
|
201
203
|
decoded = decoded.replace(/\/+$/, '');
|
|
202
204
|
|
|
203
205
|
if (/^[A-Z]:$/.test(decoded)) {
|
|
@@ -207,6 +209,13 @@ function decodeProjectName(dirName) {
|
|
|
207
209
|
return decoded || '[未知项目]';
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
// 从有损 decode 结果中提取项目显示名:取最后一个路径段
|
|
213
|
+
export function getProjectDisplayName(dirName) {
|
|
214
|
+
const decoded = decodeProjectName(dirName);
|
|
215
|
+
const segments = decoded.replace(/\\/g, '/').replace(/\/+$/, '').split('/');
|
|
216
|
+
return segments[segments.length - 1] || decoded || '[未知项目]';
|
|
217
|
+
}
|
|
218
|
+
|
|
210
219
|
export function computeUsageStats(records, scenarioKeywords, costMode = 'auto') {
|
|
211
220
|
const stats = {
|
|
212
221
|
sessionCount: new Set(records.filter(r => !(r.metadata?.isSubagent || r.isSubagent)).map(r => r.sessionId).filter(Boolean)).size,
|
|
@@ -404,7 +413,16 @@ export function computeUsageStats(records, scenarioKeywords, costMode = 'auto')
|
|
|
404
413
|
}
|
|
405
414
|
|
|
406
415
|
export function filterRecordsByPeriod(records, period, refDate, options = {}) {
|
|
407
|
-
|
|
416
|
+
// 字符串日期 "2026-05-26" 被 new Date() 解析为 UTC 午夜,
|
|
417
|
+
// formatDate 用本地时间,UTC- 时区下日期会偏移一天。
|
|
418
|
+
// 修复:将 YYYY-MM-DD 字符串按本地时间解析
|
|
419
|
+
let d;
|
|
420
|
+
if (typeof refDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(refDate)) {
|
|
421
|
+
const [y, m, day] = refDate.split('-').map(Number);
|
|
422
|
+
d = new Date(y, m - 1, day);
|
|
423
|
+
} else {
|
|
424
|
+
d = new Date(refDate);
|
|
425
|
+
}
|
|
408
426
|
let start, end;
|
|
409
427
|
|
|
410
428
|
switch (period) {
|
package/lib/attribution.js
CHANGED
|
@@ -22,6 +22,19 @@ export function classifyAttribution(input = {}) {
|
|
|
22
22
|
...evidence.map(e => e.tool),
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
+
// Step blame: confirmed by line-level tracking
|
|
26
|
+
if (input.lineBlame?.source === 'step_blame') {
|
|
27
|
+
return {
|
|
28
|
+
commitHash: input.commitHash || null,
|
|
29
|
+
classification: 'confirmed_ai',
|
|
30
|
+
primaryTool: input.primaryTool || null,
|
|
31
|
+
tools: unique([...(Array.isArray(input.tools) ? input.tools : []), input.primaryTool].filter(Boolean)),
|
|
32
|
+
evidence: [...(input.evidence || []), 'step_blame'],
|
|
33
|
+
source: 'auto',
|
|
34
|
+
reason: 'step_blame',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
if (override?.classification) {
|
|
26
39
|
const classification = normalizeClassification(override.classification);
|
|
27
40
|
return {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, statSync, unlinkSync } from 'fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'path';
|
|
3
|
+
import { StepTracker } from './step-tracker.js';
|
|
4
|
+
|
|
5
|
+
export const ORIGINS = Object.freeze({
|
|
6
|
+
CLAUDE_CODE: 'claude_code',
|
|
7
|
+
CODEX_CLI: 'codex_cli',
|
|
8
|
+
OPENCODE: 'opencode',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise(resolveSleep => setTimeout(resolveSleep, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function withDbLock(dbPath, fn) {
|
|
16
|
+
const lockPath = `${dbPath}.lock`;
|
|
17
|
+
const deadline = Date.now() + 2_000;
|
|
18
|
+
let fd = null;
|
|
19
|
+
|
|
20
|
+
while (Date.now() < deadline) {
|
|
21
|
+
try {
|
|
22
|
+
fd = openSync(lockPath, 'wx');
|
|
23
|
+
break;
|
|
24
|
+
} catch {
|
|
25
|
+
try {
|
|
26
|
+
if (Date.now() - statSync(lockPath).mtimeMs > 10_000) unlinkSync(lockPath);
|
|
27
|
+
} catch {}
|
|
28
|
+
await sleep(50);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (fd === null) return null;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return await fn();
|
|
36
|
+
} finally {
|
|
37
|
+
try { closeSync(fd); } catch {}
|
|
38
|
+
try { unlinkSync(lockPath); } catch {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeOriginSessionId(origin, sessionId) {
|
|
43
|
+
const normalizedOrigin = origin || ORIGINS.CLAUDE_CODE;
|
|
44
|
+
const rawSessionId = String(sessionId || 'unknown');
|
|
45
|
+
const prefix = `${normalizedOrigin}:`;
|
|
46
|
+
return rawSessionId.startsWith(prefix) ? rawSessionId : `${prefix}${rawSessionId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function recordToolUse(payload = {}) {
|
|
50
|
+
const cwd = resolve(payload.cwd || process.cwd());
|
|
51
|
+
const dbPath = payload.dbPath
|
|
52
|
+
? (isAbsolute(payload.dbPath) ? payload.dbPath : join(cwd, payload.dbPath))
|
|
53
|
+
: join(cwd, '.ccusage', 'steps.db');
|
|
54
|
+
|
|
55
|
+
if (!existsSync(dbPath)) {
|
|
56
|
+
return { recorded: false, reason: 'not_initialized' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const origin = payload.origin || ORIGINS.CLAUDE_CODE;
|
|
60
|
+
const sessionId = normalizeOriginSessionId(origin, payload.sessionId);
|
|
61
|
+
|
|
62
|
+
const result = await withDbLock(dbPath, async () => {
|
|
63
|
+
const tracker = new StepTracker(cwd, { dbPath });
|
|
64
|
+
try {
|
|
65
|
+
await tracker.open();
|
|
66
|
+
const stepId = await tracker.recordStep({
|
|
67
|
+
origin,
|
|
68
|
+
sessionId,
|
|
69
|
+
toolUseId: payload.toolUseId,
|
|
70
|
+
toolName: payload.toolName,
|
|
71
|
+
toolInput: payload.toolInput,
|
|
72
|
+
toolResponse: payload.toolResponse,
|
|
73
|
+
cwd,
|
|
74
|
+
timestamp: payload.timestamp || new Date().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
return stepId
|
|
77
|
+
? { recorded: true, stepId, sessionId, origin }
|
|
78
|
+
: { recorded: false, reason: 'no_target_files', sessionId, origin };
|
|
79
|
+
} finally {
|
|
80
|
+
tracker.close();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return result || { recorded: false, reason: 'lock_timeout', sessionId, origin };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeToolCall(call = {}) {
|
|
88
|
+
const tool = call.tool || {};
|
|
89
|
+
return {
|
|
90
|
+
toolUseId: call.toolUseId || call.tool_use_id || call.id || call.call_id || call.callId,
|
|
91
|
+
toolName: call.toolName || call.tool_name || call.name || tool.name,
|
|
92
|
+
toolInput: call.toolInput || call.tool_input || call.input || call.args || tool.input || {},
|
|
93
|
+
toolResponse: call.toolResponse || call.tool_response || call.output || call.response || tool.output,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function recordToolBatch(payload = {}) {
|
|
98
|
+
const cwd = resolve(payload.cwd || process.cwd());
|
|
99
|
+
const dbPath = payload.dbPath
|
|
100
|
+
? (isAbsolute(payload.dbPath) ? payload.dbPath : join(cwd, payload.dbPath))
|
|
101
|
+
: join(cwd, '.ccusage', 'steps.db');
|
|
102
|
+
|
|
103
|
+
if (!existsSync(dbPath)) {
|
|
104
|
+
return { recorded: false, reason: 'not_initialized' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const origin = payload.origin || ORIGINS.CLAUDE_CODE;
|
|
108
|
+
const sessionId = normalizeOriginSessionId(origin, payload.sessionId);
|
|
109
|
+
const toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls.map(normalizeToolCall) : [];
|
|
110
|
+
if (toolCalls.length === 0) {
|
|
111
|
+
return { recorded: false, reason: 'empty_batch', sessionId, origin };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const batchId = payload.batchId || toolCalls
|
|
115
|
+
.map(call => call.toolUseId || `${call.toolName || 'tool'}:${JSON.stringify(call.toolInput || {})}`)
|
|
116
|
+
.join(',');
|
|
117
|
+
|
|
118
|
+
const result = await withDbLock(dbPath, async () => {
|
|
119
|
+
const tracker = new StepTracker(cwd, { dbPath });
|
|
120
|
+
try {
|
|
121
|
+
await tracker.open();
|
|
122
|
+
const stepId = await tracker.recordStep({
|
|
123
|
+
origin,
|
|
124
|
+
sessionId,
|
|
125
|
+
toolUseId: `batch:${batchId}`,
|
|
126
|
+
toolName: 'ToolBatch',
|
|
127
|
+
toolInput: { toolCalls },
|
|
128
|
+
toolCalls,
|
|
129
|
+
cwd,
|
|
130
|
+
timestamp: payload.timestamp || new Date().toISOString(),
|
|
131
|
+
});
|
|
132
|
+
return stepId
|
|
133
|
+
? { recorded: true, stepId, sessionId, origin, toolCallCount: toolCalls.length }
|
|
134
|
+
: { recorded: false, reason: 'no_target_files', sessionId, origin, toolCallCount: toolCalls.length };
|
|
135
|
+
} finally {
|
|
136
|
+
tracker.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return result || { recorded: false, reason: 'lock_timeout', sessionId, origin };
|
|
141
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { DEFAULT_ATTRIBUTION_OPTIONS } from './git-attribution-options.js';
|
|
4
5
|
|
|
5
6
|
const CONFIG_LOCATIONS = [
|
|
6
7
|
join(homedir(), '.lumencode.json'),
|
|
@@ -25,8 +26,31 @@ const DEFAULT_CONFIG = {
|
|
|
25
26
|
review: ['review', '审查', '检查', '代码审查', '/review'],
|
|
26
27
|
planning: ['计划', 'plan', '设计', '架构', '方案', 'design', 'architect'],
|
|
27
28
|
},
|
|
29
|
+
aiAttribution: DEFAULT_ATTRIBUTION_OPTIONS,
|
|
30
|
+
stepTracking: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
dbPath: '.ccusage/steps.db',
|
|
33
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
34
|
+
ignorePatterns: ['node_modules/', '.git/', 'dist/', 'build/', '.next/', '.cache/'],
|
|
35
|
+
},
|
|
28
36
|
};
|
|
29
37
|
|
|
38
|
+
// 深合并:对嵌套对象和数组做合并而非覆盖
|
|
39
|
+
function deepMerge(target, source) {
|
|
40
|
+
const result = { ...target };
|
|
41
|
+
for (const key of Object.keys(source)) {
|
|
42
|
+
const sv = source[key];
|
|
43
|
+
const tv = target[key];
|
|
44
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) &&
|
|
45
|
+
tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
46
|
+
result[key] = deepMerge(tv, sv);
|
|
47
|
+
} else {
|
|
48
|
+
result[key] = sv;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
export function loadConfig(configPath) {
|
|
31
55
|
let config = { ...DEFAULT_CONFIG };
|
|
32
56
|
|
|
@@ -35,7 +59,7 @@ export function loadConfig(configPath) {
|
|
|
35
59
|
if (existsSync(configPath)) {
|
|
36
60
|
try {
|
|
37
61
|
const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
38
|
-
config =
|
|
62
|
+
config = deepMerge(config, userConfig);
|
|
39
63
|
return config;
|
|
40
64
|
} catch (e) {
|
|
41
65
|
console.error(`配置文件读取失败: ${configPath}`, e.message);
|
|
@@ -50,7 +74,7 @@ export function loadConfig(configPath) {
|
|
|
50
74
|
if (existsSync(p)) {
|
|
51
75
|
try {
|
|
52
76
|
const userConfig = JSON.parse(readFileSync(p, 'utf-8'));
|
|
53
|
-
config =
|
|
77
|
+
config = deepMerge(config, userConfig);
|
|
54
78
|
return config;
|
|
55
79
|
} catch (e) {
|
|
56
80
|
console.error(`配置文件读取失败: ${p}`, e.message);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function scoreSessionCandidate(commit, session, context = {}) {
|
|
2
|
+
const distanceMs = context.distanceMs ?? Number.MAX_SAFE_INTEGER;
|
|
3
|
+
const fileOverlapRatio = context.fileOverlapRatio ?? 0;
|
|
4
|
+
const matchedFiles = context.matchedFiles ?? [];
|
|
5
|
+
let score = 0;
|
|
6
|
+
const reasons = [];
|
|
7
|
+
|
|
8
|
+
if (context.hasStrongBashMatch) {
|
|
9
|
+
score += 100;
|
|
10
|
+
reasons.push('bash_git_commit');
|
|
11
|
+
}
|
|
12
|
+
if (context.projectMatches) {
|
|
13
|
+
score += 40;
|
|
14
|
+
reasons.push('project_match');
|
|
15
|
+
}
|
|
16
|
+
if (fileOverlapRatio > 0) {
|
|
17
|
+
score += Math.round(fileOverlapRatio * 35);
|
|
18
|
+
reasons.push('file_overlap');
|
|
19
|
+
}
|
|
20
|
+
if (Number.isFinite(distanceMs)) {
|
|
21
|
+
score += Math.max(0, 20 - Math.floor(distanceMs / 60000));
|
|
22
|
+
reasons.push('time_proximity');
|
|
23
|
+
}
|
|
24
|
+
if (session.primaryTool) {
|
|
25
|
+
score += 5;
|
|
26
|
+
reasons.push('primary_tool');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
sessionId: session.id,
|
|
31
|
+
score,
|
|
32
|
+
reasons,
|
|
33
|
+
distanceMs,
|
|
34
|
+
fileOverlapRatio,
|
|
35
|
+
matchedFiles,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const DEFAULT_ATTRIBUTION_OPTIONS = {
|
|
2
|
+
windows: {
|
|
3
|
+
weakWindowMinutes: 30,
|
|
4
|
+
crossDayWindowDays: 3,
|
|
5
|
+
},
|
|
6
|
+
confidenceThresholds: {
|
|
7
|
+
high: 0.75,
|
|
8
|
+
medium: 0.45,
|
|
9
|
+
low: 0.20,
|
|
10
|
+
},
|
|
11
|
+
confidenceWeights: {
|
|
12
|
+
high: 1.0,
|
|
13
|
+
medium: 0.7,
|
|
14
|
+
low: 0.2,
|
|
15
|
+
none: 0,
|
|
16
|
+
},
|
|
17
|
+
scoreWeights: {
|
|
18
|
+
explicitSignature: 0.85,
|
|
19
|
+
explicitAuthor: 0.80,
|
|
20
|
+
genericAISignature: 0.70,
|
|
21
|
+
sessionStrong: 0.40,
|
|
22
|
+
sessionCrossDay: 0.25,
|
|
23
|
+
sessionWeak: 0.15,
|
|
24
|
+
sessionCrossDayWeak: 0.10,
|
|
25
|
+
fileOverlap: 0.30,
|
|
26
|
+
styleBulletList: 0.15,
|
|
27
|
+
styleConventionalScope: 0.05,
|
|
28
|
+
styleImperativeMood: 0.10,
|
|
29
|
+
styleLongStructuredBody: 0.05,
|
|
30
|
+
baselineDeviationHigh: 0.15,
|
|
31
|
+
baselineDeviationMedium: 0.08,
|
|
32
|
+
negativeMergeCommit: -0.50,
|
|
33
|
+
negativeInformal: -0.20,
|
|
34
|
+
negativeSmallScope: -0.15,
|
|
35
|
+
negativeWIP: -0.15,
|
|
36
|
+
humanBaselineMatch: -0.10,
|
|
37
|
+
},
|
|
38
|
+
explicitSignalPolicy: {
|
|
39
|
+
coAuthor: 'strong',
|
|
40
|
+
generatedWith: 'strong',
|
|
41
|
+
assistedBy: 'strong',
|
|
42
|
+
robotEmoji: 'medium',
|
|
43
|
+
genericAIKeywords: 'medium',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function finiteNumber(value) {
|
|
48
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function positiveNumber(value, fallback) {
|
|
52
|
+
return finiteNumber(value) && value > 0 ? value : fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ratioNumber(value, fallback) {
|
|
56
|
+
return finiteNumber(value) && value >= 0 && value <= 1 ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeThresholds(input = {}, defaults) {
|
|
60
|
+
let high = ratioNumber(input.high, defaults.high);
|
|
61
|
+
let medium = ratioNumber(input.medium, defaults.medium);
|
|
62
|
+
let low = ratioNumber(input.low, defaults.low);
|
|
63
|
+
|
|
64
|
+
if (high < medium) high = defaults.high;
|
|
65
|
+
if (medium < low) medium = defaults.medium;
|
|
66
|
+
if (high < medium) medium = defaults.medium;
|
|
67
|
+
if (medium < low) low = defaults.low;
|
|
68
|
+
|
|
69
|
+
return { high, medium, low };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeWeights(input = {}, defaults) {
|
|
73
|
+
return {
|
|
74
|
+
high: ratioNumber(input.high, defaults.high),
|
|
75
|
+
medium: ratioNumber(input.medium, defaults.medium),
|
|
76
|
+
low: ratioNumber(input.low, defaults.low),
|
|
77
|
+
none: ratioNumber(input.none, defaults.none),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scoreWeight(value, fallback) {
|
|
82
|
+
return finiteNumber(value) && value >= -1 && value <= 1 ? value : fallback;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeScoreWeights(input = {}, defaults) {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const [key, fallback] of Object.entries(defaults)) {
|
|
88
|
+
result[key] = scoreWeight(input[key], fallback);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveAttributionOptions(input = {}) {
|
|
94
|
+
const defaults = DEFAULT_ATTRIBUTION_OPTIONS;
|
|
95
|
+
return {
|
|
96
|
+
windows: {
|
|
97
|
+
weakWindowMinutes: positiveNumber(input.windows?.weakWindowMinutes, defaults.windows.weakWindowMinutes),
|
|
98
|
+
crossDayWindowDays: positiveNumber(input.windows?.crossDayWindowDays, defaults.windows.crossDayWindowDays),
|
|
99
|
+
},
|
|
100
|
+
confidenceThresholds: normalizeThresholds(input.confidenceThresholds, defaults.confidenceThresholds),
|
|
101
|
+
confidenceWeights: normalizeWeights(input.confidenceWeights, defaults.confidenceWeights),
|
|
102
|
+
scoreWeights: normalizeScoreWeights(input.scoreWeights, defaults.scoreWeights),
|
|
103
|
+
explicitSignalPolicy: { ...defaults.explicitSignalPolicy, ...(input.explicitSignalPolicy || {}) },
|
|
104
|
+
};
|
|
105
|
+
}
|
package/lib/git-paths.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function normalizePathForGit(value) {
|
|
2
|
+
if (!value) return '';
|
|
3
|
+
return String(value).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '').toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function normalizeCommitFilePath(value) {
|
|
7
|
+
return normalizePathForGit(value).replace(/^\.?\//, '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toRepoRelativePath(filePath, repoPath) {
|
|
11
|
+
const file = normalizePathForGit(filePath);
|
|
12
|
+
const repo = normalizePathForGit(repoPath);
|
|
13
|
+
if (!file) return '';
|
|
14
|
+
if (!repo || !file.includes('/')) return normalizeCommitFilePath(file.replace(/^[a-z]:\//i, ''));
|
|
15
|
+
if (file === repo) return '';
|
|
16
|
+
if (file.startsWith(repo + '/')) return normalizeCommitFilePath(file.slice(repo.length + 1));
|
|
17
|
+
return normalizeCommitFilePath(file);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function projectKey(value) {
|
|
21
|
+
return normalizePathForGit(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pathContains(parent, child) {
|
|
25
|
+
return parent === child || child.startsWith(parent + '/');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function projectMatches(commitRepo, sessionProject) {
|
|
29
|
+
const a = projectKey(commitRepo);
|
|
30
|
+
const b = projectKey(sessionProject);
|
|
31
|
+
if (!a || !b) return true;
|
|
32
|
+
if (pathContains(a, b) || pathContains(b, a)) return true;
|
|
33
|
+
if (!a.includes('/') || !b.includes('/')) {
|
|
34
|
+
return a.split('/').pop() === b.split('/').pop();
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function filePathMatches(a, b) {
|
|
40
|
+
return normalizeCommitFilePath(a) === normalizeCommitFilePath(b);
|
|
41
|
+
}
|