thuban 0.3.2 → 0.3.4
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 +137 -102
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +16 -8
- package/cli.js +0 -2412
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -713
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -250
- package/packages/scanner/dependency-graph.js +0 -536
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -226
- package/packages/scanner/hallucination-detector.js +0 -652
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -92
- package/packages/scanner/license-manager.js +0 -318
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/sentinel-core.js +0 -608
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -565
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thuban Pre-Commit Gate
|
|
3
|
-
*
|
|
4
|
-
* @purpose 3-second critical scan on staged files — blocks commits with hallucinations
|
|
5
|
-
* @module thuban
|
|
6
|
-
* @layer scanner
|
|
7
|
-
* @exports-to cli
|
|
8
|
-
* @critical-for Revenue retention — stickiest feature, devs never remove it
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const { execSync } = require('child_process');
|
|
14
|
-
|
|
15
|
-
class PreCommitGate {
|
|
16
|
-
constructor(opts = {}) {
|
|
17
|
-
this.rootPath = opts.rootPath || process.cwd();
|
|
18
|
-
this.strict = opts.strict !== false; // Block commit on critical issues
|
|
19
|
-
this.timeout = opts.timeout || 5000; // 5 second max
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get staged files from git
|
|
24
|
-
*/
|
|
25
|
-
getStagedFiles() {
|
|
26
|
-
try {
|
|
27
|
-
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
28
|
-
cwd: this.rootPath,
|
|
29
|
-
encoding: 'utf-8',
|
|
30
|
-
timeout: 3000,
|
|
31
|
-
});
|
|
32
|
-
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.mjs', '.cjs'];
|
|
33
|
-
return output
|
|
34
|
-
.split('\n')
|
|
35
|
-
.filter(f => f.trim() && extensions.some(ext => f.endsWith(ext)))
|
|
36
|
-
.map(f => path.resolve(this.rootPath, f.trim()));
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Quick hallucination check on a single file
|
|
44
|
-
*/
|
|
45
|
-
checkFile(filePath) {
|
|
46
|
-
const issues = [];
|
|
47
|
-
let content;
|
|
48
|
-
try {
|
|
49
|
-
const stat = fs.statSync(filePath);
|
|
50
|
-
if (stat.size > 512 * 1024) return issues; // Skip files > 512KB
|
|
51
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
52
|
-
} catch (e) {
|
|
53
|
-
return issues;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const lines = content.split('\n');
|
|
57
|
-
const relPath = path.relative(this.rootPath, filePath);
|
|
58
|
-
|
|
59
|
-
// Phantom API patterns — APIs that don't exist but AI commonly generates
|
|
60
|
-
const phantomAPIs = [
|
|
61
|
-
{ pattern: /\bJSON\.tryParse\s*\(/g, name: 'JSON.tryParse()', fix: 'Use JSON.parse() with try/catch' },
|
|
62
|
-
{ pattern: /\bArray\.flatten\s*\(/g, name: 'Array.flatten()', fix: 'Use Array.flat()' },
|
|
63
|
-
{ pattern: /\bconsole\.success\s*\(/g, name: 'console.success()', fix: 'Use console.log()' },
|
|
64
|
-
{ pattern: /\bconsole\.fail\s*\(/g, name: 'console.fail()', fix: 'Use console.error()' },
|
|
65
|
-
{ pattern: /\bfs\.readFileAsync\s*\(/g, name: 'fs.readFileAsync()', fix: 'Use fs.promises.readFile()' },
|
|
66
|
-
{ pattern: /\bfs\.writeFileAsync\s*\(/g, name: 'fs.writeFileAsync()', fix: 'Use fs.promises.writeFile()' },
|
|
67
|
-
{ pattern: /\bfs\.existsAsync\s*\(/g, name: 'fs.existsAsync()', fix: 'Use fs.promises.access()' },
|
|
68
|
-
{ pattern: /\bObject\.deepClone\s*\(/g, name: 'Object.deepClone()', fix: 'Use structuredClone() or JSON.parse(JSON.stringify())' },
|
|
69
|
-
{ pattern: /\bString\.prototype\.contains\s*\(/g, name: 'String.contains()', fix: 'Use String.includes()' },
|
|
70
|
-
{ pattern: /\bPromise\.delay\s*\(/g, name: 'Promise.delay()', fix: 'Use new Promise(r => setTimeout(r, ms))' },
|
|
71
|
-
{ pattern: /\bprocess\.exit\s*\(\s*["'].*["']\s*\)/g, name: 'process.exit(string)', fix: 'process.exit() takes a number, not a string' },
|
|
72
|
-
{ pattern: /\brequire\s*\(\s*['"]node:(?:fetch|axios)['"]s*\)/g, name: 'require("node:fetch")', fix: 'fetch is global in Node 18+, or use node-fetch package' },
|
|
73
|
-
{ pattern: /\.validateSync\s*\(/g, name: '.validateSync()', fix: 'Check if this method exists on the object — AI commonly invents Sync variants' },
|
|
74
|
-
{ pattern: /\bBuffer\.from\s*\(.*,\s*['"]base64url['"]\)/g, name: 'Buffer.from(x, "base64url")', fix: 'Use "base64" encoding, not "base64url"' },
|
|
75
|
-
{ pattern: /\bresponse\.json\s*\(\s*{/g, name: 'response.json({...})', fix: 'res.json() takes data, but check you\'re not confusing fetch Response.json() (no args) with Express res.json()' },
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
// Skip if this looks like a pattern definition file
|
|
79
|
-
const patternDefLines = lines.filter(l => /pattern:\s*\//.test(l)).length;
|
|
80
|
-
if (patternDefLines > 5) return issues;
|
|
81
|
-
|
|
82
|
-
for (const api of phantomAPIs) {
|
|
83
|
-
for (let i = 0; i < lines.length; i++) {
|
|
84
|
-
const line = lines[i];
|
|
85
|
-
// Skip comments and pattern definitions
|
|
86
|
-
if (/^\s*(\/\/|\/\*|\*|#|pattern:|name:|suggestion:|message:)/.test(line)) continue;
|
|
87
|
-
if (/thuban-ignore/.test(line)) continue;
|
|
88
|
-
|
|
89
|
-
if (api.pattern.test(line)) {
|
|
90
|
-
issues.push({
|
|
91
|
-
file: relPath,
|
|
92
|
-
line: i + 1,
|
|
93
|
-
severity: 'critical',
|
|
94
|
-
code: line.trim(),
|
|
95
|
-
message: `${api.name} — this API does not exist`,
|
|
96
|
-
fix: api.fix,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
// Reset regex lastIndex
|
|
100
|
-
api.pattern.lastIndex = 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return issues;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Run the gate check on all staged files
|
|
109
|
-
*/
|
|
110
|
-
run() {
|
|
111
|
-
const startTime = Date.now();
|
|
112
|
-
const stagedFiles = this.getStagedFiles();
|
|
113
|
-
|
|
114
|
-
if (stagedFiles.length === 0) {
|
|
115
|
-
return {
|
|
116
|
-
passed: true,
|
|
117
|
-
files: 0,
|
|
118
|
-
issues: [],
|
|
119
|
-
elapsed: Date.now() - startTime,
|
|
120
|
-
message: 'No staged files to check',
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const allIssues = [];
|
|
125
|
-
for (const file of stagedFiles) {
|
|
126
|
-
if (Date.now() - startTime > this.timeout) break; // Respect timeout
|
|
127
|
-
const fileIssues = this.checkFile(file);
|
|
128
|
-
allIssues.push(...fileIssues);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const criticals = allIssues.filter(i => i.severity === 'critical');
|
|
132
|
-
const passed = this.strict ? criticals.length === 0 : true;
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
passed,
|
|
136
|
-
files: stagedFiles.length,
|
|
137
|
-
issues: allIssues,
|
|
138
|
-
criticals: criticals.length,
|
|
139
|
-
elapsed: Date.now() - startTime,
|
|
140
|
-
message: passed
|
|
141
|
-
? `${stagedFiles.length} files checked. All clear.`
|
|
142
|
-
: `BLOCKED: ${criticals.length} critical issue${criticals.length > 1 ? 's' : ''} found in staged files.`,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Install the git pre-commit hook
|
|
148
|
-
*/
|
|
149
|
-
static install(rootPath = process.cwd()) {
|
|
150
|
-
const gitDir = path.join(rootPath, '.git');
|
|
151
|
-
if (!fs.existsSync(gitDir)) {
|
|
152
|
-
return { success: false, error: 'Not a git repository' };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const hooksDir = path.join(gitDir, 'hooks');
|
|
156
|
-
if (!fs.existsSync(hooksDir)) {
|
|
157
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
161
|
-
const hookContent = `#!/bin/sh
|
|
162
|
-
# Thuban Pre-Commit Gate — blocks commits with hallucinated APIs
|
|
163
|
-
# Remove this file to disable, or run: thuban gate --uninstall
|
|
164
|
-
|
|
165
|
-
npx --yes thuban gate --ci 2>&1
|
|
166
|
-
exit $?
|
|
167
|
-
`;
|
|
168
|
-
|
|
169
|
-
// Check if hook already exists
|
|
170
|
-
if (fs.existsSync(hookPath)) {
|
|
171
|
-
const existing = fs.readFileSync(hookPath, 'utf-8');
|
|
172
|
-
if (existing.includes('thuban gate')) {
|
|
173
|
-
return { success: true, message: 'Thuban gate already installed' };
|
|
174
|
-
}
|
|
175
|
-
// Append to existing hook
|
|
176
|
-
fs.appendFileSync(hookPath, '\n' + hookContent);
|
|
177
|
-
} else {
|
|
178
|
-
fs.writeFileSync(hookPath, hookContent);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Make executable on Unix
|
|
182
|
-
try { fs.chmodSync(hookPath, '755'); } catch (e) { /* Windows */ }
|
|
183
|
-
|
|
184
|
-
return { success: true, message: 'Pre-commit hook installed' };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Uninstall the git pre-commit hook
|
|
189
|
-
*/
|
|
190
|
-
static uninstall(rootPath = process.cwd()) {
|
|
191
|
-
const hookPath = path.join(rootPath, '.git', 'hooks', 'pre-commit');
|
|
192
|
-
if (!fs.existsSync(hookPath)) {
|
|
193
|
-
return { success: true, message: 'No pre-commit hook found' };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
197
|
-
if (content.includes('thuban gate')) {
|
|
198
|
-
// Remove thuban lines
|
|
199
|
-
const cleaned = content
|
|
200
|
-
.split('\n')
|
|
201
|
-
.filter(l => !l.includes('thuban') && !l.includes('Thuban'))
|
|
202
|
-
.join('\n')
|
|
203
|
-
.trim();
|
|
204
|
-
|
|
205
|
-
if (cleaned.length < 10) {
|
|
206
|
-
fs.unlinkSync(hookPath);
|
|
207
|
-
} else {
|
|
208
|
-
fs.writeFileSync(hookPath, cleaned + '\n');
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return { success: true, message: 'Thuban gate uninstalled' };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
module.exports = PreCommitGate;
|