@devtrace-ai/cli 1.0.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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/analyzer.js +163 -0
- package/dist/capture/claude.js +219 -0
- package/dist/capture/trailers.js +112 -0
- package/dist/card.js +142 -0
- package/dist/chat.js +266 -0
- package/dist/claude-correlate.js +150 -0
- package/dist/claude-hook-install.js +183 -0
- package/dist/claude-hooks.js +236 -0
- package/dist/cli.js +113 -0
- package/dist/config.js +174 -0
- package/dist/core/scan.js +265 -0
- package/dist/core/survival.js +400 -0
- package/dist/detector.js +92 -0
- package/dist/digest.js +183 -0
- package/dist/git.js +212 -0
- package/dist/hooks.js +166 -0
- package/dist/html_report.js +325 -0
- package/dist/ignore.js +126 -0
- package/dist/index.js +798 -0
- package/dist/metrics.js +133 -0
- package/dist/reporter.js +127 -0
- package/dist/storage.js +344 -0
- package/dist/test_runner.js +143 -0
- package/dist/types.js +2 -0
- package/package.json +53 -0
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MetricsClassifier = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
class MetricsClassifier {
|
|
39
|
+
/**
|
|
40
|
+
* Classify the primary change type based on diff files and structures.
|
|
41
|
+
*/
|
|
42
|
+
static classify(diff, commitMessage = '') {
|
|
43
|
+
if (diff.filesChanged === 0) {
|
|
44
|
+
return { changeType: 'unknown', confidence: 1.0 };
|
|
45
|
+
}
|
|
46
|
+
let docsScore = 0;
|
|
47
|
+
let testScore = 0;
|
|
48
|
+
let configScore = 0;
|
|
49
|
+
let styleScore = 0;
|
|
50
|
+
let featureScore = 0;
|
|
51
|
+
let refactorScore = 0;
|
|
52
|
+
for (const file of diff.files) {
|
|
53
|
+
const filepath = file.path.toLowerCase();
|
|
54
|
+
const ext = path.extname(filepath);
|
|
55
|
+
// 1. Documentation files
|
|
56
|
+
if (ext === '.md' ||
|
|
57
|
+
ext === '.txt' ||
|
|
58
|
+
filepath.includes('/docs/') ||
|
|
59
|
+
filepath.includes('/license')) {
|
|
60
|
+
docsScore += file.insertions + file.deletions + 10;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// 2. Test files
|
|
64
|
+
if (filepath.includes('test') ||
|
|
65
|
+
filepath.includes('spec') ||
|
|
66
|
+
filepath.includes('__tests__')) {
|
|
67
|
+
testScore += file.insertions + file.deletions + 10;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// 3. Config files
|
|
71
|
+
if (filepath.endsWith('.json') ||
|
|
72
|
+
filepath.endsWith('.yaml') ||
|
|
73
|
+
filepath.endsWith('.yml') ||
|
|
74
|
+
filepath.endsWith('.toml') ||
|
|
75
|
+
filepath.startsWith('.') ||
|
|
76
|
+
filepath.includes('config')) {
|
|
77
|
+
configScore += file.insertions + file.deletions + 10;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// 4. Style sheets
|
|
81
|
+
if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') {
|
|
82
|
+
styleScore += file.insertions + file.deletions + 10;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// 5. Code changes - Feature vs Bugfix/Refactor heuristic
|
|
86
|
+
// Greenfield code dumps (lots of insertions, very few deletions) suggest feature
|
|
87
|
+
const linesChanged = file.insertions + file.deletions;
|
|
88
|
+
if (linesChanged > 0) {
|
|
89
|
+
const ratio = file.insertions / linesChanged;
|
|
90
|
+
if (ratio > 0.8 && file.insertions > 20) {
|
|
91
|
+
featureScore += file.insertions;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
refactorScore += file.insertions + file.deletions;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const scores = [
|
|
99
|
+
{ type: 'docs', score: docsScore },
|
|
100
|
+
{ type: 'test', score: testScore },
|
|
101
|
+
{ type: 'config', score: configScore },
|
|
102
|
+
{ type: 'style', score: styleScore },
|
|
103
|
+
{ type: 'feature', score: featureScore },
|
|
104
|
+
{ type: 'refactor', score: refactorScore }
|
|
105
|
+
];
|
|
106
|
+
// Sort by descending score
|
|
107
|
+
scores.sort((a, b) => b.score - a.score);
|
|
108
|
+
const top = scores[0];
|
|
109
|
+
if (top.score === 0) {
|
|
110
|
+
return { changeType: 'unknown', confidence: 0.5 };
|
|
111
|
+
}
|
|
112
|
+
const totalScore = scores.reduce((sum, item) => sum + item.score, 0);
|
|
113
|
+
const confidence = parseFloat((top.score / totalScore).toFixed(2));
|
|
114
|
+
// Refine refactor vs bugfix
|
|
115
|
+
// If it's a code edit, default to bugfix if commit msg suggests fix, otherwise refactor
|
|
116
|
+
let changeType = top.type;
|
|
117
|
+
const msg = commitMessage.toLowerCase();
|
|
118
|
+
const isFixMsg = msg && (msg.includes('fix') ||
|
|
119
|
+
msg.includes('bug') ||
|
|
120
|
+
msg.includes('issue') ||
|
|
121
|
+
msg.includes('patch') ||
|
|
122
|
+
msg.includes('resolve') ||
|
|
123
|
+
msg.includes('close'));
|
|
124
|
+
if ((changeType === 'refactor' || changeType === 'feature' || changeType === 'unknown') && isFixMsg) {
|
|
125
|
+
changeType = 'bugfix';
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
changeType,
|
|
129
|
+
confidence
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.MetricsClassifier = MetricsClassifier;
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ReportGenerator = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const config_1 = require("./config");
|
|
40
|
+
class ReportGenerator {
|
|
41
|
+
static reportFileName = 'DEVTRACE_LOG.md';
|
|
42
|
+
/**
|
|
43
|
+
* Compiles data analysis into a readable markdown file in the ignored storage directory.
|
|
44
|
+
*/
|
|
45
|
+
static generateReport(stats, sessions, projectDir = process.cwd()) {
|
|
46
|
+
const config = config_1.ConfigManager.loadConfig(projectDir);
|
|
47
|
+
const reportPath = path.join(projectDir, config.storageDir, this.reportFileName);
|
|
48
|
+
let md = `# DevTrace-AI Empirical Interaction Log\n\n`;
|
|
49
|
+
md += `## 📊 Executive Telemetry Summary\n\n`;
|
|
50
|
+
md += `| Metric | Value |\n`;
|
|
51
|
+
md += `| :--- | :--- |\n`;
|
|
52
|
+
md += `| **Total Traced Commits** | ${stats.totalCommits} |\n`;
|
|
53
|
+
md += `| **AI-Assisted Commits** | ${stats.aiCommits} (${stats.aiContributionRate}%) |\n`;
|
|
54
|
+
md += `| **Human-Only Commits** | ${stats.humanCommits} (${(100 - stats.aiContributionRate).toFixed(1)}%) |\n`;
|
|
55
|
+
md += `| **Avg Insertions / Commit** | +${stats.avgInsertions} |\n`;
|
|
56
|
+
md += `| **Avg Deletions / Commit** | -${stats.avgDeletions} |\n`;
|
|
57
|
+
md += `| **AI Code Churn/Rework Rate** | ${stats.reworkRate}% |\n\n`;
|
|
58
|
+
md += `## 🛠️ Tool Comparison Metrics\n\n`;
|
|
59
|
+
md += `| Tool | Commits | Avg Insertions | Avg Deletions | Test Pass Rate | Correctness | Completeness | Effort |\n`;
|
|
60
|
+
md += `| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |\n`;
|
|
61
|
+
for (const tool of Object.keys(stats.byTool)) {
|
|
62
|
+
const ts = stats.byTool[tool];
|
|
63
|
+
if (ts.count === 0)
|
|
64
|
+
continue;
|
|
65
|
+
const totalTests = ts.testPasses + ts.testFailures;
|
|
66
|
+
const passRate = totalTests > 0 ? `${((ts.testPasses / totalTests) * 100).toFixed(0)}%` : '—';
|
|
67
|
+
const corr = ts.avgCorrectness > 0 ? `${ts.avgCorrectness}/5` : '—';
|
|
68
|
+
const comp = ts.avgCompleteness > 0 ? `${ts.avgCompleteness}/5` : '—';
|
|
69
|
+
const eff = ts.avgEffort > 0 ? `${ts.avgEffort}/5` : '—';
|
|
70
|
+
const avgIns = (ts.insertions / ts.count).toFixed(0);
|
|
71
|
+
const avgDel = (ts.deletions / ts.count).toFixed(0);
|
|
72
|
+
md += `| \`${tool}\` | ${ts.count} | +${avgIns} | -${avgDel} | ${passRate} | ${corr} | ${comp} | ${eff} |\n`;
|
|
73
|
+
}
|
|
74
|
+
md += `\n`;
|
|
75
|
+
md += `## 🏷️ Change Type Distribution\n\n`;
|
|
76
|
+
md += `| Change Type | Commits | AI-Assisted | Human-Only | Avg Insertions |\n`;
|
|
77
|
+
md += `| :--- | :---: | :---: | :---: | :---: |\n`;
|
|
78
|
+
for (const type of Object.keys(stats.byChangeType)) {
|
|
79
|
+
const cs = stats.byChangeType[type];
|
|
80
|
+
if (cs.count === 0)
|
|
81
|
+
continue;
|
|
82
|
+
md += `| \`${type}\` | ${cs.count} | ${cs.aiCount} | ${cs.humanCount} | +${cs.avgInsertions.toFixed(0)} |\n`;
|
|
83
|
+
}
|
|
84
|
+
md += `\n`;
|
|
85
|
+
md += `## 📅 Activity Timeline\n\n`;
|
|
86
|
+
md += `| Date | Total Commits | AI Commits | Human Commits | Sparkline |\n`;
|
|
87
|
+
md += `| :--- | :---: | :---: | :---: | :--- |\n`;
|
|
88
|
+
const sortedDates = Object.keys(stats.timeline).sort();
|
|
89
|
+
for (const date of sortedDates) {
|
|
90
|
+
const day = stats.timeline[date];
|
|
91
|
+
const aiBlocks = '█'.repeat(day.ai);
|
|
92
|
+
const humanBlocks = '░'.repeat(day.human);
|
|
93
|
+
md += `| ${date} | ${day.total} | ${day.ai} | ${day.human} | \`${aiBlocks}${humanBlocks}\` |\n`;
|
|
94
|
+
}
|
|
95
|
+
md += `\n`;
|
|
96
|
+
md += `## 📜 Traced Sessions Log\n\n`;
|
|
97
|
+
md += `| Timestamp | Branch | Tool | Change Type | Lines | Tests | Correctness | Survival (HEAD) | Survival (30d) |\n`;
|
|
98
|
+
md += `| :--- | :--- | :--- | :--- | :---: | :---: | :---: | :---: | :---: |\n`;
|
|
99
|
+
// Show latest 15 sessions for readability
|
|
100
|
+
const recentSessions = [...sessions].reverse().slice(0, 15);
|
|
101
|
+
for (const s of recentSessions) {
|
|
102
|
+
const time = s.timestamp.substring(11, 16);
|
|
103
|
+
const date = s.timestamp.substring(5, 10);
|
|
104
|
+
const delta = `+${s.diff.totalInsertions}/-${s.diff.totalDeletions}`;
|
|
105
|
+
let testStatus = '—';
|
|
106
|
+
if (s.testResult) {
|
|
107
|
+
testStatus = s.testResult.exitCode === 0 ? '✅ Pass' : '❌ Fail';
|
|
108
|
+
}
|
|
109
|
+
const correctness = s.annotation && s.annotation.correctness ? `${s.annotation.correctness}/5` : '—';
|
|
110
|
+
const survival = s.churn && s.churn.survivalRate !== undefined ? `${(s.churn.survivalRate * 100).toFixed(0)}%` : '—';
|
|
111
|
+
let survival30d = '—';
|
|
112
|
+
const w = s.churn?.survival30d;
|
|
113
|
+
if (w) {
|
|
114
|
+
if (w.status === 'measured')
|
|
115
|
+
survival30d = `${(w.rate * 100).toFixed(0)}%`;
|
|
116
|
+
else if (w.status === 'window-open')
|
|
117
|
+
survival30d = 'window open';
|
|
118
|
+
else
|
|
119
|
+
survival30d = 'unknown';
|
|
120
|
+
}
|
|
121
|
+
md += `| ${date} ${time} | \`${s.branchName}\` | \`${s.aiTool}\` | \`${s.changeType}\` | ${delta} | ${testStatus} | ${correctness} | ${survival} | ${survival30d} |\n`;
|
|
122
|
+
}
|
|
123
|
+
fs.writeFileSync(reportPath, md, 'utf8');
|
|
124
|
+
return reportPath;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.ReportGenerator = ReportGenerator;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.StorageManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const config_1 = require("./config");
|
|
40
|
+
/**
|
|
41
|
+
* CSV cell for windowed 30-day survival: a bare ratio when measured, or one of the honest
|
|
42
|
+
* non-numeric labels otherwise — never a fabricated 0/false for "window still open".
|
|
43
|
+
*/
|
|
44
|
+
function formatSurvival30d(survival30d) {
|
|
45
|
+
if (!survival30d)
|
|
46
|
+
return '';
|
|
47
|
+
if (survival30d.status === 'measured')
|
|
48
|
+
return survival30d.rate.toFixed(4);
|
|
49
|
+
return survival30d.status; // 'window-open' | 'unknown'
|
|
50
|
+
}
|
|
51
|
+
class StorageManager {
|
|
52
|
+
static CURRENT_VERSION = 1;
|
|
53
|
+
static getStoragePath(projectDir = process.cwd()) {
|
|
54
|
+
const config = config_1.ConfigManager.loadConfig(projectDir);
|
|
55
|
+
const storagePath = path.join(projectDir, config.storageDir);
|
|
56
|
+
if (!fs.existsSync(storagePath)) {
|
|
57
|
+
fs.mkdirSync(storagePath, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
return storagePath;
|
|
60
|
+
}
|
|
61
|
+
static getJsonlPath(projectDir = process.cwd()) {
|
|
62
|
+
return path.join(this.getStoragePath(projectDir), 'sessions.jsonl');
|
|
63
|
+
}
|
|
64
|
+
static getCsvPath(projectDir = process.cwd()) {
|
|
65
|
+
return path.join(this.getStoragePath(projectDir), 'sessions.csv');
|
|
66
|
+
}
|
|
67
|
+
static isProcessRunning(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return err.code === 'EPERM';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
static sleepSync(ms) {
|
|
77
|
+
try {
|
|
78
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
const limit = Date.now() + ms;
|
|
82
|
+
while (Date.now() < limit) { }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Acquires a cooperative lock on the sessions log file.
|
|
87
|
+
* Resolves stale locks automatically by checking the owner's process health.
|
|
88
|
+
*/
|
|
89
|
+
static acquireLock(projectDir) {
|
|
90
|
+
const lockPath = path.join(this.getStoragePath(projectDir), 'sessions.jsonl.lock');
|
|
91
|
+
const maxRetries = 100;
|
|
92
|
+
let retries = 0;
|
|
93
|
+
while (retries < maxRetries) {
|
|
94
|
+
try {
|
|
95
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
96
|
+
fs.writeSync(fd, String(process.pid));
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err.code === 'EEXIST') {
|
|
102
|
+
// Check if lock file is stale
|
|
103
|
+
try {
|
|
104
|
+
const stats = fs.statSync(lockPath);
|
|
105
|
+
const lockContent = fs.readFileSync(lockPath, 'utf8').trim();
|
|
106
|
+
const pid = parseInt(lockContent, 10);
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const isStaleByTime = now - stats.mtimeMs > 5000;
|
|
109
|
+
if ((isNaN(pid) && isStaleByTime) || (!isNaN(pid) && !this.isProcessRunning(pid))) {
|
|
110
|
+
// Stale lock, clean it up
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(lockPath);
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
continue; // Retry acquisition immediately
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// If we fail to read/stat, treat as normal contention and wait
|
|
120
|
+
}
|
|
121
|
+
retries++;
|
|
122
|
+
this.sleepSync(100);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Timeout acquiring database lock on: ${lockPath}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Releases the cooperative lock.
|
|
133
|
+
*/
|
|
134
|
+
static releaseLock(projectDir) {
|
|
135
|
+
const lockPath = path.join(this.getStoragePath(projectDir), 'sessions.jsonl.lock');
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(lockPath)) {
|
|
138
|
+
fs.unlinkSync(lockPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Appends a session object to the JSONL log file.
|
|
145
|
+
*/
|
|
146
|
+
static appendSession(session, projectDir = process.cwd()) {
|
|
147
|
+
this.acquireLock(projectDir);
|
|
148
|
+
try {
|
|
149
|
+
const jsonlPath = this.getJsonlPath(projectDir);
|
|
150
|
+
session.version = this.CURRENT_VERSION;
|
|
151
|
+
const line = JSON.stringify(session) + '\n';
|
|
152
|
+
fs.appendFileSync(jsonlPath, line, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
this.releaseLock(projectDir);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Synchronously loads all sessions.
|
|
160
|
+
*/
|
|
161
|
+
static loadSessionsSync(projectDir = process.cwd()) {
|
|
162
|
+
const jsonlPath = this.getJsonlPath(projectDir);
|
|
163
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const sessions = [];
|
|
167
|
+
try {
|
|
168
|
+
const content = fs.readFileSync(jsonlPath, 'utf8');
|
|
169
|
+
const lines = content.split('\n');
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
if (!trimmed)
|
|
173
|
+
continue;
|
|
174
|
+
try {
|
|
175
|
+
const session = JSON.parse(trimmed);
|
|
176
|
+
sessions.push(this.migrateSession(session));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.warn(`[DevTrace] Skipped corrupt log line: ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.warn(`[DevTrace] Failed to read sessions synchronously: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
return sessions;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Loads all sessions asynchronously (uses synchronous load under lock internally).
|
|
190
|
+
*/
|
|
191
|
+
static async loadSessions(projectDir = process.cwd()) {
|
|
192
|
+
this.acquireLock(projectDir);
|
|
193
|
+
try {
|
|
194
|
+
return this.loadSessionsSync(projectDir);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.releaseLock(projectDir);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Overwrite all sessions under lock.
|
|
202
|
+
*/
|
|
203
|
+
static saveAllSessions(sessions, projectDir = process.cwd()) {
|
|
204
|
+
this.acquireLock(projectDir);
|
|
205
|
+
try {
|
|
206
|
+
const jsonlPath = this.getJsonlPath(projectDir);
|
|
207
|
+
const tempPath = `${jsonlPath}.tmp`;
|
|
208
|
+
const lines = sessions.map(s => {
|
|
209
|
+
s.version = this.CURRENT_VERSION;
|
|
210
|
+
return JSON.stringify(s);
|
|
211
|
+
}).join('\n') + '\n';
|
|
212
|
+
fs.writeFileSync(tempPath, lines, 'utf8');
|
|
213
|
+
fs.renameSync(tempPath, jsonlPath);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.releaseLock(projectDir);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Transactionally updates a single session by ID under lock.
|
|
221
|
+
* Prevents overwrite race conditions during long-running tasks.
|
|
222
|
+
*/
|
|
223
|
+
static updateSessionSync(id, updater, projectDir = process.cwd()) {
|
|
224
|
+
this.acquireLock(projectDir);
|
|
225
|
+
try {
|
|
226
|
+
const sessions = this.loadSessionsSync(projectDir);
|
|
227
|
+
const index = sessions.findIndex(s => s.id === id);
|
|
228
|
+
if (index !== -1) {
|
|
229
|
+
updater(sessions[index]);
|
|
230
|
+
const jsonlPath = this.getJsonlPath(projectDir);
|
|
231
|
+
const tempPath = `${jsonlPath}.tmp`;
|
|
232
|
+
const lines = sessions.map(s => {
|
|
233
|
+
s.version = this.CURRENT_VERSION;
|
|
234
|
+
return JSON.stringify(s);
|
|
235
|
+
}).join('\n') + '\n';
|
|
236
|
+
fs.writeFileSync(tempPath, lines, 'utf8');
|
|
237
|
+
fs.renameSync(tempPath, jsonlPath);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
this.releaseLock(projectDir);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Exports the session logs into a standard format CSV loadable in R/Python.
|
|
246
|
+
* Sanitizes potential CSV Injection triggers (=, +, -, @) to secure formula execution.
|
|
247
|
+
*/
|
|
248
|
+
static async exportToCsv(projectDir = process.cwd()) {
|
|
249
|
+
const sessions = await this.loadSessions(projectDir);
|
|
250
|
+
const csvPath = this.getCsvPath(projectDir);
|
|
251
|
+
const headers = [
|
|
252
|
+
'id',
|
|
253
|
+
'timestamp',
|
|
254
|
+
'commitHash',
|
|
255
|
+
'parentCommitHash',
|
|
256
|
+
'captureMode',
|
|
257
|
+
'aiTool',
|
|
258
|
+
'aiToolConfidence',
|
|
259
|
+
'branchName',
|
|
260
|
+
'filesChanged',
|
|
261
|
+
'totalInsertions',
|
|
262
|
+
'totalDeletions',
|
|
263
|
+
'netLines',
|
|
264
|
+
'changeType',
|
|
265
|
+
'changeTypeConfidence',
|
|
266
|
+
'testsPassed',
|
|
267
|
+
'testsFailed',
|
|
268
|
+
'correctness',
|
|
269
|
+
'completeness',
|
|
270
|
+
'effortToIntegrate',
|
|
271
|
+
'promptText',
|
|
272
|
+
'survivalRate',
|
|
273
|
+
'survival30d',
|
|
274
|
+
'taskComplexity',
|
|
275
|
+
'llmModelName',
|
|
276
|
+
'notes'
|
|
277
|
+
];
|
|
278
|
+
const rows = [headers.join(',')];
|
|
279
|
+
for (const s of sessions) {
|
|
280
|
+
const escape = (val, isString = false) => {
|
|
281
|
+
if (val === null || val === undefined)
|
|
282
|
+
return '';
|
|
283
|
+
let str = String(val);
|
|
284
|
+
// Escape CSV Injection triggers (=, +, -, @) only for strings to avoid breaking negative numbers
|
|
285
|
+
if (isString && /^[=\+\-@]/.test(str)) {
|
|
286
|
+
str = `'` + str;
|
|
287
|
+
}
|
|
288
|
+
str = str.replace(/"/g, '""');
|
|
289
|
+
return str.includes(',') || str.includes('"') || str.includes('\n') ? `"${str}"` : str;
|
|
290
|
+
};
|
|
291
|
+
const testResult = s.testResult;
|
|
292
|
+
const annotation = s.annotation;
|
|
293
|
+
const churn = s.churn;
|
|
294
|
+
const row = [
|
|
295
|
+
[s.id, false],
|
|
296
|
+
[s.timestamp, false],
|
|
297
|
+
[s.commitHash, false],
|
|
298
|
+
[s.parentCommitHash, false],
|
|
299
|
+
[s.captureMode, false],
|
|
300
|
+
[s.aiTool, true],
|
|
301
|
+
[s.aiToolConfidence, false],
|
|
302
|
+
[s.branchName, true],
|
|
303
|
+
[s.diff.filesChanged, false],
|
|
304
|
+
[s.diff.totalInsertions, false],
|
|
305
|
+
[s.diff.totalDeletions, false],
|
|
306
|
+
[s.diff.netLines, false],
|
|
307
|
+
[s.changeType, true],
|
|
308
|
+
[s.changeTypeConfidence, false],
|
|
309
|
+
[testResult ? testResult.passed : '', false],
|
|
310
|
+
[testResult ? testResult.failed : '', false],
|
|
311
|
+
[annotation && annotation.correctness ? annotation.correctness : '', false],
|
|
312
|
+
[annotation && annotation.completeness ? annotation.completeness : '', false],
|
|
313
|
+
[annotation && annotation.effortToIntegrate ? annotation.effortToIntegrate : '', false],
|
|
314
|
+
[annotation && annotation.promptText ? annotation.promptText : '', true],
|
|
315
|
+
[churn && churn.survivalRate !== undefined ? churn.survivalRate : '', false],
|
|
316
|
+
[formatSurvival30d(churn?.survival30d), false],
|
|
317
|
+
[annotation && annotation.taskComplexity ? annotation.taskComplexity : '', true],
|
|
318
|
+
[annotation && annotation.llmModelName ? annotation.llmModelName : '', true],
|
|
319
|
+
[annotation && annotation.notes ? annotation.notes : '', true]
|
|
320
|
+
];
|
|
321
|
+
rows.push(row.map(([val, isStr]) => escape(val, isStr)).join(','));
|
|
322
|
+
}
|
|
323
|
+
fs.writeFileSync(csvPath, rows.join('\n') + '\n', 'utf8');
|
|
324
|
+
return csvPath;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Migrate older schemas to ensure properties exist.
|
|
328
|
+
*/
|
|
329
|
+
static migrateSession(session) {
|
|
330
|
+
if (!session.version || session.version < 1) {
|
|
331
|
+
if (!session.diff) {
|
|
332
|
+
session.diff = { filesChanged: 0, totalInsertions: 0, totalDeletions: 0, netLines: 0, files: [] };
|
|
333
|
+
}
|
|
334
|
+
if (!session.changeType)
|
|
335
|
+
session.changeType = 'unknown';
|
|
336
|
+
if (!session.changeTypeConfidence)
|
|
337
|
+
session.changeTypeConfidence = 0.0;
|
|
338
|
+
if (session.testResult === undefined)
|
|
339
|
+
session.testResult = null;
|
|
340
|
+
}
|
|
341
|
+
return session;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
exports.StorageManager = StorageManager;
|