thuban 0.3.3 → 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/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 +12 -7
- package/cli.js +0 -2627
- 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 -758
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -276
- package/packages/scanner/dependency-graph.js +0 -541
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-collector.js +0 -64
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -301
- package/packages/scanner/hallucination-detector.js +0 -822
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -100
- package/packages/scanner/license-manager.js +0 -331
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/monitor-notifier.js +0 -64
- package/packages/scanner/monitor-service.js +0 -103
- package/packages/scanner/monitor-store.js +0 -117
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/scan-diff.js +0 -50
- package/packages/scanner/scan-runner.js +0 -172
- package/packages/scanner/secret-scanner.js +0 -612
- package/packages/scanner/secret-scanner.test.js +0 -103
- package/packages/scanner/sentinel-core.js +0 -616
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -583
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");class ExecutiveReport{constructor(n={}){this.rootPath=n.rootPath||process.cwd(),this.companyName=n.companyName||this._detectProjectName(),this.generatedAt=new Date}_detectProjectName(){try{return JSON.parse(fs.readFileSync(path.join(this.rootPath,"package.json"),"utf-8")).name||path.basename(this.rootPath)}catch{return path.basename(this.rootPath)}}generate(n){const{scanResults:e={},hallResults:t={},debtCost:o={},ghostResults:s={},aiScoreResults:a={},cloneResults:r={},passportData:i={},fileCount:l=0,lineCount:d=0}=n,c=`THB-${Date.now().toString(36).toUpperCase()}`,h=this._calculateOverallScore(n),m=this._scoreToGrade(h),g=this._gradeColor(m);return this._renderHTML({reportId:c,score:h,grade:m,gradeColor:g,scanResults:e,hallResults:t,debtCost:o,ghostResults:s,aiScoreResults:a,cloneResults:r,passportData:i,fileCount:l,lineCount:d})}_calculateOverallScore(n){let e=100;const t=n.hallResults||{},o=n.ghostResults||{},s=n.cloneResults||{},a=n.aiScoreResults||{},r=n.scanResults||{};e-=15*(t.phantomAPIs?.length||0),e-=5*(t.deprecatedAPIs?.length||0),e-=2*(o.totalGhosts||0),e-=3*(s.totalClusters||0),e-=1*(a.aiLikelyCount||0);let i=0;if(r&&"object"==typeof r)for(const[,n]of Object.entries(r))n?.issues&&(i+=n.issues.length);return e-=Math.min(.5*i,30),Math.max(0,Math.min(100,Math.round(e)))}_scoreToGrade(n){return n>=90?"A":n>=80?"B":n>=65?"C":n>=50?"D":"F"}_gradeColor(n){return{A:"#00ff88",B:"#88ff00",C:"#ffd93d",D:"#ff8844",F:"#ff4444"}[n]||"#ff4444"}_renderHTML(n){const e=this.generatedAt,t=e.toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"}),o=e.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}),s=n.hallResults.phantomAPIs?.length||0,a=n.hallResults.deprecatedAPIs?.length||0,r=n.ghostResults.totalGhosts||0,i=n.ghostResults.totalWastedLines||0,l=n.cloneResults.totalClusters||0,d=n.cloneResults.totalDuplicateLines||0,c=n.aiScoreResults.aiLikelyCount||0,h=n.aiScoreResults.aiPossibleCount||0,m=n.aiScoreResults.aiPercentage||0,g=n.debtCost.totalHoursManual||0,v=n.debtCost.totalCostManual||0,u=n.debtCost.totalHoursThuban||0,p=n.debtCost.totalSaving||0,f=n.debtCost.savingPercent||0,b=n.debtCost.weeklyDebtAccumulation||.7,y=Math.round(4*b*80);let x="";if(n.hallResults.phantomAPIs)for(const e of n.hallResults.phantomAPIs.slice(0,8))x+=`<tr>\n <td class="sev-critical">CRITICAL</td>\n <td>${this._esc(e.file||"")}:${e.line||"?"}</td>\n <td>${this._esc(e.name||e.code||"Phantom API")}</td>\n <td>Will crash at runtime — method does not exist</td>\n </tr>`;if(n.hallResults.deprecatedAPIs)for(const e of n.hallResults.deprecatedAPIs.slice(0,5))x+=`<tr>\n <td class="sev-high">HIGH</td>\n <td>${this._esc(e.file||"")}:${e.line||"?"}</td>\n <td>${this._esc(e.name||e.code||"Deprecated API")}</td>\n <td>Deprecated — will break in future versions</td>\n </tr>`;let $="";if(n.ghostResults.ghosts)for(const e of n.ghostResults.ghosts.slice(0,8))$+=`<tr>\n <td>${this._esc(e.file)}:${e.line}</td>\n <td>${this._esc(e.name)}()</td>\n <td>${e.wastedLines} lines</td>\n <td>${e.references} references</td>\n </tr>`;let w="";if(n.aiScoreResults.highConfidence)for(const e of n.aiScoreResults.highConfidence.slice(0,8))w+=`<tr>\n <td class="sev-critical">${e.confidence}%</td>\n <td>${this._esc(e.file)}:${e.line}</td>\n <td>${this._esc(e.name)}()</td>\n <td>${e.lineCount} lines</td>\n </tr>`;let C="";if(n.cloneResults.clusters)for(const e of n.cloneResults.clusters.slice(0,6)){const n=e.members.map(n=>`${this._esc(n.file)}:${n.line}`).join("<br>");C+=`<tr>\n <td>${this._esc(e.pattern)}</td>\n <td>${e.totalInstances}</td>\n <td style="font-size:0.75rem">${n}</td>\n <td>${this._esc(e.recommendation)}</td>\n </tr>`}return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Thuban Code Health Report — ${this._esc(this.companyName)}</title>\n<style>\n @page { size: A4; margin: 15mm; }\n @media print {\n body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\n .no-print { display: none !important; }\n .page-break { page-break-before: always; }\n }\n\n :root {\n --bg: #0a0a1a;\n --surface: #12122a;\n --border: #2a2a5a;\n --text: #e0e0f0;\n --dim: #8888aa;\n --cyan: #00f6ff;\n --green: #00ff88;\n --yellow: #ffd93d;\n --red: #ff4444;\n --mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;\n --sans: 'Segoe UI', system-ui, -apple-system, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 11pt;\n line-height: 1.5;\n max-width: 210mm;\n margin: 0 auto;\n padding: 20mm 15mm;\n }\n\n /* Header */\n .report-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n border-bottom: 2px solid var(--cyan);\n padding-bottom: 1.5rem;\n margin-bottom: 2rem;\n }\n\n .report-header .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .report-header .logo svg { width: 36px; height: 36px; }\n\n .report-header .logo-text {\n font-size: 1.5rem;\n font-weight: 300;\n letter-spacing: 0.3em;\n color: var(--cyan);\n }\n\n .report-header .meta {\n text-align: right;\n font-size: 0.8rem;\n color: var(--dim);\n line-height: 1.8;\n }\n\n .report-title {\n text-align: center;\n margin-bottom: 2.5rem;\n }\n\n .report-title h1 {\n font-size: 1.8rem;\n font-weight: 700;\n margin-bottom: 0.25rem;\n }\n\n .report-title .project-name {\n color: var(--cyan);\n font-size: 1.1rem;\n font-family: var(--mono);\n }\n\n /* Score Ring */\n .score-section {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 3rem;\n margin-bottom: 2.5rem;\n padding: 2rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n }\n\n .score-ring {\n position: relative;\n width: 140px;\n height: 140px;\n }\n\n .score-ring svg { transform: rotate(-90deg); }\n\n .score-ring .score-value {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .score-ring .grade {\n font-size: 2.5rem;\n font-weight: 700;\n font-family: var(--mono);\n line-height: 1;\n }\n\n .score-ring .number {\n font-size: 0.85rem;\n color: var(--dim);\n }\n\n .score-summary {\n flex: 1;\n }\n\n .score-summary h2 {\n font-size: 1.3rem;\n margin-bottom: 0.75rem;\n }\n\n .stat-row {\n display: flex;\n gap: 2rem;\n flex-wrap: wrap;\n margin-top: 1rem;\n }\n\n .stat {\n text-align: center;\n min-width: 80px;\n }\n\n .stat .stat-value {\n font-size: 1.5rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .stat .stat-label {\n font-size: 0.7rem;\n color: var(--dim);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n .stat-critical .stat-value { color: var(--red); }\n .stat-warning .stat-value { color: var(--yellow); }\n .stat-good .stat-value { color: var(--green); }\n .stat-info .stat-value { color: var(--cyan); }\n\n /* Sections */\n .section {\n margin-bottom: 2rem;\n }\n\n .section-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid var(--border);\n }\n\n .section-header h3 {\n font-size: 1rem;\n font-weight: 600;\n }\n\n .section-header .icon { font-size: 1.2rem; }\n\n /* Tables */\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n padding: 0.5rem;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n font-weight: 600;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--dim);\n }\n\n td {\n padding: 0.4rem 0.5rem;\n border-bottom: 1px solid rgba(42,42,90,0.3);\n font-family: var(--mono);\n font-size: 0.75rem;\n word-break: break-all;\n }\n\n .sev-critical { color: var(--red); font-weight: 700; }\n .sev-high { color: var(--yellow); font-weight: 600; }\n\n /* Cost Box */\n .cost-box {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem 2rem;\n margin-bottom: 1rem;\n }\n\n .cost-grid {\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 1.5rem;\n text-align: center;\n }\n\n .cost-item .cost-value {\n font-size: 1.8rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .cost-item .cost-label {\n font-size: 0.75rem;\n color: var(--dim);\n margin-top: 0.25rem;\n }\n\n .cost-manual .cost-value { color: var(--red); }\n .cost-thuban .cost-value { color: var(--cyan); }\n .cost-saving .cost-value { color: var(--green); }\n\n .roi-bar {\n margin-top: 1.5rem;\n padding: 1rem;\n background: rgba(0,255,136,0.08);\n border: 1px solid rgba(0,255,136,0.2);\n border-radius: 8px;\n text-align: center;\n font-size: 0.85rem;\n }\n\n .roi-bar strong { color: var(--green); }\n\n /* Warning callout */\n .callout {\n padding: 1rem 1.5rem;\n border-radius: 8px;\n margin-bottom: 1rem;\n font-size: 0.85rem;\n }\n\n .callout-danger {\n background: rgba(255,68,68,0.08);\n border: 1px solid rgba(255,68,68,0.2);\n color: #ffaaaa;\n }\n\n .callout-warning {\n background: rgba(255,217,61,0.08);\n border: 1px solid rgba(255,217,61,0.2);\n color: #ffe088;\n }\n\n /* Footer */\n .report-footer {\n margin-top: 3rem;\n padding-top: 1.5rem;\n border-top: 1px solid var(--border);\n display: flex;\n justify-content: space-between;\n font-size: 0.75rem;\n color: var(--dim);\n }\n\n .report-footer a { color: var(--cyan); text-decoration: none; }\n\n /* Print button */\n .print-btn {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n background: var(--cyan);\n color: var(--bg);\n border: none;\n padding: 0.75rem 1.5rem;\n border-radius: 8px;\n font-weight: 600;\n font-size: 0.9rem;\n cursor: pointer;\n box-shadow: 0 4px 20px rgba(0,246,255,0.3);\n z-index: 100;\n }\n\n .print-btn:hover { background: #33f8ff; }\n\n /* Responsive for screen viewing */\n @media screen and (max-width: 700px) {\n body { padding: 1rem; }\n .cost-grid { grid-template-columns: 1fr; }\n .stat-row { flex-direction: column; gap: 0.75rem; }\n .score-section { flex-direction: column; gap: 1.5rem; }\n }\n</style>\n</head>\n<body>\n\n<button class="print-btn no-print" onclick="window.print()">Save as PDF</button>\n\n\x3c!-- Header --\x3e\n<div class="report-header">\n <div class="logo">\n <svg viewBox="0 0 32 32" fill="none">\n <polygon points="16,2 30,28 2,28" stroke="#00f6ff" stroke-width="2" fill="none"/>\n <polygon points="16,8 25,25 7,25" stroke="#00f6ff" stroke-width="1" fill="rgba(0,246,255,0.1)"/>\n <circle cx="16" cy="18" r="3" fill="#00f6ff"/>\n </svg>\n <span class="logo-text">THUBAN</span>\n </div>\n <div class="meta">\n Report ID: ${n.reportId}<br>\n Generated: ${t} at ${o}<br>\n Engine: Thuban Code Health v0.3.0\n </div>\n</div>\n\n\x3c!-- Title --\x3e\n<div class="report-title">\n <h1>Code Health Report</h1>\n <div class="project-name">${this._esc(this.companyName)}</div>\n</div>\n\n\x3c!-- Score --\x3e\n<div class="score-section">\n <div class="score-ring">\n <svg width="140" height="140" viewBox="0 0 140 140">\n <circle cx="70" cy="70" r="60" fill="none" stroke="${n.gradeColor}22" stroke-width="10"/>\n <circle cx="70" cy="70" r="60" fill="none" stroke="${n.gradeColor}" stroke-width="10"\n stroke-dasharray="${Math.round(3.77*n.score)} 377"\n stroke-linecap="round"/>\n </svg>\n <div class="score-value">\n <div class="grade" style="color:${n.gradeColor}">${n.grade}</div>\n <div class="number">${n.score}/100</div>\n </div>\n </div>\n <div class="score-summary">\n <h2>Codebase Health: ${"A"===n.grade?"Excellent":"B"===n.grade?"Good":"C"===n.grade?"Needs Attention":"D"===n.grade?"At Risk":"Critical"}</h2>\n <div class="stat-row">\n <div class="stat stat-info">\n <div class="stat-value">${n.fileCount.toLocaleString()}</div>\n <div class="stat-label">Files Scanned</div>\n </div>\n <div class="stat stat-info">\n <div class="stat-value">${n.lineCount.toLocaleString()}</div>\n <div class="stat-label">Lines of Code</div>\n </div>\n <div class="stat ${s>0?"stat-critical":"stat-good"}">\n <div class="stat-value">${s}</div>\n <div class="stat-label">Hallucinated APIs</div>\n </div>\n <div class="stat ${r>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${r}</div>\n <div class="stat-label">Ghost Functions</div>\n </div>\n <div class="stat ${c>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${m}%</div>\n <div class="stat-label">AI-Generated</div>\n </div>\n </div>\n </div>\n</div>\n\n${s>0?`\n<div class="callout callout-danger">\n <strong>PRODUCTION RISK:</strong> ${s} API call${s>1?"s":""} in this codebase reference${1===s?"s":""} methods that do not exist.\n These will throw runtime errors in production. This is not tech debt — this is code that <strong>will crash</strong>.\n</div>\n`:""}\n\n\x3c!-- Tech Debt Cost --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">💰</span>\n <h3>Technical Debt Cost Analysis</h3>\n </div>\n <div class="cost-box">\n <div class="cost-grid">\n <div class="cost-item cost-manual">\n <div class="cost-value">£${v.toLocaleString()}</div>\n <div class="cost-label">Manual fix cost (${g} dev-hours at £80/hr)</div>\n </div>\n <div class="cost-item cost-thuban">\n <div class="cost-value">${u}h</div>\n <div class="cost-label">Fix time with Thuban auto-fix</div>\n </div>\n <div class="cost-item cost-saving">\n <div class="cost-value">£${p.toLocaleString()}</div>\n <div class="cost-label">You save (${f}%)</div>\n </div>\n </div>\n <div class="roi-bar">\n Every week this debt goes unaddressed, <strong>${b} more hours</strong> accumulate.\n That is <strong>£${y.toLocaleString()}/month</strong> in growing technical debt.\n Thuban Pro costs £19/month. <strong>ROI: ${v>0?Math.round(v/19):0}x.</strong>\n </div>\n </div>\n</div>\n\n${s+a>0?`\n\x3c!-- Hallucinations --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">🔮</span>\n <h3>AI Hallucination Detection</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n These API calls reference methods, modules, or endpoints that do not exist in the libraries being used.\n They were likely generated by an AI coding tool and will fail at runtime.\n </p>\n <table>\n <thead>\n <tr><th>Severity</th><th>Location</th><th>API Call</th><th>Impact</th></tr>\n </thead>\n <tbody>${x}</tbody>\n </table>\n</div>\n`:""}\n\n${r>0?`\n<div class="page-break"></div>\n\x3c!-- Ghost Code --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">👻</span>\n <h3>Ghost Code — Dead Functions</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${r} function${r>1?"s":""} exist in the codebase but are never called.\n This represents ${i.toLocaleString()} wasted lines of code (${n.ghostResults.wastedPercent||"?"}% of the codebase).\n </p>\n <table>\n <thead>\n <tr><th>Location</th><th>Function</th><th>Size</th><th>References</th></tr>\n </thead>\n <tbody>${$}</tbody>\n </table>\n</div>\n`:""}\n\n${c>0?`\n\x3c!-- AI Confidence --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">🤖</span>\n <h3>AI-Generated Code Confidence</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${m}% of functions show strong signals of AI generation.\n ${c} high confidence, ${h} possible.\n These functions should be reviewed for correctness, edge cases, and test coverage.\n </p>\n <table>\n <thead>\n <tr><th>AI %</th><th>Location</th><th>Function</th><th>Size</th></tr>\n </thead>\n <tbody>${w}</tbody>\n </table>\n</div>\n`:""}\n\n${l>0?`\n\x3c!-- Copy-Paste Drift --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">📋</span>\n <h3>Copy-Paste Drift</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${l} cluster${l>1?"s":""} of similar code found across ${d} duplicate lines.\n These indicate copy-pasted logic that has drifted apart and should be extracted into shared utilities.\n </p>\n <table>\n <thead>\n <tr><th>Pattern</th><th>Instances</th><th>Locations</th><th>Recommendation</th></tr>\n </thead>\n <tbody>${C}</tbody>\n </table>\n</div>\n`:""}\n\n\x3c!-- Recommendations --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">⚡</span>\n <h3>Recommended Actions</h3>\n </div>\n <div style="display:grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; font-size:0.85rem;">\n ${s>0?`\n <div style="color:var(--red); font-weight:700;">1. IMMEDIATE</div>\n <div>Fix ${s} hallucinated API${s>1?"s":""} — these will crash in production. Run <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban fix . --fix</code></div>\n `:""}\n ${r>0?`\n <div style="color:var(--yellow); font-weight:700;">${s>0?"2":"1"}. HIGH</div>\n <div>Remove ${r} ghost function${r>1?"s":""} (${i} wasted lines). Run <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban ghosts .</code> for full list</div>\n `:""}\n ${l>0?`\n <div style="color:var(--yellow); font-weight:700;">${s>0?"3":"2"}. MEDIUM</div>\n <div>Consolidate ${l} copy-paste cluster${l>1?"s":""} into shared utilities</div>\n `:""}\n ${c>0?`\n <div style="color:var(--cyan); font-weight:700;">${s>0?"4":"3"}. REVIEW</div>\n <div>Audit ${c} high-confidence AI-generated function${c>1?"s":""} for correctness and test coverage</div>\n `:""}\n <div style="color:var(--green); font-weight:700;">ONGOING</div>\n <div>Install pre-commit gate: <code style="background:var(--surface);padding:0.1rem 0.4rem;border-radius:4px;font-family:var(--mono);color:var(--cyan);">thuban gate --fix</code> — blocks hallucinated code from entering your repo</div>\n </div>\n</div>\n\n\x3c!-- Footer --\x3e\n<div class="report-footer">\n <div>\n <strong>Thuban</strong> — Code Health Engine<br>\n <a href="https://thuban.dev">thuban.dev</a> | A Silverwings product\n </div>\n <div style="text-align:right;">\n Report: ${n.reportId}<br>\n This report was generated automatically.<br>\n For full details, run: <code style="font-family:var(--mono); color:var(--cyan);">npx thuban scan .</code>\n </div>\n</div>\n\n</body>\n</html>`}_esc(n){return String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}save(n,e){const t=e||this.rootPath,o=`thuban-report-${this.generatedAt.toISOString().split("T")[0]}.html`,s=path.join(t,o);return fs.writeFileSync(s,n,"utf-8"),s}}module.exports=ExecutiveReport;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path");function collectFiles(e,t=[]){const s=["node_modules",".git","dist","build",".next","__pycache__",".venv","venv","coverage",".cache",".turbo",...t],n=[".js",".ts",".jsx",".tsx",".mjs",".cjs",".py",".pyw",".go",".rs",".java",".kt",".cs",".php",".rb",".json"],i=1048576,c=[],o=new Set;return function e(t){if(c.length>=5e4)return;let r;try{r=fs.readdirSync(t,{withFileTypes:!0})}catch(e){return}for(const a of r){if(c.length>=5e4)return;if(s.some(e=>a.name===e||a.name.match(e)))continue;const r=path.join(t,a.name);if(a.isSymbolicLink())try{const t=fs.realpathSync(r);if(o.has(t))continue;o.add(t);const s=fs.statSync(r);s.isDirectory()?e(r):s.isFile()&&n.some(e=>a.name.endsWith(e))&&s.size>0&&s.size<=i&&c.push(r)}catch{continue}else if(a.isDirectory())e(r);else if(a.isFile()&&n.some(e=>a.name.endsWith(e)))try{const e=fs.statSync(r);e.size>0&&e.size<=i&&c.push(r)}catch(e){}}}(e),c}module.exports={collectFiles:collectFiles};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");class FileWatcher extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),watchPatterns:e.watchPatterns||["**/*.js","**/*.ts","**/*.json"],ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**","build/**",".orion/snapshots/**","coverage/**","*.log"],debounceMs:e.debounceMs||300,...e},this.watchers=[],this.watchedFiles=new Set,this.debounceTimers=new Map,this.isRunning=!1}async start(){if(this.isRunning)return{success:!0,message:"Already watching"};console.log("[FILE-WATCHER] Starting file watcher..."),console.log(`[FILE-WATCHER] Root path: ${this.config.rootPath}`);try{return await this._discoverFiles(this.config.rootPath),this._setupWatchers(),this.isRunning=!0,console.log(`[FILE-WATCHER] Watching ${this.watchedFiles.size} files`),{success:!0,filesWatched:this.watchedFiles.size}}catch(e){return console.error("[FILE-WATCHER] Start failed:",e.message),{success:!1,error:e.message}}}async stop(){console.log("[FILE-WATCHER] Stopping file watcher...");for(const e of this.watchers)try{e.close()}catch(e){}for(const e of this.debounceTimers.values())clearTimeout(e);return this.watchers=[],this.debounceTimers.clear(),this.isRunning=!1,console.log("[FILE-WATCHER] Stopped"),{success:!0}}getWatchedCount(){return this.watchedFiles.size}getWatchedFiles(){return Array.from(this.watchedFiles)}shouldIgnore(e){const t=path.relative(this.config.rootPath,e);for(const e of this.config.ignorePatterns)if(this._matchPattern(t,e))return!0;return!1}shouldWatch(e){const t=path.relative(this.config.rootPath,e);if(this.shouldIgnore(e))return!1;for(const e of this.config.watchPatterns)if(this._matchPattern(t,e))return!0;return!1}async _discoverFiles(e){try{const t=fs.readdirSync(e,{withFileTypes:!0});for(const s of t){const t=path.join(e,s.name);s.isDirectory()?this.shouldIgnore(t)||await this._discoverFiles(t):s.isFile()&&this.shouldWatch(t)&&this.watchedFiles.add(t)}}catch(t){"ENOENT"!==t.code&&"EACCES"!==t.code&&console.warn(`[FILE-WATCHER] Error scanning ${e}:`,t.message)}}_setupWatchers(){const e=new Set;for(const t of this.watchedFiles)e.add(path.dirname(t));for(const t of e)try{const e=fs.watch(t,{persistent:!0},(e,s)=>{if(s){const i=path.join(t,s);this._handleFileEvent(e,i)}});e.on("error",e=>{console.warn(`[FILE-WATCHER] Watcher error for ${t}:`,e.message)}),this.watchers.push(e)}catch(e){console.warn(`[FILE-WATCHER] Could not watch ${t}:`,e.message)}try{const e=fs.watch(this.config.rootPath,{recursive:!0},(e,t)=>{if(t){const s=path.join(this.config.rootPath,t);this._handleFileEvent(e,s)}});e.on("error",e=>{"ERR_FEATURE_UNAVAILABLE_ON_PLATFORM"!==e.code&&console.warn("[FILE-WATCHER] Root watcher error:",e.message)}),this.watchers.push(e)}catch(e){}}_handleFileEvent(e,t){this.shouldIgnore(t)||(this.debounceTimers.has(t)&&clearTimeout(this.debounceTimers.get(t)),this.debounceTimers.set(t,setTimeout(()=>{this.debounceTimers.delete(t),this._processFileEvent(e,t)},this.config.debounceMs)))}_processFileEvent(e,t){const s=fs.existsSync(t),i=s&&fs.statSync(t).isFile();let o;s?i&&this.shouldWatch(t)&&(this.watchedFiles.has(t)?o={type:"change",filePath:t,timestamp:(new Date).toISOString()}:(this.watchedFiles.add(t),o={type:"add",filePath:t,timestamp:(new Date).toISOString()})):this.watchedFiles.has(t)&&(this.watchedFiles.delete(t),o={type:"delete",filePath:t,timestamp:(new Date).toISOString()}),o&&(this.emit(o.type,o),this.emit("any",o))}_matchPattern(e,t){const s=e.replace(/\\/g,"/");let i=t.replace(/\\/g,"/").replace(/\./g,"\\.").replace(/\*\*/g,"<<<GLOBSTAR>>>").replace(/\*/g,"[^/]*").replace(/<<<GLOBSTAR>>>/g,".*").replace(/\?/g,".");i="^"+i+"$";try{return new RegExp(i).test(s)}catch(e){return!1}}}module.exports=FileWatcher;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path");class GhostCodeDetector{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}scan(e){const t=[],s={};for(const n of e){let e;try{if(fs.statSync(n).size>524288)continue;e=fs.readFileSync(n,"utf-8")}catch(e){continue}const i=path.relative(this.rootPath,n);if(this._isSkippable(i))continue;const o=e.split("\n");for(let e=0;e<o.length;e++){const n=o[e],c=this._extractFunction(n,e,o);c&&(c.file=i,c.line=e+1,c.lineCount=this._countFunctionLines(o,e),t.push(c),s[c.name]||(s[c.name]=0))}}for(const n of e){let e;try{e=fs.readFileSync(n,"utf-8")}catch(e){continue}const i=this._stripCommentsAndStrings(e);for(const e of t){if(e.name.length<3)continue;const t=new RegExp(`\\b${this._escapeRegex(e.name)}\\b`,"g"),o=i.match(t);if(o){const t=path.relative(this.rootPath,n)===e.file;s[e.name]+=o.length-(t?1:0)}}}const n=t.filter(e=>(s[e.name]||0)<=0&&!e.isExport&&!e.isLifecycle&&e.lineCount>=5).map(e=>({...e,references:s[e.name]||0,wastedLines:e.lineCount,category:"ghost",severity:e.lineCount>30?"high":"medium",message:`${e.name}() — ${e.lineCount} lines, never called`})).sort((e,t)=>t.wastedLines-e.wastedLines),i=n.reduce((e,t)=>e+t.wastedLines,0),o=e.reduce((e,t)=>{try{return e+fs.readFileSync(t,"utf-8").split("\n").length}catch(t){return e}},0);return{ghosts:n,totalGhosts:n.length,totalWastedLines:i,wastedPercent:o>0?Math.round(i/o*1e3)/10:0,totalLines:o}}_extractFunction(e,t,s){const n=e.trim();let i=n.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);return i?{name:i[1],type:"function",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/),i?{name:i[1],type:"const-function",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/),i&&(n.includes("=>")||t+1<s.length&&s[t+1].includes("=>"))?{name:i[1],type:"arrow",isExport:this._isExported(e,t,s),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),i?{name:i[1],type:"kotlin-fun",isExport:!n.startsWith("private"),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),i?{name:i[1],type:"rust-fn",isExport:/^pub/.test(n),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),i?{name:i[1],type:"php-function",isExport:!/private/.test(n),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),i?{name:i[1],type:"ruby-def",isExport:!i[1].startsWith("_"),isLifecycle:this._isLifecycle(i[1])}:(i=n.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/),i&&!["if","for","while","switch","catch","constructor","render","toString"].includes(i[1])?{name:i[1],type:"method",isExport:!1,isLifecycle:this._isLifecycle(i[1])}:null)))))))}_isExported(e,t,s){return!!/^export\s/.test(e.trim())||!!/module\.exports/.test(e)}_isLifecycle(e){return["constructor","render","componentDidMount","componentWillUnmount","componentDidUpdate","shouldComponentUpdate","getSnapshotBeforeUpdate","getDerivedStateFromProps","useEffect","useState","useMemo","toString","valueOf","toJSON","inspect","setup","teardown","beforeEach","afterEach","beforeAll","afterAll","init","destroy","configure","bootstrap","main","get","set","post","put","delete","patch","handle","onCreate","onStart","onResume","onPause","onStop","onDestroy","onCreateView","onViewCreated","onDestroyView","hashCode","equals","new","default","drop","fmt","from","into","clone","copy","initialize","to_s","to_str","inspect","freeze","dup","__construct","__destruct","__toString","__get","__set","__call"].includes(e)||e.startsWith("_")||e.startsWith("on")}_countFunctionLines(e,t){let s=0,n=!1,i=0;for(let o=t;o<e.length&&o<t+200;o++){const t=e[o];i++;for(const e of t)"{"===e&&(s++,n=!0),"}"===e&&s--;if(n&&s<=0)break}return i}_isSkippable(e){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/spec[\/\\]/i,/__test__/i,/__mocks__/i,/\.config\./i,/\.d\.ts$/,/index\.(js|ts)$/,/server\.(js|ts)$/,/app\.(js|ts)$/,/routes?\.(js|ts)$/,/middleware/i].some(t=>t.test(e))}_escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}_stripCommentsAndStrings(e){return e.replace(/`[^`\\]*(?:\\[\s\S][^`\\]*)*`/g,'""').replace(/"[^"\\\n]*(?:\\.[^"\\\n]*)*"/g,'""').replace(/'[^'\\\n]*(?:\\.[^'\\\n]*)*'/g,"''").replace(/\/\*[\s\S]*?\*\//g," ").replace(/\/\/[^\n]*/g," ").replace(/#[^\n]*/g," ")}}module.exports=GhostCodeDetector;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path");class HallucinationDetector{constructor(e={}){this.config={rootPath:e.rootPath||process.cwd(),...e},this.builtins=new Set(["assert","async_hooks","buffer","child_process","cluster","console","constants","crypto","dgram","diagnostics_channel","dns","domain","events","fs","http","http2","https","inspector","module","net","os","path","perf_hooks","process","punycode","querystring","readline","repl","stream","string_decoder","sys","timers","tls","trace_events","tty","url","util","v8","vm","wasi","worker_threads","zlib","node:fs","node:path","node:os","node:crypto","node:http","node:https","node:events","node:util","node:stream","node:child_process","node:url","node:querystring","node:buffer","node:net","node:tls","node:dns","node:readline","node:zlib","node:worker_threads","node:cluster","node:vm","node:assert","node:timers","node:timers/promises","node:test","fs/promises","stream/promises","timers/promises","dns/promises","readline/promises"]),this.phantomPackages=new Set(["node-fetch-extra","express-async","mongoose-v2","react-native-web-view","axios-retry-enhanced","lodash-extended","moment-timezone-v2","socket.io-extra","graphql-tools-v2","next-auth-v5","prisma-client-v2","tailwind-utils","react-query-v4","jest-extended-v2"]),this.phantomAPIs=[{pattern:/fs\.readFileAsync\s*\(/,suggestion:"Use fs.promises.readFile() or fs.readFileSync()",name:"fs.readFileAsync",id:"HALL_API001"},{pattern:/fs\.writeFileAsync\s*\(/,suggestion:"Use fs.promises.writeFile() or fs.writeFileSync()",name:"fs.writeFileAsync",id:"HALL_API002"},{pattern:/fs\.existsAsync\s*\(/,suggestion:"Use fs.promises.access() or fs.existsSync()",name:"fs.existsAsync",id:"HALL_API003"},{pattern:/path\.exists\s*\(/,suggestion:"Use fs.existsSync() — path module has no exists()",name:"path.exists",id:"HALL_API004"},{pattern:/path\.isFile\s*\(/,suggestion:"Use fs.statSync().isFile() — path module has no isFile()",name:"path.isFile",id:"HALL_API005"},{pattern:/path\.isDirectory\s*\(/,suggestion:"Use fs.statSync().isDirectory()",name:"path.isDirectory",id:"HALL_API006"},{pattern:/console\.success\s*\(/,suggestion:"Use console.log() — console.success() does not exist",name:"console.success",id:"HALL_API007"},{pattern:/console\.verbose\s*\(/,suggestion:"Use console.log() or console.debug()",name:"console.verbose",id:"HALL_API008"},{pattern:/JSON\.tryParse\s*\(/,suggestion:"Use try/catch with JSON.parse()",name:"JSON.tryParse",id:"HALL_API009"},{pattern:/JSON\.safeStringify\s*\(/,suggestion:"Use JSON.stringify() with a replacer",name:"JSON.safeStringify",id:"HALL_API010"},{pattern:/Array\.flatten\s*\(/,suggestion:"Use Array.prototype.flat()",name:"Array.flatten",id:"HALL_API011"},{pattern:/Object\.deepClone\s*\(/,suggestion:"Use structuredClone() or JSON.parse(JSON.stringify())",name:"Object.deepClone",id:"HALL_API012"},{pattern:/Object\.deepMerge\s*\(/,suggestion:"Use a library like lodash.merge or write a custom function",name:"Object.deepMerge",id:"HALL_API013"},{pattern:/String\.prototype\.replaceAll\s*&&.*polyfill/i,suggestion:"replaceAll() is native since ES2021 — no polyfill needed",name:"replaceAll polyfill",id:"HALL_API014"},{pattern:/require\s*\(\s*['"]fs\/sync['"]\s*\)/,suggestion:"fs/sync is not a real module — use fs directly",name:"fs/sync",id:"HALL_API015"},{pattern:/require\s*\(\s*['"]http\/server['"]\s*\)/,suggestion:"http/server is not a real module — use http.createServer()",name:"http/server",id:"HALL_API016"},{pattern:/process\.env\.get\s*\(/,suggestion:"Use process.env.VAR_NAME — process.env is a plain object, not a Map",name:"process.env.get()",id:"HALL_API017"},{pattern:/process\.exit\s*\(\s*['"]/,suggestion:"process.exit() takes a number, not a string",name:"process.exit(string)",id:"HALL_API018"}],this.pythonPhantomAPIs=[{pattern:/os\.path\.exists_sync\s*\(/,suggestion:"Use os.path.exists() — exists_sync does not exist in Python",name:"os.path.exists_sync",id:"PY_HALL001"},{pattern:/json\.tryParse\s*\(/,suggestion:"Use json.loads() with try/except",name:"json.tryParse",id:"PY_HALL002"},{pattern:/\w+\.flatMap\s*\(/,suggestion:"Python lists have no flatMap — use list comprehension or itertools.chain.from_iterable()",name:"list.flatMap",id:"PY_HALL003"},{pattern:/\w+\.merge\s*\((?!.*\bself\b)/,suggestion:"Use {**dict1, **dict2} or dict1 | dict2 (3.9+)",name:"dict.merge",id:"PY_HALL004"},{pattern:/\w+\.format_map\s*\(/,suggestion:"str.format_map exists but is rarely correct — did you mean .format()?",name:"string.format_map misuse",id:"PY_HALL005"},{pattern:/from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i,suggestion:"Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary",name:"Unnecessary OrderedDict",id:"PY_HALL006"},{pattern:/async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i,suggestion:"asyncio.sleep(0) to yield is a code smell — review async design",name:"asyncio.sleep(0) hack",id:"PY_HALL007"},{pattern:/import\s+tensorflow\.v2/,suggestion:"tensorflow.v2 is not a real module — use import tensorflow",name:"tensorflow.v2",id:"PY_HALL008"},{pattern:/from\s+sklearn\.model_selection\s+import\s+train_test_split_v2/,suggestion:"train_test_split_v2 does not exist — use train_test_split",name:"sklearn v2 hallucination",id:"PY_HALL009"},{pattern:/requests\.async_get\s*\(/,suggestion:"requests has no async_get — use aiohttp or httpx",name:"requests.async_get",id:"PY_HALL010"}],this.pythonDeprecated=[{pattern:/from\s+distutils/,suggestion:"distutils removed in Python 3.12 — use setuptools",since:"Python 3.12",name:"distutils",id:"PY_DEPR001"},{pattern:/from\s+imp\s+import/,suggestion:"imp removed in Python 3.12 — use importlib",since:"Python 3.12",name:"imp module",id:"PY_DEPR002"},{pattern:/asyncio\.get_event_loop\(\)/,suggestion:"Deprecated in 3.10+ — use asyncio.run() or get_running_loop()",since:"Python 3.10",name:"asyncio.get_event_loop()",id:"PY_DEPR003"},{pattern:/collections\.MutableMapping/,suggestion:"Moved to collections.abc.MutableMapping in Python 3.3+",since:"Python 3.9",name:"collections.MutableMapping",id:"PY_DEPR004"},{pattern:/optparse\.OptionParser/,suggestion:"optparse deprecated since Python 3.2 — use argparse",since:"Python 3.2",name:"optparse",id:"PY_DEPR005"},{pattern:/cgi\.parse_header\s*\(/,suggestion:"cgi module deprecated in Python 3.11, removed in 3.13",since:"Python 3.11",name:"cgi module",id:"PY_DEPR006"},{pattern:/from\s+typing\s+import\s+(List|Dict|Tuple|Set)\b/,suggestion:"Use built-in list, dict, tuple, set for type hints (Python 3.9+)",since:"Python 3.9",name:"typing.List/Dict/Tuple",id:"PY_DEPR007"},{pattern:/unittest\.makeSuite\s*\(/,suggestion:"makeSuite deprecated — use TestLoader.loadTestsFromTestCase",since:"Python 3.11",name:"unittest.makeSuite",id:"PY_DEPR008"}],this.pythonSmells=[{pattern:/print\s*\(\s*f?['"]\s*debug/i,id:"PY_SMELL001",name:"Debug Print",message:"Debug print statement left in code"},{pattern:/except\s*:\s*$|except\s+Exception\s*:\s*\n\s*pass/m,id:"PY_SMELL002",name:"Bare Except",message:"Bare except or swallowed exception — hides real errors"},{pattern:/#\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"PY_SMELL003",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/raise\s+NotImplementedError\s*\(?\s*\)?/,id:"PY_SMELL004",name:"Not Implemented",message:"Stub implementation — function body was not generated"},{pattern:/['"]your.api.key.here['"]|['"]sk-[.]{3,}['"]/i,id:"PY_SMELL005",name:"Dummy Credential",message:"Placeholder credential — will fail at runtime"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"PY_SMELL006",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/import\s+\*/,id:"PY_SMELL007",name:"Wildcard Import",message:"Wildcard import — pollutes namespace, hides dependencies"}],this.goDeprecated=[{pattern:/ioutil\.ReadFile\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.ReadFile()",since:"Go 1.16",name:"ioutil.ReadFile",id:"GO_DEPR001"},{pattern:/ioutil\.WriteFile\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.WriteFile()",since:"Go 1.16",name:"ioutil.WriteFile",id:"GO_DEPR002"},{pattern:/ioutil\.TempDir\s*\(/,suggestion:"ioutil removed in Go 1.16 — use os.MkdirTemp()",since:"Go 1.16",name:"ioutil.TempDir",id:"GO_DEPR003"},{pattern:/ioutil\.ReadAll\s*\(/,suggestion:"ioutil removed in Go 1.16 — use io.ReadAll()",since:"Go 1.16",name:"ioutil.ReadAll",id:"GO_DEPR004"},{pattern:/strings\.Title\s*\(/,suggestion:"strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text",since:"Go 1.18",name:"strings.Title",id:"GO_DEPR005"}],this.goSmells=[{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"GO_SMELL001",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/panic\s*\(\s*["']not implemented["']\s*\)/,id:"GO_SMELL002",name:"Panic Not Implemented",message:"Stub implementation — will panic at runtime"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"GO_SMELL003",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.rustPhantomAPIs=[{pattern:/std::fs::File::open_async\s*\(/,suggestion:"Use tokio::fs::File::open() for async file I/O",name:"File::open_async",id:"RS_HALL001"},{pattern:/Vec::flatten\s*\(/,suggestion:"Use .into_iter().flatten() — Vec has no flatten() method",name:"Vec::flatten",id:"RS_HALL002"},{pattern:/HashMap::get_or_default\s*\(/,suggestion:"Use .get().unwrap_or(&default) or entry().or_insert()",name:"HashMap::get_or_default",id:"RS_HALL003"},{pattern:/String::from_utf8_lossy_owned\s*\(/,suggestion:"Use String::from_utf8_lossy().into_owned()",name:"String::from_utf8_lossy_owned",id:"RS_HALL004"},{pattern:/\.async_iter\s*\(/,suggestion:"Use futures::stream::iter() from the futures crate",name:".async_iter()",id:"RS_HALL005"},{pattern:/std::net::TcpStream::connect_async\s*\(/,suggestion:"Use tokio::net::TcpStream::connect()",name:"TcpStream::connect_async",id:"RS_HALL006"},{pattern:/std::thread::sleep_async\s*\(/,suggestion:"Use tokio::time::sleep() for async sleep",name:"thread::sleep_async",id:"RS_HALL007"},{pattern:/Vec::remove_all\s*\(/,suggestion:"Use .retain(|x| condition) or .clear()",name:"Vec::remove_all",id:"RS_HALL008"},{pattern:/str::split_whitespace_n\s*\(/,suggestion:"Use .splitn(n, char::is_whitespace) instead",name:"str::split_whitespace_n",id:"RS_HALL009"},{pattern:/\.map_async\s*\(/,suggestion:"Use futures::future::join_all() or tokio::task::spawn",name:".map_async()",id:"RS_HALL010"}],this.rustDeprecated=[{pattern:/std::sync::ONCE_INIT/,suggestion:"std::sync::ONCE_INIT removed — use Once::new()",since:"Rust 1.38",name:"ONCE_INIT",id:"RS_DEPR001"},{pattern:/\bstd::mem::uninitialized\s*\(/,suggestion:"std::mem::uninitialized() removed — use MaybeUninit::uninit()",since:"Rust 1.39",name:"mem::uninitialized",id:"RS_DEPR002"},{pattern:/\btry!\s*\(/,suggestion:"try!() macro removed — use the ? operator",since:"Rust 2018",name:"try!() macro",id:"RS_DEPR003"},{pattern:/extern\s+crate\s+std\s*;/,suggestion:"extern crate std is implicit since Rust 2018 edition",since:"Rust 2018",name:"extern crate std",id:"RS_DEPR004"},{pattern:/extern\s+crate\s+alloc\s*;/,suggestion:"extern crate alloc is implicit in 2018+ edition",since:"Rust 2018",name:"extern crate alloc",id:"RS_DEPR005"},{pattern:/std::error::Error::cause\s*\(/,suggestion:".cause() deprecated — use .source() instead",since:"Rust 1.33",name:"Error::cause()",id:"RS_DEPR006"}],this.rustSmells=[{pattern:/todo!\s*\(\s*\)/,id:"RS_SMELL001",name:"todo! macro",message:"todo!() macro — will panic at runtime"},{pattern:/unimplemented!\s*\(\s*\)/,id:"RS_SMELL002",name:"unimplemented! macro",message:"unimplemented!() macro — will panic at runtime"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"RS_SMELL003",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/unwrap\(\).*unwrap\(\)/,id:"RS_SMELL004",name:"Double Unwrap",message:"Chained unwrap() — will panic on None/Err"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"RS_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/unsafe\s*\{/,id:"RS_SMELL006",name:"Unsafe Block",message:"unsafe block — verify memory safety guarantees"},{pattern:/\.unwrap\(\)\s*;/,id:"RS_SMELL007",name:"Bare unwrap()",message:".unwrap() will panic on None/Err — use ? or match"},{pattern:/println!\s*\(\s*["'].*[Dd]ebug/,id:"RS_SMELL008",name:"Debug println!",message:"Debug println! left in code — use the log crate"},{pattern:/eprintln!\s*\(\s*["'].*[Dd]ebug/,id:"RS_SMELL009",name:"Debug eprintln!",message:"Debug eprintln! left in code — use the log crate"},{pattern:/#\[allow\(dead_code\)\]/,id:"RS_SMELL010",name:"Suppressed Dead Code",message:"#[allow(dead_code)] suppresses warnings — remove unused code"}],this.javaSmells=[{pattern:/System\.out\.println\s*\(.*debug/i,id:"JAVA_SMELL001",name:"Debug Println",message:"Debug System.out.println — use a logger"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"JAVA_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/throw\s+new\s+(?:Runtime)?Exception\s*\(\s*["']Not\s+implemented["']/i,id:"JAVA_SMELL003",name:"Not Implemented",message:"Stub implementation — will throw at runtime"},{pattern:/catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/,id:"JAVA_SMELL004",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"JAVA_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.csharpSmells=[{pattern:/Console\.WriteLine\s*\(.*[Dd]ebug/i,id:"CS_SMELL001",name:"Debug WriteLine",message:"Debug Console.WriteLine — use ILogger or Debug.Write"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"CS_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/throw\s+new\s+(?:NotImplementedException)\s*\(/,id:"CS_SMELL003",name:"Not Implemented",message:"Stub implementation — will throw at runtime"},{pattern:/catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/,id:"CS_SMELL004",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"CS_SMELL005",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.kotlinPhantomAPIs=[{pattern:/\.flatMap\s*\{[^}]*\}(?!.*asSequence)/,suggestion:"Use .flatMap { } — verify this is not confusing sequence vs list behavior",name:"flatMap confusion",id:"KT_HALL001"},{pattern:/listOf\(\)\.stream\s*\(/,suggestion:"Kotlin lists have no .stream() — use .asSequence() or direct collection ops",name:"listOf().stream()",id:"KT_HALL002"},{pattern:/String\.format\s*\(/,suggestion:'Prefer Kotlin string templates "$variable" over String.format()',name:"String.format Java-style",id:"KT_HALL003"},{pattern:/\bObject\s*\(\s*\)/,suggestion:"Kotlin uses Any() not Object() — or use object keyword for singletons",name:"Object() Java-style",id:"KT_HALL004"},{pattern:/\.forEach\s*\(\s*::println\s*\)(?!.*debug)/i,suggestion:"println is for debug output — use a logger",name:"println via forEach",id:"KT_HALL005"},{pattern:/coroutineScope\s*\{\s*launch\s*\{[^}]*\}\s*\}(?!\s*\.join)/,suggestion:"Unjoined coroutine in coroutineScope may cause issues — ensure structured concurrency",name:"Unawaited launch",id:"KT_HALL006"},{pattern:/Dispatchers\.IO\s*\+\s*Dispatchers/,suggestion:"Combining Dispatchers is not valid — use one dispatcher at a time",name:"Combined Dispatchers",id:"KT_HALL007"},{pattern:/\.toList\(\)\.stream\(\)/,suggestion:"Use Kotlin sequences (.asSequence()) instead of .toList().stream()",name:"toList().stream()",id:"KT_HALL008"},{pattern:/companion object.*getInstance/i,suggestion:"Kotlin idiom for singletons is object keyword, not companion object + getInstance()",name:"Java-style singleton",id:"KT_HALL009"},{pattern:/lateinit var.*\?/,suggestion:"lateinit properties cannot be nullable — remove ? or use by lazy {}",name:"lateinit nullable",id:"KT_HALL010"}],this.kotlinDeprecated=[{pattern:/\bapply\s+plugin:\s*['"]kotlin-android-extensions['"]/,suggestion:"kotlin-android-extensions deprecated — use view binding or Jetpack ViewBinding",since:"Kotlin 1.8",name:"kotlin-android-extensions",id:"KT_DEPR001"},{pattern:/\bkotlinx\.android\.synthetic/,suggestion:"Synthetic imports deprecated — use view binding or findViewById",since:"Kotlin 1.8",name:"Synthetic imports",id:"KT_DEPR002"},{pattern:/\bCoroutineScope\(EmptyCoroutineContext\)/,suggestion:"EmptyCoroutineContext coroutine scope is error-prone — use viewModelScope or lifecycleScope",since:"Kotlin Coroutines 1.6",name:"EmptyCoroutineContext scope",id:"KT_DEPR003"},{pattern:/\bBuildersKt\.launch\b/,suggestion:"Internal BuildersKt APIs are deprecated — use coroutineScope { launch {} }",since:"Kotlin Coroutines 1.5",name:"BuildersKt.launch",id:"KT_DEPR004"},{pattern:/\basyncLazy\s*\{/,suggestion:"asyncLazy {} is not a standard Kotlin API — use lazy {} or async {}",since:"Kotlin 1.6",name:"asyncLazy",id:"KT_DEPR005"}],this.kotlinSmells=[{pattern:/\bprintln\s*\(/,id:"KT_SMELL001",name:"Debug println",message:"println() is for debug output — use a proper logger"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"KT_SMELL002",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/TODO\s*\(\s*\)/,id:"KT_SMELL003",name:"TODO() stub",message:"TODO() will throw NotImplementedError at runtime"},{pattern:/!!\s*\./,id:"KT_SMELL004",name:"Non-null assertion chain",message:"!! non-null assertion — will throw NullPointerException if null"},{pattern:/\bas\s+\w+\b(?!.*\?)/,suggestion:"Unsafe cast — prefer safe cast (as? Type) with null check",id:"KT_SMELL005",name:"Unsafe cast",message:"Unsafe cast with as — will throw ClassCastException if wrong type"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"KT_SMELL006",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/catch\s*\(\s*e:\s*Exception\s*\)\s*\{\s*\}/,id:"KT_SMELL007",name:"Empty Catch",message:"Empty catch block — swallows all exceptions"},{pattern:/throw\s+NotImplementedError\s*\(/,id:"KT_SMELL008",name:"Not Implemented",message:"Stub implementation — will throw at runtime"}],this.phpPhantomAPIs=[{pattern:/array_flatten\s*\(/,suggestion:"PHP has no array_flatten() — use array_merge(...$array) or a recursive function",name:"array_flatten",id:"PHP_HALL001"},{pattern:/str_contains_all\s*\(/,suggestion:"PHP has no str_contains_all() — use multiple str_contains() calls",name:"str_contains_all",id:"PHP_HALL002"},{pattern:/array_unique_values\s*\(/,suggestion:"PHP has no array_unique_values() — use array_values(array_unique())",name:"array_unique_values",id:"PHP_HALL003"},{pattern:/\$pdo->fetchAll\s*\(/,suggestion:"fetchAll() is on PDOStatement not PDO — call $stmt->fetchAll()",name:"PDO::fetchAll",id:"PHP_HALL004"},{pattern:/json_decode_safe\s*\(/,suggestion:"PHP has no json_decode_safe() — wrap json_decode() with json_last_error() check",name:"json_decode_safe",id:"PHP_HALL005"},{pattern:/str_replace_all\s*\(/,suggestion:"PHP has no str_replace_all() — use str_replace() which already replaces all occurrences",name:"str_replace_all",id:"PHP_HALL006"},{pattern:/array_map_keys\s*\(/,suggestion:"PHP has no array_map_keys() — use array_combine(array_map(...), array_keys())",name:"array_map_keys",id:"PHP_HALL007"},{pattern:/\$request->getJson\s*\(/,suggestion:'Not a standard PHP function — use json_decode(file_get_contents("php://input"))',name:"$request->getJson",id:"PHP_HALL008"},{pattern:/Date::now\s*\(/,suggestion:"PHP has no Date::now() — use new DateTime() or time()",name:"Date::now",id:"PHP_HALL009"},{pattern:/\bawait\s+/,suggestion:"PHP has no await keyword — use synchronous code or ReactPHP for async",name:"await keyword",id:"PHP_HALL010"}],this.phpDeprecated=[{pattern:/\bmysql_connect\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_connect",id:"PHP_DEPR001"},{pattern:/\bmysql_query\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_query",id:"PHP_DEPR002"},{pattern:/\bmysql_fetch_array\s*\(/,suggestion:"mysql_* functions removed in PHP 7 — use PDO or MySQLi",since:"PHP 7.0",name:"mysql_fetch_array",id:"PHP_DEPR003"},{pattern:/\bereg\s*\(/,suggestion:"ereg() removed in PHP 7 — use preg_match()",since:"PHP 7.0",name:"ereg()",id:"PHP_DEPR004"},{pattern:/\bsplit\s*\(/,suggestion:"split() removed in PHP 7 — use preg_split() or explode()",since:"PHP 7.0",name:"split()",id:"PHP_DEPR005"},{pattern:/\bcreate_function\s*\(/,suggestion:"create_function() removed in PHP 8 — use anonymous functions (closures)",since:"PHP 8.0",name:"create_function",id:"PHP_DEPR006"},{pattern:/\bmagic_quotes_gpc\b/,suggestion:"magic_quotes_gpc removed in PHP 7 — sanitize inputs manually",since:"PHP 7.0",name:"magic_quotes_gpc",id:"PHP_DEPR007"},{pattern:/\beach\s*\(/,suggestion:"each() deprecated in PHP 7.2, removed in PHP 8 — use foreach",since:"PHP 8.0",name:"each()",id:"PHP_DEPR008"},{pattern:/\bmcrypt_/,suggestion:"mcrypt removed in PHP 7.2 — use OpenSSL functions",since:"PHP 7.2",name:"mcrypt functions",id:"PHP_DEPR009"}],this.phpSmells=[{pattern:/\beval\s*\(/,id:"PHP_SMELL001",name:"eval() Usage",message:"eval() is dangerous — code injection risk"},{pattern:/\bextract\s*\(\s*\$_(?:GET|POST|REQUEST)/,id:"PHP_SMELL002",name:"extract() from superglobal",message:"extract($_GET/_POST) — variable injection vulnerability"},{pattern:/\$\$\w+/,id:"PHP_SMELL003",name:"Variable variable",message:"Variable variables ($$var) — hard to trace and injection risk"},{pattern:/\bshell_exec\s*\(/,id:"PHP_SMELL004",name:"shell_exec()",message:"shell_exec() — remote code execution risk if input unsanitized"},{pattern:/\bsystem\s*\(/,id:"PHP_SMELL005",name:"system()",message:"system() — remote code execution risk if input unsanitized"},{pattern:/\bpassthru\s*\(/,id:"PHP_SMELL006",name:"passthru()",message:"passthru() — remote code execution risk if input unsanitized"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"PHP_SMELL007",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"PHP_SMELL008",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"},{pattern:/\$_(?:GET|POST|REQUEST)\[.*\].*SELECT.*\./i,id:"PHP_SMELL009",name:"SQL Injection Risk",message:"User input directly in SQL string — use prepared statements"},{pattern:/\bvar_dump\s*\(/,id:"PHP_SMELL010",name:"Debug var_dump",message:"var_dump() is for debug only — remove before production"}],this.rubyPhantomAPIs=[{pattern:/\.flatten_map\s*\{/,suggestion:"Ruby has no .flatten_map — use .flat_map { }",name:".flatten_map",id:"RB_HALL001"},{pattern:/Array\.of\s*\(/,suggestion:"Ruby has no Array.of() — use Array() or []",name:"Array.of()",id:"RB_HALL002"},{pattern:/Hash\.from_array\s*\(/,suggestion:"Ruby has no Hash.from_array() — use array.to_h or Hash[array]",name:"Hash.from_array",id:"RB_HALL003"},{pattern:/\.async\s*\{/,suggestion:"Ruby has no .async {} — use Thread.new or concurrent-ruby gem",name:".async block",id:"RB_HALL004"},{pattern:/String\.format\s*\(/,suggestion:"Ruby has no String.format() — use string interpolation or % operator",name:"String.format",id:"RB_HALL005"},{pattern:/\bpromise\s*\{/,suggestion:"Ruby has no built-in promise {} — use concurrent-ruby gem or Thread",name:"promise block",id:"RB_HALL006"},{pattern:/\.try!\s*\(/,suggestion:"Ruby has no .try!() — use &. (safe navigation) or Rails .try()",name:".try!()",id:"RB_HALL007"},{pattern:/JSON\.safe_parse\s*\(/,suggestion:"Ruby has no JSON.safe_parse — use JSON.parse with rescue",name:"JSON.safe_parse",id:"RB_HALL008"},{pattern:/ActiveRecord::Base\.execute\s*\(/,suggestion:"ActiveRecord::Base has no .execute() — use connection.execute() or where()",name:"Base.execute",id:"RB_HALL009"},{pattern:/\.collect_map\s*\{/,suggestion:"Ruby has no .collect_map — use .map { }.flatten(1) or .flat_map",name:".collect_map",id:"RB_HALL010"}],this.rubyDeprecated=[{pattern:/\brequire\s+['"]thread['"]/,suggestion:'require "thread" is deprecated — Thread is now built-in without require',since:"Ruby 2.0",name:'require "thread"',id:"RB_DEPR001"},{pattern:/\bObject#type\b|\.type\s*==/,suggestion:".type is deprecated — use .class or .is_a?",since:"Ruby 1.8",name:"Object#type",id:"RB_DEPR002"},{pattern:/\bERB::Util\.html_escape\b/,suggestion:"ERB::Util.html_escape deprecated — use CGI.escapeHTML or h() in views",since:"Ruby 2.6",name:"ERB::Util.html_escape",id:"RB_DEPR003"},{pattern:/\bFile\.exists\?\s*\(/,suggestion:"File.exists? deprecated — use File.exist? (no s)",since:"Ruby 2.2",name:"File.exists?",id:"RB_DEPR004"},{pattern:/\bDir\.exists\?\s*\(/,suggestion:"Dir.exists? deprecated — use Dir.exist? (no s)",since:"Ruby 2.2",name:"Dir.exists?",id:"RB_DEPR005"},{pattern:/\bObject#returning\b|\s+returning\s+/,suggestion:"Object#returning removed from Rails core — use tap { |obj| }",since:"Rails 3.0",name:"Object#returning",id:"RB_DEPR006"}],this.rubySmells=[{pattern:/\bputs\s+(?!STDOUT)/,id:"RB_SMELL001",name:"Debug puts",message:"puts is for debug output — use Rails logger or a logging library"},{pattern:/\bp\s+\w/,id:"RB_SMELL002",name:"Debug p()",message:"p() is for debug output — remove before production"},{pattern:/\beval\s*\(/,id:"RB_SMELL003",name:"eval() Usage",message:"eval() is dangerous — code injection risk"},{pattern:/\bsystem\s*\(/,id:"RB_SMELL004",name:"system() Shell Call",message:"system() call — command injection risk if input unsanitized"},{pattern:/`[^`]+`/,id:"RB_SMELL005",name:"Backtick Shell Exec",message:"Backtick shell execution — command injection risk"},{pattern:/\bexec\s*\(/,id:"RB_SMELL006",name:"exec() call",message:"exec() replaces the process — ensure this is intentional"},{pattern:/\brescue\s*$|\brescue\s+Exception\b/,id:"RB_SMELL007",name:"Bare rescue",message:"Bare rescue catches all exceptions including fatal ones — be specific"},{pattern:/["'].*#\{.*sql.*\}.*["']/i,id:"RB_SMELL008",name:"SQL String Interpolation",message:"SQL built with string interpolation — use parameterized queries"},{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i,id:"RB_SMELL009",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished"},{pattern:/raise\s+NotImplementedError/,id:"RB_SMELL010",name:"Not Implemented",message:"Stub implementation — will raise at runtime"}],this.jsExtensions=new Set([".js",".ts",".jsx",".tsx",".mjs",".cjs"]),this.pyExtensions=new Set([".py",".pyw"]),this.goExtensions=new Set([".go"]),this.rustExtensions=new Set([".rs"]),this.javaExtensions=new Set([".java",".kt"]),this.csharpExtensions=new Set([".cs"]),this.phpExtensions=new Set([".php"]),this.rubyExtensions=new Set([".rb"]),this.aiCodeSmells=[{pattern:/\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)\s*(this|here|later)/i,id:"AI_SMELL001",name:"Placeholder TODO",message:"AI-generated placeholder — likely unfinished implementation"},{pattern:/throw new Error\(['"]Not implemented['"]\)/,id:"AI_SMELL002",name:"Not Implemented",message:"Stub implementation — function body was not generated"},{pattern:/\/\/\s*\.\.\.\s*(rest|remaining|more|other|additional)/i,id:"AI_SMELL003",name:"Truncated Code",message:"AI output was truncated — code is incomplete"},{pattern:/\/\/\s*(your|replace|insert|put)\s+(code|logic|implementation|api[_\s]?key)/i,id:"AI_SMELL004",name:"Template Placeholder",message:"Template placeholder left in code — needs real implementation"},{pattern:/['"]your-api-key-here['"]|['"]sk-[.]{3,}['"]|['"]xxx+['"]/i,id:"AI_SMELL005",name:"Dummy Credential",message:"Placeholder credential — will fail at runtime"},{pattern:/example\.com|test@test\.com|john@doe\.com|foo@bar\.com/i,id:"AI_SMELL006",name:"Example Domain",message:"Example domain/email in production code"},{pattern:/localhost:\d{4}(?!.*(?:dev|test|local|development))/i,id:"AI_SMELL007",name:"Hardcoded Localhost",message:"Hardcoded localhost URL — will fail in production"}],this.deprecatedAPIs=[{pattern:/new Buffer\s*\(/,suggestion:"Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()",since:"Node 6",name:"new Buffer()",id:"DEPR_API001"},{pattern:/require\s*\(\s*['"]sys['"]\s*\)/,suggestion:'Use require("util") — sys was removed in Node 1.0',since:"Node 1.0",name:'require("sys")',id:"DEPR_API002"},{pattern:/fs\.exists\s*\((?!Sync)/,suggestion:"Use fs.access() or fs.stat() — fs.exists() is deprecated",since:"Node 1.0",name:"fs.exists()",id:"DEPR_API003"},{pattern:/domain\.create\s*\(/,suggestion:"Use async_hooks or try/catch — domain module is deprecated",since:"Node 4",name:"domain.create()",id:"DEPR_API004",fixable:!1},{pattern:/require\s*\(\s*['"]punycode['"]\s*\)/,suggestion:"Use userland punycode package — built-in is deprecated",since:"Node 7",name:'require("punycode")',id:"DEPR_API005",fixable:!1},{pattern:/url\.parse\s*\(/,suggestion:"Use new URL() — url.parse() is legacy",since:"Node 11",name:"url.parse()",id:"DEPR_API006"},{pattern:/querystring\.(parse|stringify)\s*\(/,suggestion:"Use URLSearchParams — querystring is legacy",since:"Node 14",name:"querystring",id:"DEPR_API007",fixable:!1},{pattern:/util\.isArray\s*\(/,suggestion:"Use Array.isArray() — util type checks are deprecated",since:"Node 4",name:"util.isArray()",id:"DEPR_API008"},{pattern:/util\.isFunction\s*\(/,suggestion:'Use typeof fn === "function"',since:"Node 4",name:"util.isFunction()",id:"DEPR_API009"},{pattern:/util\.pump\s*\(/,suggestion:"Use stream.pipeline() — util.pump() was removed",since:"Node 1.0",name:"util.pump()",id:"DEPR_API010",fixable:!1}]}async scan(e){const t=Date.now(),s={phantomImports:[],phantomAPIs:[],aiSmells:[],deprecatedAPIs:[],unresolvedDeps:[],stats:{filesScanned:0,totalIssues:0,scanTime:0}},n=this._loadDeclaredDeps(),i=this._loadIgnoreRules();for(const t of e)try{const e=path.extname(t).toLowerCase(),a=this.jsExtensions.has(e),o=this.pyExtensions.has(e),r=this.goExtensions.has(e),l=this.rustExtensions.has(e),c=this.javaExtensions.has(e),d=this.csharpExtensions.has(e),p=this.phpExtensions.has(e),m=this.rubyExtensions.has(e);if(!(a||o||r||l||c||d||p||m))continue;const u=path.relative(this.config.rootPath,t);if(i.some(e=>u.includes(e)||t.includes(e)))continue;const g=fs.readFileSync(t,"utf-8"),h=g.split("\n"),_=new Set;for(let e=0;e<h.length;e++)h[e].includes("thuban-ignore")&&(_.add(e+1),_.add(e+2));const P=this._isPatternDefinitionFile(g);s.stats.filesScanned++,a?(this._checkPhantomImports(u,g,h,n,s,_),P||this._checkPhantomAPIs(u,g,h,s,_),this._checkAISmells(u,g,h,s,_),P||this._checkDeprecatedAPIs(u,g,h,s,_)):o?(this._checkLanguagePatterns(u,g,h,s,_,this.pythonPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.pythonDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.pythonSmells)):p?(this._checkLanguagePatterns(u,g,h,s,_,this.phpPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.phpDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.phpSmells)):m?(this._checkLanguagePatterns(u,g,h,s,_,this.rubyPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.rubyDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.rubySmells)):r?(this._checkLanguagePatterns(u,g,h,s,_,this.goDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.goSmells)):l?(this._checkLanguagePatterns(u,g,h,s,_,this.rustPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.rustDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.rustSmells)):c?".kt"===e?(this._checkLanguagePatterns(u,g,h,s,_,this.kotlinPhantomAPIs,"phantomAPIs"),this._checkLanguagePatterns(u,g,h,s,_,this.kotlinDeprecated,"deprecatedAPIs"),this._checkLanguageSmells(u,g,h,s,_,this.kotlinSmells)):this._checkLanguageSmells(u,g,h,s,_,this.javaSmells):d&&this._checkLanguageSmells(u,g,h,s,_,this.csharpSmells)}catch(e){}return s.stats.totalIssues=s.phantomImports.length+s.phantomAPIs.length+s.aiSmells.length+s.deprecatedAPIs.length+s.unresolvedDeps.length,s.stats.scanTime=Date.now()-t,s}_checkLanguagePatterns(e,t,s,n,i,a,o){for(let t=0;t<s.length;t++){if(i.has(t+1))continue;const r=s[t];for(const s of a)if(s.pattern.test(r)){const i={file:e,line:t+1,name:s.name,id:s.id};s.suggestion&&(i.suggestion=s.suggestion),s.since&&(i.since=s.since,i.fix=s.suggestion),"deprecatedAPIs"===o?n.deprecatedAPIs.push(i):n.phantomAPIs.push(i)}}}_checkLanguageSmells(e,t,s,n,i,a){for(let t=0;t<s.length;t++){if(i.has(t+1))continue;const o=s[t];for(const s of a)s.pattern.test(o)&&n.aiSmells.push({file:e,line:t+1,id:s.id,name:s.name,message:s.message,type:"ai_smell",severity:"medium"})}}formatReport(e){const t="[0m",s="[1m",n="[31m",i="[32m",a="[33m",o="[36m",r="[90m",l="[35m";let c="";if(c+=`\n${l} ╔══════════════════════════════════════════════╗${t}\n`,c+=`${l} ║${s} THUBAN HALLUCINATION REPORT ${t}${l}║${t}\n`,c+=`${l} ╚══════════════════════════════════════════════╝${t}\n\n`,0===e.stats.totalIssues)return c+=` ${i}${s}No hallucinations detected.${t} ${r}Clean codebase.${t}\n\n`,c;if(e.phantomImports.length>0){c+=` ${n}${s}Phantom Imports (${e.phantomImports.length})${t}\n`,c+=` ${r}Packages that don't exist in package.json or node_modules${t}\n\n`;for(const s of e.phantomImports.slice(0,10))c+=` ${n}✗${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.module}${t} ${r}— ${s.reason}${t}\n`;e.phantomImports.length>10&&(c+=` ${r}... and ${e.phantomImports.length-10} more${t}\n`),c+="\n"}if(e.phantomAPIs.length>0){c+=` ${n}${s}Phantom APIs (${e.phantomAPIs.length})${t}\n`,c+=` ${r}Methods/properties that don't exist on their objects${t}\n\n`;for(const s of e.phantomAPIs.slice(0,10))c+=` ${n}✗${t} ${s.id?`${r}[${s.id}]${t} `:""}${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.name}${t} ${r}— ${s.suggestion}${t}\n`,c+=` ${r}Suppress: // thuban-ignore ${s.id||"HALL_API"}${t}\n`;e.phantomAPIs.length>10&&(c+=` ${r}... and ${e.phantomAPIs.length-10} more${t}\n`),c+="\n"}if(e.aiSmells.length>0){c+=` ${a}${s}AI Code Smells (${e.aiSmells.length})${t}\n`,c+=` ${r}Patterns typical of AI-generated code that needs review${t}\n\n`;for(const s of e.aiSmells.slice(0,10))c+=` ${a}!${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${r}${s.message}${t}\n`;e.aiSmells.length>10&&(c+=` ${r}... and ${e.aiSmells.length-10} more${t}\n`),c+="\n"}if(e.deprecatedAPIs.length>0){c+=` ${a}${s}Deprecated APIs (${e.deprecatedAPIs.length})${t}\n`,c+=` ${r}APIs that LLMs still suggest but are deprecated or removed${t}\n\n`;for(const s of e.deprecatedAPIs.slice(0,10))c+=` ${a}⚠${t} ${o}${s.file}${t}:${s.line}\n`,c+=` ${a}${s.name}${t} ${r}(deprecated since ${s.since})${t}\n`,c+=` ${i}→${t} ${r}${s.suggestion}${t}\n`;e.deprecatedAPIs.length>10&&(c+=` ${r}... and ${e.deprecatedAPIs.length-10} more${t}\n`),c+="\n"}return c+=` ${s}Summary:${t} ${o}${e.stats.filesScanned}${t} files scanned, `,c+=`${e.stats.totalIssues>0?n:i}${e.stats.totalIssues}${t} hallucination issues found `,c+=`${r}(${e.stats.scanTime}ms)${t}\n\n`,c}_loadDeclaredDeps(){const e=new Set;return this._packageJsonPaths=new Map,this._collectPackageJsonDeps(this.config.rootPath,e,0),e}_collectPackageJsonDeps(e,t,s){if(!(s>5))try{const n=path.join(e,"package.json");if(fs.existsSync(n)){const s=JSON.parse(fs.readFileSync(n,"utf-8")),i=new Set;for(const e of Object.keys(s.dependencies||{}))t.add(e),i.add(e);for(const e of Object.keys(s.devDependencies||{}))t.add(e),i.add(e);for(const e of Object.keys(s.peerDependencies||{}))t.add(e),i.add(e);this._packageJsonPaths.set(e,i)}const i=new Set(["node_modules",".git","dist","build","coverage",".next",".cache","vendor"]),a=fs.readdirSync(e,{withFileTypes:!0});for(const n of a)!n.isDirectory()||i.has(n.name)||n.name.startsWith(".")||this._collectPackageJsonDeps(path.join(e,n.name),t,s+1)}catch(e){}}_checkPhantomImports(e,t,s,n,i,a=new Set){const o=[/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,/import\s+.*?from\s+['"]([^'"]+)['"]/g,/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g];for(const s of o){let o;for(;null!==(o=s.exec(t));){const s=o[1];if(s.startsWith(".")||s.startsWith("/"))continue;const r=s.split("/")[0];if(this.builtins.has(s)||this.builtins.has(r))continue;const l=s.startsWith("@")?s.split("/").slice(0,2).join("/"):r;if(!n.has(l)&&!this._findInNodeModules(file,l)){const n=this._findLineNumber(t,o.index);if(a.has(n))continue;const r=this.phantomPackages.has(s);i.phantomImports.push({file:e,line:n,module:s,reason:r?"Known AI-hallucinated package — does not exist on npm":"Not in package.json and not in node_modules",severity:r?"critical":"high",fixable:!1})}}}}_checkPhantomAPIs(e,t,s,n,i=new Set){for(const a of this.phantomAPIs){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=r.exec(t));){const r=this._findLineNumber(t,o.index);if(i.has(r))continue;const l=s[r-1]||"";this._isInsidePattern(l,o[0])||n.phantomAPIs.push({file:e,line:r,name:a.name,id:a.id,suggestion:a.suggestion,severity:"high",fixable:!0,fixAction:"replace_phantom_api"})}}}_checkAISmells(e,t,s,n,i=new Set){for(const a of this.aiCodeSmells){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=r.exec(t));){const r=this._findLineNumber(t,o.index);i.has(r)||n.aiSmells.push({file:e,line:r,id:a.id,name:a.name,message:a.message,severity:"warning",code:s[r-1]?.trim().substring(0,100)})}}}_checkDeprecatedAPIs(e,t,s,n,i=new Set){for(const a of this.deprecatedAPIs){let o;const r=new RegExp(a.pattern.source,a.pattern.flags+(a.pattern.flags.includes("g")?"":"g"));for(;null!==(o=r.exec(t));){const r=this._findLineNumber(t,o.index);if(i.has(r))continue;const l=s[r-1]||"";this._isInsidePattern(l,o[0])||n.deprecatedAPIs.push({file:e,line:r,name:a.name,id:a.id,suggestion:a.suggestion,since:a.since,severity:"warning",fixable:!0,fixAction:"update_deprecated_api"})}}}_findLineNumber(e,t){let s=1;for(let n=0;n<t&&n<e.length;n++)"\n"===e[n]&&s++;return s}_isPatternDefinitionFile(e){const t=(e.match(/pattern:\s*\//g)||[]).length,s=(e.match(/phantom|hallucin/gi)||[]).length;return t>5&&s>2}_isInsidePattern(e,t){const s=e.trim();return!!(/pattern:\s*\//.test(s)||s.startsWith("//")||s.startsWith("*")||/(?:name|suggestion|message|description):\s*['"]/.test(s))}_loadIgnoreRules(){const e=[];try{const t=path.join(this.config.rootPath,".thubanrc.json"),s=JSON.parse(fs.readFileSync(t,"utf-8"));s.hallucination?.ignore&&e.push(...s.hallucination.ignore),s.ignore&&e.push(...s.ignore)}catch(e){}return e}_findInNodeModules(e,t){let s=path.dirname(e);const n=this.config.rootPath;for(;s.length>=n.length;){const e=path.join(s,"node_modules",t);try{if(fs.existsSync(e))return!0}catch(e){}const n=path.dirname(s);if(n===s)break;s=n}return!1}}module.exports=HallucinationDetector;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");class HealthChecker extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),serverPath:e.serverPath||path.join(e.rootPath||process.cwd(),"server"),checkTimeout:e.checkTimeout||5e3,...e},this.systemChecks=[{id:"senate",name:"Senate",description:"Multi-model orchestration",check:()=>this._checkSenate()},{id:"citadel",name:"Citadel",description:"5-layer safety system",check:()=>this._checkCitadel()},{id:"forge",name:"Forge",description:"Build system",check:()=>this._checkForge()},{id:"ocee",name:"OCEE",description:"Orchestrated continuous execution",check:()=>this._checkOCEE()},{id:"handoff",name:"Handoff",description:"Human-AI collaboration",check:()=>this._checkHandoff()},{id:"shadow_realm",name:"Shadow Realm",description:"Validation sandbox",check:()=>this._checkShadowRealm()},{id:"knowledge",name:"Knowledge Manager",description:"Build tracking and learning",check:()=>this._checkKnowledge()},{id:"server",name:"Express Server",description:"API server health",check:()=>this._checkServer()}]}async checkAll(){console.log("[HEALTH-CHECKER] Running full health check...");const e=Date.now(),s={timestamp:(new Date).toISOString(),duration:0,overall:"healthy",systems:[],healthySystems:[],unhealthySystems:[],summary:{}};for(const e of this.systemChecks)try{const t=await Promise.race([e.check(),this._timeout(this.config.checkTimeout)]),i={id:e.id,name:e.name,description:e.description,status:t.healthy?"healthy":"unhealthy",healthy:t.healthy,details:t.details||{},issues:t.issues||[],lastChecked:(new Date).toISOString()};s.systems.push(i),i.healthy?s.healthySystems.push(e.id):s.unhealthySystems.push(e.id)}catch(t){s.systems.push({id:e.id,name:e.name,description:e.description,status:"error",healthy:!1,error:t.message,lastChecked:(new Date).toISOString()}),s.unhealthySystems.push(e.id)}return 0===s.unhealthySystems.length?s.overall="healthy":s.unhealthySystems.length<=2?s.overall="degraded":s.overall="unhealthy",s.duration=Date.now()-e,s.summary={total:this.systemChecks.length,healthy:s.healthySystems.length,unhealthy:s.unhealthySystems.length},console.log(`[HEALTH-CHECKER] Health check complete: ${s.overall} (${s.summary.healthy}/${s.summary.total} systems healthy)`),this.emit("checkComplete",s),s}async checkSystem(e){const s=this.systemChecks.find(s=>s.id===e);if(!s)return{error:`Unknown system: ${e}`};try{const e=await s.check();return{id:s.id,name:s.name,...e,lastChecked:(new Date).toISOString()}}catch(e){return{id:s.id,name:s.name,healthy:!1,error:e.message,lastChecked:(new Date).toISOString()}}}getAvailableChecks(){return this.systemChecks.map(e=>({id:e.id,name:e.name,description:e.description}))}async _checkSenate(){const e={healthy:!0,details:{},issues:[]};try{const s=[path.join(this.config.serverPath,"senate.js"),path.join(this.config.serverPath,"senate-v2.js"),path.join(this.config.serverPath,"data","model-registry.json"),path.join(this.config.serverPath,"data","senate-weights-v2.json")];let t=0;for(const e of s)fs.existsSync(e)&&t++;e.details.filesFound=t,e.details.totalFiles=s.length;const i=path.join(this.config.serverPath,"data","model-registry.json");if(fs.existsSync(i)){const s=JSON.parse(fs.readFileSync(i,"utf8"));e.details.providersConfigured=Object.keys(s.providers||{}).length,e.details.modelsRegistered=Object.values(s.providers||{}).reduce((e,s)=>e+(s.models?.length||0),0)}t<2&&(e.healthy=!1,e.issues.push("Core Senate files missing"))}catch(s){e.healthy=!1,e.issues.push(`Senate check failed: ${s.message}`)}return e}async _checkCitadel(){const e={healthy:!0,details:{},issues:[]};try{const s=path.join(this.config.rootPath,"citadel"),t=["enforcer.js","watchers.js","constitution-loader.js","file-guard.js"];let i=0;for(const e of t)fs.existsSync(path.join(s,e))&&i++;e.details.filesFound=i,e.details.totalFiles=t.length;const a=path.join(s,"audit");if(fs.existsSync(a)){const s=fs.readdirSync(a).filter(e=>e.endsWith(".json"));if(e.details.auditFilesCount=s.length,s.length>0){const t=s.sort().pop(),i=path.join(a,t),n=fs.statSync(i);e.details.lastAudit=n.mtime.toISOString();const o=(Date.now()-n.mtime.getTime())/36e5;o>24&&e.issues.push(`No audit activity in ${Math.round(o)} hours`)}}i<3&&(e.healthy=!1,e.issues.push("Core Citadel files missing"))}catch(s){e.healthy=!1,e.issues.push(`Citadel check failed: ${s.message}`)}return e}async _checkForge(){const e={healthy:!0,details:{},issues:[]};try{const s=path.join(this.config.rootPath,"forge"),t=["forge-core.js","task-decomposer.js","intent-matcher.js","diagnostician-v2.js","prescriber.js","forge-loop.js"];let i=0;for(const e of t)fs.existsSync(path.join(s,e))&&i++;e.details.filesFound=i,e.details.totalFiles=t.length,e.details.completeness=Math.round(i/t.length*100)+"%",i<4&&(e.healthy=!1,e.issues.push("Core Forge components missing"))}catch(s){e.healthy=!1,e.issues.push(`Forge check failed: ${s.message}`)}return e}async _checkOCEE(){const e={healthy:!0,details:{},issues:[]};try{const s=path.join(this.config.serverPath,"ocee"),t=["ocee-core.js","persistent-task-queue.js","checkpoint-manager.js","build-session-manager.js","build-graph-generator.js"];let i=0;for(const e of t)fs.existsSync(path.join(s,e))&&i++;e.details.filesFound=i,e.details.totalFiles=t.length;const a=path.join(s,"queue");if(fs.existsSync(a))try{const s=fs.readdirSync(a);e.details.pendingTasks=s.length}catch(s){e.details.pendingTasks="unknown"}i<3&&(e.healthy=!1,e.issues.push("Core OCEE components missing"))}catch(s){e.healthy=!1,e.issues.push(`OCEE check failed: ${s.message}`)}return e}async _checkHandoff(){const e={healthy:!0,details:{},issues:[]};try{const s=path.join(this.config.serverPath,"handoff"),t=["index.js","risk-scorer.js","gate-checker.js","mission-state.js","audit-logger.js","evidence-bundle.js","cost-tracker.js"];let i=0;for(const e of t)fs.existsSync(path.join(s,e))&&i++;e.details.filesFound=i,e.details.totalFiles=t.length,i<5&&(e.healthy=!1,e.issues.push("Core Handoff components missing"))}catch(s){e.healthy=!1,e.issues.push(`Handoff check failed: ${s.message}`)}return e}async _checkShadowRealm(){const e={healthy:!0,details:{},issues:[]};try{const s=path.join(this.config.rootPath,"build-engine","orion-core"),t=["shadow-realm-v2.js","foreman.js","healing-loop.js"];let i=0;for(const e of t)fs.existsSync(path.join(s,e))&&i++;e.details.filesFound=i,e.details.totalFiles=t.length;try{const t=require(path.join(s,"shadow-realm-v2.js")).validateInSandbox('console.log("test");',"test.js");e.details.validationWorking=!0===t.syntaxValid}catch(s){e.details.validationWorking=!1,e.issues.push(`Shadow Realm validation test failed: ${s.message}`)}i<2&&(e.healthy=!1,e.issues.push("Core Shadow Realm components missing"))}catch(s){e.healthy=!1,e.issues.push(`Shadow Realm check failed: ${s.message}`)}return e}async _checkKnowledge(){const e={healthy:!0,details:{},issues:[]};try{const s=path.join(this.config.rootPath,"orion-knowledge.json");if(fs.existsSync(s)){const t=JSON.parse(fs.readFileSync(s,"utf8"));if(e.details.buildsTracked=t.builds?.length||0,e.details.capabilitiesTracked=t.capabilities?.length||0,t.builds&&t.builds.length>0){const s=t.builds[t.builds.length-1];e.details.lastBuild=s.timestamp||s.date}}else e.healthy=!1,e.issues.push("orion-knowledge.json not found")}catch(s){e.healthy=!1,e.issues.push(`Knowledge check failed: ${s.message}`)}return e}async _checkServer(){const e={healthy:!0,details:{},issues:[]};try{const s=[path.join(this.config.serverPath,"package.json"),path.join(this.config.serverPath,"index.js")];let t=0;for(const e of s)fs.existsSync(e)&&t++;e.details.filesFound=t;const i=path.join(this.config.serverPath,"package.json");if(fs.existsSync(i)){const s=JSON.parse(fs.readFileSync(i,"utf8"));e.details.dependencies=Object.keys(s.dependencies||{}).length,e.details.nodeVersion=s.engines?.node||"not specified"}const a=path.join(this.config.serverPath,"node_modules");e.details.nodeModulesInstalled=fs.existsSync(a),e.details.nodeModulesInstalled||e.issues.push("node_modules not installed - run npm install")}catch(s){e.healthy=!1,e.issues.push(`Server check failed: ${s.message}`)}return e}_timeout(e){return new Promise((s,t)=>setTimeout(()=>t(new Error("Health check timeout")),e))}}module.exports=HealthChecker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path");class HtmlReportGenerator{constructor(e={}){this.config={rootPath:e.rootPath||process.cwd(),outputDir:e.outputDir||process.cwd(),...e}}generate(e,n={},r={}){const t=(new Date).toISOString(),a=r.projectName||path.basename(this.config.rootPath);return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Thuban Report — ${this._esc(a)}</title>\n<style>\n${this._css()}\n</style>\n</head>\n<body>\n<div class="container">\n ${this._header(a,t,e)}\n ${this._overallScore(e)}\n ${this._categoryCards(e)}\n ${this._motherCodeSection(e)}\n ${this._topIssues(e)}\n ${this._fixableItems(e)}\n ${this._dependencySection(n)}\n ${this._recommendations(e)}\n ${this._footer(e,r)}\n</div>\n<script>\n${this._js()}\n<\/script>\n</body>\n</html>`}async writeReport(e,n={},r={}){const t=this.generate(e,n,r),a=`thuban-report-${(new Date).toISOString().slice(0,10)}.html`,o=path.join(this.config.outputDir,a);return fs.writeFileSync(o,t,"utf-8"),o}_header(e,n,r){return`\n <header class="header">\n <div class="header-left">\n <div class="logo">\n <svg width="32" height="32" viewBox="0 0 32 32" fill="none">\n <polygon points="16,2 30,28 2,28" stroke="#00f6ff" stroke-width="2" fill="none"/>\n <polygon points="16,8 25,25 7,25" stroke="#00f6ff" stroke-width="1" fill="rgba(0,246,255,0.1)"/>\n <circle cx="16" cy="18" r="3" fill="#00f6ff"/>\n </svg>\n <h1>THUBAN</h1>\n </div>\n <span class="subtitle">Code Health Report</span>\n </div>\n <div class="header-right">\n <div class="meta-item"><span class="meta-label">Project</span><span class="meta-value">${this._esc(e)}</span></div>\n <div class="meta-item"><span class="meta-label">Generated</span><span class="meta-value">${new Date(n).toLocaleString()}</span></div>\n <div class="meta-item"><span class="meta-label">Files</span><span class="meta-value">${r.stats.totalFiles}</span></div>\n <div class="meta-item"><span class="meta-label">Scan Time</span><span class="meta-value">${r.stats.scanTime}ms</span></div>\n </div>\n </header>`}_overallScore(e){const n=e.scores.overall,r=n>=90?"A":n>=80?"B":n>=70?"C":n>=60?"D":"F",t=n>=80?"grade-good":n>=60?"grade-warn":"grade-bad",a=2*Math.PI*54,o=a-n/100*a;return`\n <section class="score-section">\n <div class="score-ring-container">\n <svg class="score-ring" viewBox="0 0 120 120">\n <circle cx="60" cy="60" r="54" stroke="#1a1a2e" stroke-width="8" fill="none"/>\n <circle cx="60" cy="60" r="54" stroke="${this._scoreColor(n)}" stroke-width="8" fill="none"\n stroke-dasharray="${a}" stroke-dashoffset="${o}"\n stroke-linecap="round" transform="rotate(-90 60 60)" class="score-progress"/>\n </svg>\n <div class="score-inner">\n <span class="score-number">${n}</span>\n <span class="score-label">/ 100</span>\n </div>\n </div>\n <div class="score-details">\n <div class="grade ${t}">Grade ${r}</div>\n <div class="score-summary">\n <div class="stat"><span class="stat-num">${e.stats.totalIssues}</span><span class="stat-label">Issues</span></div>\n <div class="stat"><span class="stat-num">${e.stats.fixableCount}</span><span class="stat-label">Auto-fixable</span></div>\n <div class="stat"><span class="stat-num">${e.stats.manualCount}</span><span class="stat-label">Manual</span></div>\n <div class="stat"><span class="stat-num">${e.stats.estimatedFixTime}</span><span class="stat-label">Est. Fix Time</span></div>\n </div>\n </div>\n </section>`}_categoryCards(e){return`\n <section class="categories-section">\n <h2>Category Breakdown</h2>\n <div class="category-grid">${[{key:"security",icon:"🛡️",label:"Security"},{key:"architecture",icon:"🏗️",label:"Architecture"},{key:"codeQuality",icon:"📝",label:"Code Quality"},{key:"motherCode",icon:"🧬",label:"Mother Code"},{key:"dependencies",icon:"🔗",label:"Dependencies"}].map(n=>{const r=e.scores[n.key]||0,t=e.categories[n.key]||{},a=t.issues?.length||0;return`\n <div class="category-card">\n <div class="card-header">\n <span class="card-icon">${n.icon}</span>\n <span class="card-label">${n.label}</span>\n </div>\n <div class="card-score" style="color: ${this._scoreColor(r)}">${r}%</div>\n <div class="card-bar">\n <div class="card-bar-fill" style="width: ${r}%; background: ${this._scoreColor(r)}"></div>\n </div>\n <div class="card-issues">${a} issue${1!==a?"s":""}</div>\n </div>`}).join("")}</div>\n </section>`}_motherCodeSection(e){const n=e.categories.motherCode;if(!n)return"";const r=n.coverage||0;return`\n <section class="mother-code-section">\n <h2>🧬 Mother Code Coverage</h2>\n <div class="mc-grid">\n <div class="mc-stat">\n <div class="mc-circle" style="background: conic-gradient(${this._scoreColor(r)} ${3.6*r}deg, #1a1a2e ${3.6*r}deg)">\n <span>${r}%</span>\n </div>\n <span class="mc-label">Coverage</span>\n </div>\n <div class="mc-details">\n <div class="mc-row"><span class="mc-dot dot-green"></span> Annotated: <strong>${n.annotated||0}</strong> files</div>\n <div class="mc-row"><span class="mc-dot dot-yellow"></span> Missing: <strong>${n.missing||0}</strong> files</div>\n <div class="mc-row"><span class="mc-dot dot-red"></span> Drifted: <strong>${n.drifted||0}</strong> files</div>\n </div>\n <div class="mc-action">\n <p>Inject Mother Code DNA into all files:</p>\n <code>thuban inject [path] --fix</code>\n </div>\n </div>\n </section>`}_topIssues(e){const n=[...e.manual||[],...e.fixable||[]].filter(e=>"info"!==e.severity).sort((e,n)=>{const r={critical:0,high:1,error:1,medium:2,warning:2,low:3};return(r[e.severity]||4)-(r[n.severity]||4)}).slice(0,20);return 0===n.length?"":`\n <section class="issues-section">\n <h2>Top Issues</h2>\n <table class="issues-table">\n <thead><tr><th>Severity</th><th>File</th><th>Issue</th><th>Fix</th></tr></thead>\n <tbody>${n.map(e=>`\n <tr>\n <td><span class="sev sev-${e.severity}">${(e.severity||"").toUpperCase()}</span></td>\n <td class="file-cell">${this._esc(e.file||"")}</td>\n <td>${this._esc(e.message||e.type||"")}</td>\n <td>${e.fixable?'<span class="fixable-badge">Auto-fix</span>':'<span class="manual-badge">Manual</span>'}</td>\n </tr>`).join("")}</tbody>\n </table>\n </section>`}_fixableItems(e){if(!e.fixable||0===e.fixable.length)return"";const n={};for(const r of e.fixable){const e=r.fixAction||"unknown";n[e]||(n[e]={count:0,label:e}),n[e].count++}const r={inject_widget:"Inject Mother Code DNA",regenerate_widget:"Update stale annotations",remove_console_log:"Remove debug console.logs",extract_to_env:"Extract hardcoded secrets to .env",flag_for_removal:"Remove orphan files",break_circular:"Break circular dependencies"},t=Object.entries(n).sort((e,n)=>n[1].count-e[1].count).map(([e,n])=>`\n <div class="fix-item">\n <span class="fix-count">${n.count}</span>\n <span class="fix-label">${r[e]||e}</span>\n </div>`).join("");return`\n <section class="fixable-section">\n <h2>Auto-Fixable Tech Debt</h2>\n <p class="fix-intro">Run <code>thuban fix [path] --fix</code> to resolve ${e.stats.fixableCount} items automatically:</p>\n <div class="fix-grid">${t}</div>\n </section>`}_dependencySection(e){if(!e||!e.circular?.length&&!e.orphans?.length&&!e.critical?.length)return"";let n='<section class="deps-section"><h2>Dependency Analysis</h2><div class="deps-grid">';return e.circular?.length>0&&(n+=`\n <div class="dep-card dep-danger">\n <h3>Circular Dependencies</h3>\n <span class="dep-count">${e.circular.length}</span>\n <ul>${e.circular.slice(0,5).map(e=>`<li>${this._esc(Array.isArray(e)?e.join(" → "):String(e))}</li>`).join("")}</ul>\n </div>`),e.orphans?.length>0&&(n+=`\n <div class="dep-card dep-warn">\n <h3>Orphan Files</h3>\n <span class="dep-count">${e.orphans.length}</span>\n <p>Files with no imports and no consumers</p>\n </div>`),e.critical?.length>0&&(n+=`\n <div class="dep-card dep-info">\n <h3>Critical Files</h3>\n <span class="dep-count">${e.critical.length}</span>\n <p>Files with the most dependents — high blast radius</p>\n </div>`),n+="</div></section>",n}_recommendations(e){const n=[];return e.scores.security<80&&n.push({priority:"critical",text:"Fix security issues immediately — hardcoded secrets or injection risks detected"}),(e.categories.architecture?.issues||[]).filter(e=>"circular_dependency"===e.type).length>0&&n.push({priority:"high",text:"Resolve circular dependencies to prevent build fragility"}),(e.categories.motherCode?.coverage||0)<50&&n.push({priority:"medium",text:`Run <code>thuban inject --fix</code> to raise Mother Code coverage from ${e.categories.motherCode?.coverage||0}% to 100%`}),e.stats.fixableCount>0&&n.push({priority:"medium",text:`Run <code>thuban fix --fix</code> to auto-resolve ${e.stats.fixableCount} items`}),(e.categories.motherCode?.drifted||0)>0&&n.push({priority:"low",text:`${e.categories.motherCode.drifted} files have drifted from their Mother Code annotations — regenerate with <code>thuban fix --fix</code>`}),0===n.length?'\n <section class="recs-section">\n <h2>Recommendations</h2>\n <div class="rec-clean">Codebase is in excellent health. No critical actions needed.</div>\n </section>':`\n <section class="recs-section">\n <h2>Recommendations</h2>\n ${n.map(e=>`\n <div class="rec-item rec-${e.priority}">\n <span class="rec-priority">${e.priority.toUpperCase()}</span>\n <span class="rec-text">${e.text}</span>\n </div>`).join("")}\n </section>`}_footer(e,n){return`\n <footer class="footer">\n <div class="footer-brand">\n <span>Generated by <strong>Thuban</strong> Code Health Engine v0.2.0</span>\n <span class="footer-link">thuban.dev</span>\n </div>\n <div class="footer-meta">\n <span>${e.stats.totalFiles} files scanned in ${e.stats.scanTime}ms</span>\n </div>\n </footer>`}_css(){return"\n:root {\n --bg: #0a0a1a;\n --surface: #12122a;\n --surface2: #1a1a3e;\n --border: #2a2a5a;\n --text: #e0e0f0;\n --text-dim: #8888aa;\n --cyan: #00f6ff;\n --green: #00ff88;\n --yellow: #ffd93d;\n --red: #ff4444;\n --orange: #ff9500;\n --purple: #a855f7;\n --font: 'Segoe UI', system-ui, -apple-system, sans-serif;\n --mono: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;\n}\n\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nbody {\n background: var(--bg);\n color: var(--text);\n font-family: var(--font);\n line-height: 1.6;\n min-height: 100vh;\n}\n\n.container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 2rem;\n}\n\n/* Header */\n.header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem 2rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n margin-bottom: 2rem;\n}\n.header-left { display: flex; align-items: center; gap: 1.5rem; }\n.logo { display: flex; align-items: center; gap: 0.75rem; }\n.logo h1 { font-size: 1.5rem; letter-spacing: 0.3em; color: var(--cyan); font-weight: 300; }\n.subtitle { color: var(--text-dim); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.1em; }\n.header-right { display: flex; gap: 2rem; }\n.meta-item { display: flex; flex-direction: column; align-items: flex-end; }\n.meta-label { font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }\n.meta-value { font-size: 0.9rem; color: var(--text); font-family: var(--mono); }\n\n/* Score Section */\n.score-section {\n display: flex;\n align-items: center;\n gap: 3rem;\n padding: 2rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n margin-bottom: 2rem;\n}\n.score-ring-container { position: relative; width: 140px; height: 140px; flex-shrink: 0; }\n.score-ring { width: 100%; height: 100%; }\n.score-progress { transition: stroke-dashoffset 1s ease-out; }\n.score-inner {\n position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);\n text-align: center;\n}\n.score-number { font-size: 2.5rem; font-weight: 700; color: var(--text); display: block; line-height: 1; }\n.score-label { font-size: 0.8rem; color: var(--text-dim); }\n.score-details { flex: 1; }\n.grade {\n font-size: 1.8rem; font-weight: 700; margin-bottom: 1rem;\n letter-spacing: 0.05em;\n}\n.grade-good { color: var(--green); }\n.grade-warn { color: var(--yellow); }\n.grade-bad { color: var(--red); }\n.score-summary { display: flex; gap: 2rem; }\n.stat { display: flex; flex-direction: column; }\n.stat-num { font-size: 1.4rem; font-weight: 600; font-family: var(--mono); }\n.stat-label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }\n\n/* Category Cards */\n.categories-section { margin-bottom: 2rem; }\n.categories-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }\n.category-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; }\n.category-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n padding: 1.25rem;\n text-align: center;\n transition: border-color 0.2s;\n}\n.category-card:hover { border-color: var(--cyan); }\n.card-header { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 0.75rem; }\n.card-icon { font-size: 1.2rem; }\n.card-label { font-size: 0.85rem; color: var(--text-dim); }\n.card-score { font-size: 2rem; font-weight: 700; font-family: var(--mono); margin-bottom: 0.5rem; }\n.card-bar { height: 4px; background: var(--surface2); border-radius: 2px; overflow: hidden; margin-bottom: 0.5rem; }\n.card-bar-fill { height: 100%; border-radius: 2px; transition: width 0.8s ease-out; }\n.card-issues { font-size: 0.75rem; color: var(--text-dim); }\n\n/* Mother Code */\n.mother-code-section {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 2rem;\n margin-bottom: 2rem;\n}\n.mother-code-section h2 { margin-bottom: 1.5rem; font-size: 1.1rem; color: var(--cyan); }\n.mc-grid { display: grid; grid-template-columns: 160px 1fr 1fr; gap: 2rem; align-items: center; }\n.mc-circle {\n width: 120px; height: 120px; border-radius: 50%;\n display: flex; align-items: center; justify-content: center;\n margin: 0 auto;\n}\n.mc-circle span { background: var(--bg); width: 90px; height: 90px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; font-weight: 700; font-family: var(--mono); }\n.mc-label { text-align: center; margin-top: 0.5rem; color: var(--text-dim); font-size: 0.85rem; }\n.mc-row { padding: 0.4rem 0; display: flex; align-items: center; gap: 0.5rem; }\n.mc-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }\n.dot-green { background: var(--green); }\n.dot-yellow { background: var(--yellow); }\n.dot-red { background: var(--red); }\n.mc-action code {\n display: inline-block; background: var(--bg); padding: 0.4rem 0.8rem;\n border-radius: 6px; font-family: var(--mono); color: var(--cyan); margin-top: 0.5rem;\n border: 1px solid var(--border);\n}\n\n/* Issues Table */\n.issues-section { margin-bottom: 2rem; }\n.issues-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }\n.issues-table {\n width: 100%; border-collapse: collapse;\n background: var(--surface); border: 1px solid var(--border); border-radius: 10px;\n overflow: hidden;\n}\n.issues-table th {\n text-align: left; padding: 0.75rem 1rem; background: var(--surface2);\n font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim);\n border-bottom: 1px solid var(--border);\n}\n.issues-table td { padding: 0.6rem 1rem; border-bottom: 1px solid rgba(42,42,90,0.5); font-size: 0.85rem; }\n.issues-table tr:last-child td { border-bottom: none; }\n.issues-table tr:hover { background: rgba(0,246,255,0.03); }\n.file-cell { font-family: var(--mono); font-size: 0.8rem; color: var(--cyan); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.sev {\n display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px;\n font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em;\n}\n.sev-critical { background: rgba(255,68,68,0.2); color: var(--red); }\n.sev-high, .sev-error { background: rgba(255,149,0,0.2); color: var(--orange); }\n.sev-medium, .sev-warning { background: rgba(255,217,61,0.2); color: var(--yellow); }\n.sev-low, .sev-info { background: rgba(136,136,170,0.15); color: var(--text-dim); }\n.fixable-badge { background: rgba(0,255,136,0.15); color: var(--green); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.7rem; }\n.manual-badge { background: rgba(255,217,61,0.15); color: var(--yellow); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.7rem; }\n\n/* Fixable Section */\n.fixable-section {\n background: var(--surface); border: 1px solid var(--border); border-radius: 12px;\n padding: 2rem; margin-bottom: 2rem;\n}\n.fixable-section h2 { margin-bottom: 0.5rem; font-size: 1.1rem; color: var(--green); }\n.fix-intro { color: var(--text-dim); font-size: 0.9rem; margin-bottom: 1.5rem; }\n.fix-intro code { background: var(--bg); padding: 0.2rem 0.5rem; border-radius: 4px; font-family: var(--mono); color: var(--cyan); border: 1px solid var(--border); }\n.fix-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }\n.fix-item {\n background: var(--bg); border: 1px solid var(--border); border-radius: 8px;\n padding: 1rem; display: flex; align-items: center; gap: 1rem;\n}\n.fix-count { font-size: 1.8rem; font-weight: 700; color: var(--green); font-family: var(--mono); min-width: 40px; }\n.fix-label { font-size: 0.85rem; color: var(--text); }\n\n/* Dependencies */\n.deps-section { margin-bottom: 2rem; }\n.deps-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }\n.deps-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }\n.dep-card {\n background: var(--surface); border-radius: 10px; padding: 1.5rem;\n border: 1px solid var(--border);\n}\n.dep-danger { border-left: 3px solid var(--red); }\n.dep-warn { border-left: 3px solid var(--yellow); }\n.dep-info { border-left: 3px solid var(--cyan); }\n.dep-card h3 { font-size: 0.9rem; margin-bottom: 0.5rem; }\n.dep-count { font-size: 2rem; font-weight: 700; font-family: var(--mono); color: var(--text); display: block; margin-bottom: 0.5rem; }\n.dep-card ul { list-style: none; font-size: 0.8rem; color: var(--text-dim); font-family: var(--mono); }\n.dep-card li { padding: 0.2rem 0; }\n.dep-card p { font-size: 0.8rem; color: var(--text-dim); }\n\n/* Recommendations */\n.recs-section { margin-bottom: 2rem; }\n.recs-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }\n.rec-item {\n display: flex; align-items: center; gap: 1rem;\n background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 1rem 1.5rem; margin-bottom: 0.5rem;\n}\n.rec-critical { border-left: 3px solid var(--red); }\n.rec-high { border-left: 3px solid var(--orange); }\n.rec-medium { border-left: 3px solid var(--yellow); }\n.rec-low { border-left: 3px solid var(--text-dim); }\n.rec-priority {\n font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em;\n padding: 0.2rem 0.5rem; border-radius: 4px; min-width: 70px; text-align: center;\n}\n.rec-critical .rec-priority { background: rgba(255,68,68,0.2); color: var(--red); }\n.rec-high .rec-priority { background: rgba(255,149,0,0.2); color: var(--orange); }\n.rec-medium .rec-priority { background: rgba(255,217,61,0.2); color: var(--yellow); }\n.rec-low .rec-priority { background: rgba(136,136,170,0.15); color: var(--text-dim); }\n.rec-text { font-size: 0.9rem; }\n.rec-text code { background: var(--bg); padding: 0.15rem 0.4rem; border-radius: 4px; font-family: var(--mono); color: var(--cyan); font-size: 0.8rem; }\n.rec-clean { background: var(--surface); border: 1px solid var(--green); border-radius: 8px; padding: 1.5rem; text-align: center; color: var(--green); }\n\n/* Footer */\n.footer {\n display: flex; justify-content: space-between; align-items: center;\n padding: 1.5rem 2rem; border-top: 1px solid var(--border);\n color: var(--text-dim); font-size: 0.8rem; margin-top: 2rem;\n}\n.footer-brand { display: flex; align-items: center; gap: 1.5rem; }\n.footer-brand strong { color: var(--cyan); }\n.footer-link { color: var(--cyan); }\n\n/* Responsive */\n@media (max-width: 900px) {\n .category-grid { grid-template-columns: repeat(2, 1fr); }\n .fix-grid { grid-template-columns: repeat(2, 1fr); }\n .mc-grid { grid-template-columns: 1fr; }\n .header { flex-direction: column; gap: 1rem; }\n .score-section { flex-direction: column; text-align: center; }\n .score-summary { justify-content: center; }\n}\n@media (max-width: 600px) {\n .category-grid { grid-template-columns: 1fr; }\n .fix-grid { grid-template-columns: 1fr; }\n .deps-grid { grid-template-columns: 1fr; }\n}\n\n/* Print */\n@media print {\n body { background: #fff; color: #111; }\n .container { max-width: 100%; }\n .header, .category-card, .mother-code-section, .fixable-section, .issues-section, .dep-card, .rec-item {\n background: #fafafa; border-color: #ddd;\n }\n}\n"}_js(){return"\n// Animate score ring on load\ndocument.addEventListener('DOMContentLoaded', () => {\n const progress = document.querySelector('.score-progress');\n if (progress) {\n const final = progress.style.strokeDashoffset || progress.getAttribute('stroke-dashoffset');\n progress.style.strokeDashoffset = progress.getAttribute('stroke-dasharray');\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n progress.style.strokeDashoffset = final;\n });\n });\n }\n});\n"}_scoreColor(e){return e>=80?"#00ff88":e>=60?"#ffd93d":"#ff4444"}_esc(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}}module.exports=HtmlReportGenerator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const{SentinelCore:SentinelCore,createSentinel:createSentinel}=require("./sentinel-core.js"),FileWatcher=require("./file-watcher.js"),CodeScanner=require("./code-scanner.js"),DriftDetector=require("./drift-detector.js"),HealthChecker=require("./health-checker.js"),AlertManager=require("./alert-manager.js"),DependencyGraph=require("./dependency-graph.js"),WidgetGenerator=require("./widget-generator.js"),SentinelKnowledge=require("./sentinel-knowledge.js"),SecretScanner=require("./secret-scanner.js");let sentinelInstance=null;function getSentinel(e={}){return sentinelInstance||(sentinelInstance=createSentinel(e)),sentinelInstance}async function quickScan(e,n={}){return new CodeScanner(n).scanFiles(e)}async function quickHealthCheck(e={}){return new HealthChecker(e).checkAll()}async function quickDriftCheck(e,n={}){const r=new DriftDetector(n),t=[];for(const n of e)t.push(await r.analyzeFile(n));return t}async function quickSecretScan(e,n={}){return new SecretScanner(n).scanFiles(e)}module.exports={SentinelCore:SentinelCore,createSentinel:createSentinel,getSentinel:getSentinel,FileWatcher:FileWatcher,CodeScanner:CodeScanner,DriftDetector:DriftDetector,HealthChecker:HealthChecker,AlertManager:AlertManager,DependencyGraph:DependencyGraph,WidgetGenerator:WidgetGenerator,SentinelKnowledge:SentinelKnowledge,SecretScanner:SecretScanner,quickScan:quickScan,quickHealthCheck:quickHealthCheck,quickDriftCheck:quickDriftCheck,quickSecretScan:quickSecretScan};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),THUBAN_DIR=path.join(os.homedir(),".thuban"),USAGE_FILE=path.join(THUBAN_DIR,"usage.json"),LICENSE_FILE=path.join(THUBAN_DIR,"license.json"),TIERS={free:{name:"Free",maxFiles:100,scansPerMonth:5,showFullDetails:!1,showDashboard:!1,showAutoFix:!1,showCIAction:!1,teaserIssues:3,maxHallucinationDetail:3},pro:{name:"Pro",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!1,teaserIssues:1/0,maxHallucinationDetail:1/0},team:{name:"Team",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},enterprise:{name:"Enterprise",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,teaserIssues:1/0,maxHallucinationDetail:1/0}};class LicenseManager{constructor(){this._ensureDir(),this.machineId=this._getMachineId(),this.usage=this._loadUsage(),this.license=this._loadLicense()}getTier(){return this.license&&this.license.key&&this._validateKey(this.license.key)&&this.license.tier||"free"}getTierConfig(){return"true"===process.env.THUBAN_DEV_MODE?TIERS.pro:TIERS[this.getTier()]||TIERS.free}canScan(){if("true"===process.env.THUBAN_DEV_MODE)return{allowed:!0,remaining:1/0,devMode:!0};const e=this.getTierConfig();if(e.scansPerMonth===1/0)return{allowed:!0,remaining:1/0};const t=this._currentMonth(),s=this.usage.scans?.[t]||0,a=Math.max(0,e.scansPerMonth-s);return{allowed:a>0,remaining:a,limit:e.scansPerMonth,used:s,resetsOn:this._nextMonthDate()}}recordScan(){if("true"===process.env.THUBAN_DEV_MODE)return;const e=this._currentMonth();this.usage.scans||(this.usage.scans={}),this.usage.scans[e]=(this.usage.scans[e]||0)+1,this.usage.lastScan=(new Date).toISOString(),this.usage.totalScans=(this.usage.totalScans||0)+1,this._saveUsage()}activate(e){const t=this._validateKey(e);return t?(this.license={key:e,tier:t.tier,activatedAt:(new Date).toISOString(),machineId:this.machineId},this._saveLicense(),{success:!0,tier:t.tier,tierName:TIERS[t.tier]?.name||t.tier}):{success:!1,error:"Invalid license key format"}}deactivate(){return this.license={},this._saveLicense(),{success:!0}}getStatus(){const e=this.getTier(),t=this.getTierConfig(),s=this.canScan(),a="true"===process.env.THUBAN_DEV_MODE;return{tier:e,tierName:t.name,machineId:this.machineId.substring(0,8)+"...",licensed:"free"!==e,devMode:a,scansUsed:a?0:s.used||0,scansRemaining:s.remaining,scansLimit:a?1/0:s.limit||t.scansPerMonth,totalScans:this.usage.totalScans||0,features:{fullDetails:t.showFullDetails,dashboard:t.showDashboard,autoFix:t.showAutoFix,ciAction:t.showCIAction,scheduledScans:"free"!==e}}}canUseProFeature(e="This feature"){const t=this.getTier();return"free"===t?{allowed:!1,message:`${e} is available on Thuban Pro and above.`}:{allowed:!0,tier:t}}_ensureDir(){try{fs.existsSync(THUBAN_DIR)||fs.mkdirSync(THUBAN_DIR,{recursive:!0})}catch(e){}}_getMachineId(){const e=[os.hostname(),os.userInfo().username,os.platform(),os.arch()].join("|");try{const t=os.networkInterfaces(),s=Object.values(t).flat().filter(e=>!e.internal&&e.mac&&"00:00:00:00:00:00"!==e.mac).map(e=>e.mac).sort();if(s.length>0)return crypto.createHash("sha256").update(e+"|"+s[0]).digest("hex")}catch(e){}return crypto.createHash("sha256").update(e).digest("hex")}_loadUsage(){try{return JSON.parse(fs.readFileSync(USAGE_FILE,"utf-8"))}catch(e){return{scans:{},totalScans:0}}}_saveUsage(){try{fs.writeFileSync(USAGE_FILE,JSON.stringify(this.usage,null,2),"utf-8")}catch(e){}}_loadLicense(){try{return JSON.parse(fs.readFileSync(LICENSE_FILE,"utf-8"))}catch(e){return{}}}_saveLicense(){try{fs.writeFileSync(LICENSE_FILE,JSON.stringify(this.license,null,2),"utf-8")}catch(e){}}_validateKey(e){if(!e||"string"!=typeof e)return null;const t=e.match(/^THUBAN-(FREE|PRO|TEAM|ENT)-([A-Za-z0-9]{16})-([A-Fa-f0-9]{8,})$/);if(!t)return null;const s=t[1],a=t[2],r=t[3];try{const e=Buffer.from("MCowBQYDK2VwAyEAThUbAn5aFEtyK3y4Code1sReAL2026SilVrWngs==","base64"),t=Buffer.from(`THUBAN-${s}-${a}`),n=Buffer.from(r,"hex");if(crypto.verify(null,t,{key:e,format:"der",type:"spki"},n))return{tier:{FREE:"free",PRO:"pro",TEAM:"team",ENT:"enterprise"}[s]||"free",valid:!0,method:"ed25519"}}catch(e){}const n=crypto.createHmac("sha256","thuban-silverwings-2026").update(`THUBAN-${s}-${a}`).digest("hex").substring(0,8);return r.substring(0,8).toLowerCase()!==n.toLowerCase()?null:{tier:{FREE:"free",PRO:"pro",TEAM:"team",ENT:"enterprise"}[s]||"free",valid:!0,method:"hmac-sha256"}}_currentMonth(){const e=new Date;return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}`}_nextMonthDate(){const e=new Date;return new Date(e.getFullYear(),e.getMonth()+1,1).toISOString().split("T")[0]}static generateKey(e="PRO"){const t=crypto.randomBytes(8).toString("hex");return`THUBAN-${e}-${t}-${crypto.createHmac("sha256","thuban-silverwings-2026").update(`THUBAN-${e}-${t}`).digest("hex").substring(0,8)}`}}module.exports=LicenseManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),{execSync:execSync,exec:exec}=require("child_process"),EventEmitter=require("events"),ROOT=path.join(__dirname,".."),SERVER=path.join(ROOT,"server"),MANIFEST_PATH=path.join(SERVER,"data","health-check-manifest.json");class MasterHealthChecker extends EventEmitter{constructor(t={}){super(),this.config={rootPath:t.rootPath||ROOT,serverPath:t.serverPath||SERVER,timeout:t.timeout||5e3,...t},this.manifest=this._loadManifest(),this.lastResults=null,this.issueCount=0}_loadManifest(){try{if(fs.existsSync(MANIFEST_PATH))return JSON.parse(fs.readFileSync(MANIFEST_PATH,"utf8"))}catch(t){console.error("[MASTER-HEALTH] Failed to load manifest:",t.message)}return null}async runFullCheck(){console.log("[MASTER-HEALTH] Starting full system health check (47 checkpoints)...");const t=Date.now(),s={timestamp:(new Date).toISOString(),duration:0,totalChecks:0,passed:0,failed:0,warnings:0,systems:{},criticalIssues:[],autoHealAttempted:[],humanEscalationNeeded:[]},e=[{id:"server",fn:()=>this._checkServer()},{id:"senate",fn:()=>this._checkSenate()},{id:"citadel",fn:()=>this._checkCitadel()},{id:"shadowRealm",fn:()=>this._checkShadowRealm()},{id:"forge",fn:()=>this._checkForge()},{id:"ocee",fn:()=>this._checkOCEE()},{id:"memory",fn:()=>this._checkMemory()},{id:"external",fn:()=>this._checkExternal()}];for(const t of e)try{const e=await t.fn();s.systems[t.id]=e,s.totalChecks+=e.checks.length,s.passed+=e.checks.filter(t=>"PASS"===t.status).length,s.failed+=e.checks.filter(t=>"FAIL"===t.status).length,s.warnings+=e.checks.filter(t=>"WARN"===t.status).length;const i=e.checks.filter(t=>"FAIL"===t.status&&"CRITICAL"===t.criticality);s.criticalIssues.push(...i);const a=e.checks.filter(t=>t.needsHumanEscalation);s.humanEscalationNeeded.push(...a)}catch(e){s.systems[t.id]={status:"ERROR",error:e.message,checks:[]}}return s.duration=Date.now()-t,s.overallStatus=this._calculateOverallStatus(s),this.lastResults=s,this.emit("checkComplete",s),console.log(`[MASTER-HEALTH] Complete: ${s.passed}/${s.totalChecks} passed (${s.duration}ms)`),s}async runQuickCheck(){console.log("[MASTER-HEALTH] Running quick check (critical systems)...");const t={timestamp:(new Date).toISOString(),type:"quick",checks:[]};t.checks.push(await this._check("SRV-001","Server Process",()=>this._fileExists(path.join(this.config.serverPath,"server.js")))),t.checks.push(await this._check("SEN-003","Model Registry",()=>{const t=path.join(this.config.serverPath,"data","model-registry.json");if(!fs.existsSync(t))return!1;const s=JSON.parse(fs.readFileSync(t,"utf8"));return Object.keys(s.providers||{}).length>=3})),t.checks.push(await this._check("CIT-001","Constitution",()=>{const t=path.join(this.config.rootPath,"citadel","constitution.json");return this._fileExists(t)&&this._isValidJson(t)})),t.checks.push(await this._check("CIT-002","File Guard",()=>this._fileExists(path.join(this.config.rootPath,"citadel","file-guard.js"))));const s=t.checks.filter(t=>"PASS"===t.status).length;return console.log(`[MASTER-HEALTH] Quick check: ${s}/${t.checks.length} passed`),t}async _checkServer(){const t=[];return t.push(await this._check("SRV-001","Server Files",()=>this._fileExists(path.join(this.config.serverPath,"server.js"))&&this._fileExists(path.join(this.config.serverPath,"package.json")),"CRITICAL")),t.push(await this._check("SRV-002","Node Modules",()=>this._fileExists(path.join(this.config.serverPath,"node_modules")),"HIGH","npm install")),t.push(await this._check("SRV-003","Package Valid",()=>this._isValidJson(path.join(this.config.serverPath,"package.json")),"HIGH")),t.push(await this._check("SRV-004","Data Directory",()=>this._dirExists(path.join(this.config.serverPath,"data")),"MEDIUM")),t.push(await this._check("SRV-005","Environment",()=>this._fileExists(path.join(this.config.rootPath,".env"))||void 0!==process.env.ANTHROPIC_API_KEY,"HIGH")),{status:this._systemStatus(t),checks:t}}async _checkSenate(){const t=[];return t.push(await this._check("SEN-001","Model Registry",()=>{const t=path.join(this.config.serverPath,"data","model-registry.json");return this._isValidJson(t)},"CRITICAL","Restore from snapshot")),t.push(await this._check("SEN-002","Senate Weights",()=>{const t=path.join(this.config.serverPath,"data","senate-weights-v2.json");return this._isValidJson(t)},"HIGH","Reset to defaults")),t.push(await this._check("SEN-003","Providers Configured",()=>{const t=path.join(this.config.serverPath,"data","model-registry.json");if(!this._isValidJson(t))return!1;const s=JSON.parse(fs.readFileSync(t,"utf8"));return Object.keys(s.providers||{}).length>=5},"CRITICAL")),t.push(await this._check("SEN-004","Senate Core",()=>this._fileExists(path.join(this.config.serverPath,"senate-v2.js"))||this._fileExists(path.join(this.config.serverPath,"senate.js")),"CRITICAL")),t.push(await this._check("SEN-005","Response Cache",()=>{const t=path.join(this.config.serverPath,"data","response-cache.json");return!this._fileExists(t)||this._isValidJson(t)},"LOW")),{status:this._systemStatus(t),checks:t}}async _checkCitadel(){const t=[];return t.push(await this._check("CIT-001","Constitution",()=>{const t=path.join(this.config.rootPath,"citadel","constitution.json");return this._isValidJson(t)},"CRITICAL","HALT - Safety rules missing")),t.push(await this._check("CIT-002","File Guard",()=>this._fileExists(path.join(this.config.rootPath,"citadel","file-guard.js")),"CRITICAL")),t.push(await this._check("CIT-003","Enforcer",()=>this._fileExists(path.join(this.config.rootPath,"citadel","enforcer.js")),"CRITICAL")),t.push(await this._check("CIT-004","Watchers",()=>this._fileExists(path.join(this.config.rootPath,"citadel","watchers.js")),"HIGH")),t.push(await this._check("CIT-005","Audit Directory",()=>this._dirExists(path.join(this.config.rootPath,"citadel","audit")),"MEDIUM","Create directory")),{status:this._systemStatus(t),checks:t}}async _checkShadowRealm(){const t=[];return t.push(await this._check("SHD-001","Shadow Realm V2",()=>this._fileExists(path.join(this.config.rootPath,"build-engine","orion-core","shadow-realm-v2.js")),"CRITICAL")),t.push(await this._check("SHD-002","Node Binary",()=>{try{return execSync("node --version",{stdio:"pipe"}),!0}catch{return!1}},"CRITICAL")),t.push(await this._check("SHD-003","Temp Writable",()=>{const t=require("os"),s=path.join(t.tmpdir(),"orion-health-test-"+Date.now());try{return fs.writeFileSync(s,"test"),fs.unlinkSync(s),!0}catch{return!1}},"HIGH")),t.push(await this._check("SHD-004","Healing Loop",()=>this._fileExists(path.join(this.config.rootPath,"build-engine","orion-core","healing-loop.js")),"HIGH")),{status:this._systemStatus(t),checks:t}}async _checkForge(){const t=[],s=path.join(this.config.rootPath,"forge");return t.push(await this._check("FRG-001","Forge Core",()=>this._fileExists(path.join(s,"forge-core.js")),"HIGH")),t.push(await this._check("FRG-002","Task Decomposer",()=>this._fileExists(path.join(s,"task-decomposer.js")),"MEDIUM")),t.push(await this._check("FRG-003","Intent Matcher",()=>this._fileExists(path.join(s,"intent-matcher.js")),"MEDIUM")),t.push(await this._check("FRG-004","Diagnostician",()=>this._fileExists(path.join(s,"diagnostician-v2.js"))||this._fileExists(path.join(s,"diagnostician.js")),"MEDIUM")),t.push(await this._check("FRG-005","Foreman",()=>this._fileExists(path.join(this.config.rootPath,"build-engine","orion-core","foreman.js")),"HIGH")),{status:this._systemStatus(t),checks:t}}async _checkOCEE(){const t=[],s=path.join(this.config.serverPath,"ocee");return t.push(await this._check("OCE-001","OCEE Core",()=>this._fileExists(path.join(s,"ocee-core.js")),"HIGH")),t.push(await this._check("OCE-002","Task Queue",()=>this._fileExists(path.join(s,"persistent-task-queue.js")),"HIGH")),t.push(await this._check("OCE-003","Checkpoint Manager",()=>this._fileExists(path.join(s,"checkpoint-manager.js")),"HIGH")),t.push(await this._check("OCE-004","Build Sessions",()=>{const t=path.join(this.config.serverPath,"data","build-sessions.json");return!this._fileExists(t)||this._isValidJson(t)},"MEDIUM")),t.push(await this._check("OCE-005","Executor Status",()=>{const t=path.join(this.config.serverPath,"data","executor-status.json");return this._isValidJson(t)},"MEDIUM")),{status:this._systemStatus(t),checks:t}}async _checkMemory(){const t=[];return t.push(await this._check("MEM-001","Orion Knowledge",()=>this._isValidJson(path.join(this.config.rootPath,"orion-knowledge.json")),"MEDIUM","Reinitialize")),t.push(await this._check("MEM-002","Memory Bank",()=>{const t=path.join(this.config.rootPath,"memory-bank");return this._fileExists(path.join(t,"progress.md"))&&this._fileExists(path.join(t,"activeContext.md"))},"MEDIUM")),t.push(await this._check("MEM-003","Snapshot Manifest",()=>{const t=path.join(this.config.serverPath,".orion","snapshot-manifest.json");return!this._fileExists(t)||this._isValidJson(t)},"MEDIUM")),t.push(await this._check("MEM-004","Build Logs",()=>this._dirExists(path.join(this.config.serverPath,"data","build-logs")),"MEDIUM")),t.push(await this._check("MEM-005","Failure Memory",()=>this._fileExists(path.join(this.config.serverPath,"services","failure-memory.js")),"MEDIUM")),{status:this._systemStatus(t),checks:t}}async _checkExternal(){const t=[];return t.push(await this._check("EXT-001","Zoho Tokens",()=>{const t=path.join(this.config.serverPath,"data","zoho-tokens.json");return!this._fileExists(t)||this._isValidJson(t)},"LOW")),t.push(await this._check("EXT-002","Cartography",()=>this._dirExists(path.join(this.config.rootPath,"cartography")),"LOW")),t.push(await this._check("EXT-003","Notification Config",()=>{const t=path.join(this.config.serverPath,"data","notification-config.json");return!this._fileExists(t)||this._isValidJson(t)},"LOW")),t.push(await this._check("EXT-004","Health History",()=>{const t=path.join(this.config.serverPath,"data","health-history.json");return!this._fileExists(t)||this._isValidJson(t)},"LOW")),{status:this._systemStatus(t),checks:t}}async _check(t,s,e,i="MEDIUM",a=null){const h={id:t,name:s,criticality:i,status:"UNKNOWN",autoHeal:a,needsHumanEscalation:!1};try{const t=await e();h.status=t?"PASS":"FAIL",t||"CRITICAL"!==i||(h.needsHumanEscalation=!0)}catch(t){h.status="ERROR",h.error=t.message,h.needsHumanEscalation=!0}return h}_fileExists(t){try{return fs.existsSync(t)}catch{return!1}}_dirExists(t){try{return fs.existsSync(t)&&fs.statSync(t).isDirectory()}catch{return!1}}_isValidJson(t){try{return!!fs.existsSync(t)&&(JSON.parse(fs.readFileSync(t,"utf8")),!0)}catch{return!1}}_systemStatus(t){const s=t.filter(t=>"FAIL"===t.status||"ERROR"===t.status);return s.filter(t=>"CRITICAL"===t.criticality).length>0?"CRITICAL":s.length>0?"DEGRADED":"HEALTHY"}_calculateOverallStatus(t){return t.criticalIssues.length>0?"CRITICAL":t.failed>0?"DEGRADED":t.warnings>0?"WARNING":"HEALTHY"}getSummary(){return this.lastResults?{status:this.lastResults.overallStatus,passed:this.lastResults.passed,failed:this.lastResults.failed,total:this.lastResults.totalChecks,criticalIssues:this.lastResults.criticalIssues.length,humanActionNeeded:this.lastResults.humanEscalationNeeded.length,lastCheck:this.lastResults.timestamp}:{status:"NO_DATA",message:"No health check run yet"}}}module.exports=MasterHealthChecker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const path=require("path");class MonitorNotifier{constructor({alertManager:e,store:i,repoConfig:t}){this.alertManager=e,this.store=i,this.repoConfig=t}async notify(e,i){const t=this.repoConfig.notification?.channels||["inbox"],s=[];if(!e.hasNewIssues)return s;const n=`Found ${e.added.length} new issue${1===e.added.length?"":"s"} in ${this.repoConfig.repoPath}`,o={timestamp:(new Date).toISOString(),repoId:this.repoConfig.repoId,repoPath:this.repoConfig.repoPath,channels:t,summary:n,newIssues:e.added.slice(0,10).map(e=>({file:e.file,line:e.line,severity:e.severity,category:e.category,message:e.message})),runTimestamp:i.timestamp};return t.includes("inbox")&&(this.store.appendNotification(o),s.push({channel:"inbox",delivered:!0})),t.includes("console")&&this.alertManager&&(await this.alertManager.sendAlert({level:"warning",category:"scheduled-scan",title:"Thuban monitor detected new issues",message:n,file:path.join(this.repoConfig.repoPath,"."),details:o}),s.push({channel:"console",delivered:!0})),t.includes("webhook")&&s.push({channel:"webhook",delivered:!1,stub:!0}),t.includes("email")&&s.push({channel:"email",delivered:!1,stub:!0}),s}}module.exports=MonitorNotifier;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const path=require("path"),AlertManager=require("./alert-manager.js"),MonitorStore=require("./monitor-store.js"),MonitorNotifier=require("./monitor-notifier.js"),{runScanPipeline:runScanPipeline}=require("./scan-runner.js"),{diffRuns:diffRuns}=require("./scan-diff.js"),{collectFiles:collectFiles}=require("./file-collector.js");function resolveIntervalMs(e,t){return"weekly"===e?6048e5:"custom"===e?Math.max(6e4,Number(t)||864e5):864e5}class MonitorService{constructor(e={}){this.store=e.store||new MonitorStore,this.timers=new Map}configureRepo({repoPath:e,frequency:t="daily",intervalMs:r,notification:o}){const n=this.store.getRepoId(e),s=resolveIntervalMs(t,r);return this.store.upsertRepo({repoId:n,repoPath:e,frequency:t,intervalMs:s,notification:o,nextRunAt:new Date(Date.now()+s).toISOString()})}async runRepoScan(e){const t=path.resolve(e.repoPath),r=this._collectFiles(t),o=Date.now(),n=await runScanPipeline({rootPath:t,files:r}),s={repoId:e.repoId,repoPath:t,timestamp:(new Date).toISOString(),durationMs:Date.now()-o,frequency:e.frequency,metrics:n.metrics,summary:n.summary,issues:n.issues},i=this.store.getLatestRun(e.repoId),a=diffRuns(i,s);s.diff=a;const l=this.store.saveRun(e.repoId,s),c=this.store.upsertRepo({...e,lastRunAt:s.timestamp,nextRunAt:new Date(Date.now()+e.intervalMs).toISOString()}),u=new MonitorNotifier({alertManager:new AlertManager({rootPath:t,consoleOutput:!0,fileOutput:!0}),store:this.store,repoConfig:c});return s.notifications=await u.notify(a,s),{run:s,runPath:l,previousRun:i,diff:a}}start(e,{once:t=!1}={}){const r=async()=>{try{await this.runRepoScan(e)}catch(t){console.error(`[THUBAN MONITOR] Scheduled scan failed for ${e.repoPath}: ${t.message}`)}};if(r(),t)return;const o=setInterval(r,e.intervalMs);return this.timers.set(e.repoId,o),o}stopAll(){for(const e of this.timers.values())clearInterval(e);this.timers.clear()}_collectFiles(e){return collectFiles(e)}}module.exports={MonitorService:MonitorService,resolveIntervalMs:resolveIntervalMs};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),THUBAN_DIR=path.join(os.homedir(),".thuban"),MONITOR_DIR=path.join(THUBAN_DIR,"monitor"),CONFIG_FILE=path.join(MONITOR_DIR,"config.json"),RUNS_DIR=path.join(MONITOR_DIR,"runs"),NOTIFICATIONS_FILE=path.join(MONITOR_DIR,"notifications.json");function ensureDir(e){fs.existsSync(e)||fs.mkdirSync(e,{recursive:!0})}function safeReadJson(e,t){try{return JSON.parse(fs.readFileSync(e,"utf-8"))}catch(e){return t}}class MonitorStore{constructor(){ensureDir(THUBAN_DIR),ensureDir(MONITOR_DIR),ensureDir(RUNS_DIR)}getRepoId(e){return crypto.createHash("sha256").update(path.resolve(e)).digest("hex").substring(0,12)}loadConfig(){return safeReadJson(CONFIG_FILE,{version:1,repos:[]})}saveConfig(e){fs.writeFileSync(CONFIG_FILE,JSON.stringify(e,null,2),"utf-8")}upsertRepo(e){const t=this.loadConfig(),n=e.repoId||this.getRepoId(e.repoPath),r={repoId:n,repoPath:path.resolve(e.repoPath),frequency:e.frequency||"daily",intervalMs:e.intervalMs,notification:e.notification||{channels:["inbox"]},createdAt:e.createdAt||(new Date).toISOString(),updatedAt:(new Date).toISOString(),enabled:!1!==e.enabled,lastRunAt:e.lastRunAt||null,nextRunAt:e.nextRunAt||null},o=t.repos.findIndex(e=>e.repoId===n);return o>=0?t.repos[o]={...t.repos[o],...r}:t.repos.push(r),this.saveConfig(t),r}listRepos(){return this.loadConfig().repos||[]}getRepo(e){const t=e?path.resolve(e):null;return this.listRepos().find(n=>n.repoId===e||n.repoPath===t)||null}getRepoRunDir(e){const t=path.join(RUNS_DIR,e);return ensureDir(t),t}saveRun(e,t){const n=this.getRepoRunDir(e),r=path.join(n,`${t.timestamp.replace(/[:.]/g,"-")}.json`);return fs.writeFileSync(r,JSON.stringify(t,null,2),"utf-8"),r}listRuns(e){const t=this.getRepoRunDir(e);return fs.readdirSync(t).filter(e=>e.endsWith(".json")).sort().map(e=>safeReadJson(path.join(t,e),null)).filter(Boolean)}getLatestRun(e){const t=this.listRuns(e);return t.length?t[t.length-1]:null}appendNotification(e){const t=safeReadJson(NOTIFICATIONS_FILE,{notifications:[]});t.notifications.push(e),fs.writeFileSync(NOTIFICATIONS_FILE,JSON.stringify(t,null,2),"utf-8")}getNotifications(e=20){return safeReadJson(NOTIFICATIONS_FILE,{notifications:[]}).notifications.slice(-e).reverse()}}module.exports=MonitorStore;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),{execSync:execSync}=require("child_process");class PreCommitGate{constructor(e={}){this.rootPath=e.rootPath||process.cwd(),this.strict=!1!==e.strict,this.timeout=e.timeout||5e3}getStagedFiles(){try{const e=execSync("git diff --cached --name-only --diff-filter=ACM",{cwd:this.rootPath,encoding:"utf-8",timeout:3e3}),s=[".js",".ts",".jsx",".tsx",".py",".mjs",".cjs",".php",".rb"];return e.split("\n").filter(e=>e.trim()&&s.some(s=>e.endsWith(s))).map(e=>path.resolve(this.rootPath,e.trim()))}catch(e){return[]}}checkFile(e){const s=[];let t;try{if(fs.statSync(e).size>524288)return s;t=fs.readFileSync(e,"utf-8")}catch(e){return s}const n=t.split("\n"),i=path.relative(this.rootPath,e),r=[{pattern:/\bJSON\.tryParse\s*\(/g,name:"JSON.tryParse()",fix:"Use JSON.parse() with try/catch"},{pattern:/\bArray\.flatten\s*\(/g,name:"Array.flatten()",fix:"Use Array.flat()"},{pattern:/\bconsole\.success\s*\(/g,name:"console.success()",fix:"Use console.log()"},{pattern:/\bconsole\.fail\s*\(/g,name:"console.fail()",fix:"Use console.error()"},{pattern:/\bfs\.readFileAsync\s*\(/g,name:"fs.readFileAsync()",fix:"Use fs.promises.readFile()"},{pattern:/\bfs\.writeFileAsync\s*\(/g,name:"fs.writeFileAsync()",fix:"Use fs.promises.writeFile()"},{pattern:/\bfs\.existsAsync\s*\(/g,name:"fs.existsAsync()",fix:"Use fs.promises.access()"},{pattern:/\bObject\.deepClone\s*\(/g,name:"Object.deepClone()",fix:"Use structuredClone() or JSON.parse(JSON.stringify())"},{pattern:/\bString\.prototype\.contains\s*\(/g,name:"String.contains()",fix:"Use String.includes()"},{pattern:/\bPromise\.delay\s*\(/g,name:"Promise.delay()",fix:"Use new Promise(r => setTimeout(r, ms))"},{pattern:/\bprocess\.exit\s*\(\s*["'].*["']\s*\)/g,name:"process.exit(string)",fix:"process.exit() takes a number, not a string"},{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"},{pattern:/\.validateSync\s*\(/g,name:".validateSync()",fix:"Check if this method exists on the object — AI commonly invents Sync variants"},{pattern:/\bBuffer\.from\s*\(.*,\s*['"]base64url['"]\)/g,name:'Buffer.from(x, "base64url")',fix:'Use "base64" encoding, not "base64url"'},{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()"}];if(n.filter(e=>/pattern:\s*\//.test(e)).length>5)return s;for(const e of r)for(let t=0;t<n.length;t++){const r=n[t];/^\s*(\/\/|\/\*|\*|#|pattern:|name:|suggestion:|message:)/.test(r)||/thuban-ignore/.test(r)||(e.pattern.test(r)&&s.push({file:i,line:t+1,severity:"critical",code:r.trim(),message:`${e.name} — this API does not exist`,fix:e.fix}),e.pattern.lastIndex=0)}return s}run(){const e=Date.now(),s=this.getStagedFiles();if(0===s.length)return{passed:!0,files:0,issues:[],elapsed:Date.now()-e,message:"No staged files to check"};const t=[];for(const n of s){if(Date.now()-e>this.timeout)break;const s=this.checkFile(n);t.push(...s)}const n=t.filter(e=>"critical"===e.severity),i=!this.strict||0===n.length;return{passed:i,files:s.length,issues:t,criticals:n.length,elapsed:Date.now()-e,message:i?`${s.length} files checked. All clear.`:`BLOCKED: ${n.length} critical issue${n.length>1?"s":""} found in staged files.`}}static install(e=process.cwd()){const s=path.join(e,".git");if(!fs.existsSync(s))return{success:!1,error:"Not a git repository"};const t=path.join(s,"hooks");fs.existsSync(t)||fs.mkdirSync(t,{recursive:!0});const n=path.join(t,"pre-commit"),i="#!/bin/sh\n# Thuban Pre-Commit Gate — blocks commits with hallucinated APIs\n# Remove this file to disable, or run: thuban gate --uninstall\n\nnpx --yes thuban gate --ci 2>&1\nexit $?\n";if(fs.existsSync(n)){if(fs.readFileSync(n,"utf-8").includes("thuban gate"))return{success:!0,message:"Thuban gate already installed"};fs.appendFileSync(n,"\n"+i)}else fs.writeFileSync(n,i);try{fs.chmodSync(n,"755")}catch(e){}return{success:!0,message:"Pre-commit hook installed"}}static uninstall(e=process.cwd()){const s=path.join(e,".git","hooks","pre-commit");if(!fs.existsSync(s))return{success:!0,message:"No pre-commit hook found"};const t=fs.readFileSync(s,"utf-8");if(t.includes("thuban gate")){const e=t.split("\n").filter(e=>!e.includes("thuban")&&!e.includes("Thuban")).join("\n").trim();e.length<10?fs.unlinkSync(s):fs.writeFileSync(s,e+"\n")}return{success:!0,message:"Thuban gate uninstalled"}}}module.exports=PreCommitGate;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function toMap(e=[]){const t=new Map;for(const s of e)t.set(s.fingerprint,s);return t}function diffRuns(e,t){const s=e?.issues||[],n=t?.issues||[],o=toMap(s),u=toMap(n),c=[],r=[];for(const e of n)o.has(e.fingerprint)||c.push(e);for(const e of s)u.has(e.fingerprint)||r.push(e);const i=e?.metrics||{},l=t?.metrics||{};return{added:c,resolved:r,unchangedCount:n.length-c.length,metricsDelta:{issueCount:(l.issueCount||0)-(i.issueCount||0),hallucinations:(l.hallucinations||0)-(i.hallucinations||0),secrets:(l.secrets||0)-(i.secrets||0),techDebtScore:null==l.techDebtScore||null==i.techDebtScore?null:l.techDebtScore-i.techDebtScore,techDebtHours:null==l.techDebtHours||null==i.techDebtHours?null:l.techDebtHours-i.techDebtHours},hasNewIssues:c.length>0}}module.exports={diffRuns:diffRuns};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const path=require("path"),CodeScanner=require("./code-scanner.js"),HallucinationDetector=require("./hallucination-detector.js"),SecretScanner=require("./secret-scanner.js"),DriftDetector=require("./drift-detector.js"),TechDebtAnalyzer=require("./tech-debt-analyzer.js");function normalizeIssue(e,s){const t=s.file||s.path||"";return{file:path.isAbsolute(t)?path.relative(e,t):t,line:s.line||null,severity:s.severity||"unknown",category:s.category||"unknown",id:s.id||null,message:s.message||"",suggestion:s.suggestion||null,fixable:!1!==s.fixable}}function fingerprintIssue(e){return[e.file||"",e.category||"",e.id||"",String(e.line||""),(e.message||"").substring(0,160)].join("|")}function summarizeIssues(e){const s={total:e.length,bySeverity:{},byCategory:{}};for(const t of e)s.bySeverity[t.severity]=(s.bySeverity[t.severity]||0)+1,s.byCategory[t.category]=(s.byCategory[t.category]||0)+1;return s}async function runScanPipeline({rootPath:e,files:s,ignorePatterns:t=[]}){const i=new CodeScanner({rootPath:e,ignorePatterns:["node_modules/**",".git/**","dist/**",...t]}),n=new HallucinationDetector({rootPath:e}),r=new SecretScanner({rootPath:e}),o=new DriftDetector({rootPath:e}),a=new TechDebtAnalyzer({rootPath:e}),[l,u,c,g,f]=await Promise.all([i.scanFiles(s),n.scan(s),r.scanFiles(s),o.detectAll?o.detectAll():Promise.resolve({issues:[]}),a.analyze?a.analyze(s):Promise.resolve(null)]),m=[];for(const[s,t]of Object.entries(l||{}))if(t&&t.issues)for(const i of t.issues)m.push(normalizeIssue(e,{file:s,...i}));for(const s of c?.issues||[])m.push(normalizeIssue(e,s));for(const s of g?.issues||[])m.push(normalizeIssue(e,s));for(const s of u?.phantomAPIs||[])m.push(normalizeIssue(e,{file:s.file,line:s.line,severity:"critical",category:"hallucination",id:s.id||"HALL_API",message:`${s.name} — this API does not exist`,suggestion:s.suggestion,fixable:!1}));for(const s of u?.phantomImports||[])m.push(normalizeIssue(e,{file:s.file,line:s.line,severity:"critical",category:"hallucination",id:s.id||"HALL_IMPORT",message:`${s.module} — ${s.reason}`,suggestion:s.suggestion||null,fixable:!1}));for(const s of u?.deprecatedAPIs||[])m.push(normalizeIssue(e,{file:s.file,line:s.line,severity:"high",category:"deprecated",id:s.id||"DEPR_API",message:`${s.name} — deprecated since ${s.since}`,suggestion:s.suggestion}));for(const s of u?.aiSmells||[])m.push(normalizeIssue(e,{file:s.file,line:s.line,severity:"warning",category:"ai_smell",id:s.id,message:s.message}));const h=[],d=new Set;for(const e of m){const s=fingerprintIssue(e);d.has(s)||(d.add(s),h.push({...e,fingerprint:s}))}const y={filesScanned:s.length,issueCount:h.length,hallucinations:h.filter(e=>"hallucination"===e.category).length,secrets:h.filter(e=>"secret"===e.category||String(e.id||"").startsWith("SECRET")).length,techDebtScore:f?.summary?.score??f?.score??null,techDebtHours:f?.summary?.estimatedHours??f?.estimatedHours??null};return{issues:h,summary:summarizeIssues(h),metrics:y,raw:{scanResults:l,hallResults:u,secretResults:c,driftResults:g,debtResults:f}}}module.exports={runScanPipeline:runScanPipeline,fingerprintIssue:fingerprintIssue,summarizeIssues:summarizeIssues};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto"),EventEmitter=require("events"),{execFileSync:execFileSync}=require("child_process");class SecretScanner extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".next/**","coverage/**"],maxFileSize:e.maxFileSize||1048576,entropyThreshold:e.entropyThreshold||4.2,minEntropyLength:e.minEntropyLength||20,includeGitHistory:!1!==e.includeGitHistory,...e},this.scanExtensions=new Set([".env",".js",".jsx",".ts",".tsx",".py",".json",".yaml",".yml",".toml",".ini",".conf",".config",".properties",".sh",".bash",".zsh"]),this.ignoredDirectories=["node_modules",".git","dist",".next","coverage",".venv","venv","__pycache__"],this.safeLinePatterns=[/process\.env\./i,/process\.env\[/i,/os\.environ/i,/sample/i,/placeholder/i,/dummy/i,/mock/i,/example/i,/replace[_-\s]?me/i,/replace[_-\s]?with/i,/local-dev-token/i,/changeme/i,/your[_-\s]?key/i,/your[_-\s]?(secret|token|password)/i,/<secret>/i,/<token>/i,/x{8,}/i],this.secretPatterns=[{id:"SECRET001",type:"Stripe Secret Key",severity:"critical",recommendation:"Move this key to a cloud secret manager and load it via environment variables.",pattern:/\bsk_(live|test)_[0-9a-zA-Z]{16,}\b/gi},{id:"SECRET002",type:"Stripe Publishable Key",severity:"high",recommendation:"Verify this publishable key is intended to be public; if not, move it to managed secrets.",pattern:/\bpk_(live|test)_[0-9a-zA-Z]{16,}\b/gi},{id:"SECRET003",type:"AWS Access Key",severity:"critical",recommendation:"Rotate the AWS credential immediately and store replacements in a cloud secret manager.",pattern:/\b(AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g},{id:"SECRET004",type:"GitHub Token",severity:"critical",recommendation:"Revoke the GitHub token and replace it with a managed secret or GitHub App credential.",pattern:/\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g},{id:"SECRET005",type:"Slack Token",severity:"critical",recommendation:"Rotate the Slack token and move it into a cloud secret manager.",pattern:/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g},{id:"SECRET006",type:"Twilio Credential",severity:"critical",recommendation:"Rotate the Twilio credential and store it in managed secrets.",pattern:/\b(SK|AC)[0-9a-fA-F]{32}\b/g},{id:"SECRET007",type:"Google API Key",severity:"critical",recommendation:"Restrict and rotate the Google API key, then move it to a cloud secret manager.",pattern:/\bAIza[0-9A-Za-z\-_]{35}\b/g},{id:"SECRET007B",type:"OpenAI Project Key",severity:"critical",recommendation:"Rotate the OpenAI project key and move it to a cloud secret manager.",pattern:/\bsk-proj-[A-Za-z0-9\-_]{20,}\b/g},{id:"SECRET007C",type:"Cartesia API Key",severity:"critical",recommendation:"Rotate the Cartesia API key and move it to a cloud secret manager.",pattern:/\bsk_car_[A-Za-z0-9]{16,}\b/g},{id:"SECRET008",type:"JWT Token",severity:"high",recommendation:"Do not hardcode JWTs; issue them dynamically and store signing secrets in managed secrets.",pattern:/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9._-]{8,}\.[A-Za-z0-9._-]{8,}\b/g},{id:"SECRET009",type:"Bearer Token",severity:"high",recommendation:"Move bearer tokens to a cloud secret manager and inject them at runtime.",pattern:/\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b/g},{id:"SECRET010",type:"Private Key",severity:"critical",recommendation:"Remove the private key from source control, rotate it, and store it in a secret manager.",pattern:/-----BEGIN (RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g},{id:"SECRET011",type:"Database Connection String with Password",severity:"critical",recommendation:"Move the connection string to managed secrets and avoid embedding passwords in code or config.",pattern:/\b(?:postgres(?:ql)?|mysql|mssql|mongodb(?:\+srv)?|redis):\/\/[^:\s]+:[^@\s]+@[^'"`\s]+\b/gi},{id:"SECRET012",type:"Webhook Secret",severity:"high",recommendation:"Store webhook secrets in a cloud secret manager and inject them via environment variables.",pattern:/\b(?:whsec_[A-Za-z0-9]+|webhook[_-]?secret[_:=\s'"]+[A-Za-z0-9_\-]{12,})\b/gi},{id:"SECRET013",type:"Hardcoded Password or Secret Assignment",severity:"high",recommendation:"Replace hardcoded credentials with environment variables backed by a cloud secret manager.",pattern:/\b(?:password|passwd|pwd|secret|client_secret|api[_-]?secret|signing[_-]?secret)\b\s*[:=]\s*['"`][^'"`\s][^'"`]{5,}['"`]/gi},{id:"SECRET014",type:"Hardcoded API Key Assignment",severity:"high",recommendation:"Move API keys to a cloud secret manager and reference them through environment variables.",pattern:/\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|token)\b\s*[:=]\s*['"`][A-Za-z0-9_\-./+=]{12,}['"`]/gi},{id:"SECRET015",type:".env File Present",severity:"medium",recommendation:"Ensure .env files are excluded from git and move production secrets to a cloud secret manager.",filePattern:/(^|[\\/])\.env(\.[^\\/]+)?$/i},{id:"SECRET016",type:"LiveKit API Secret",severity:"critical",recommendation:"Rotate the LiveKit API secret and store it in managed secrets.",pattern:/\b(?:LIVEKIT_API_SECRET|livekit[_-]?api[_-]?secret)\b\s*[:=]\s*['"`]?([A-Za-z0-9]{24,})['"`]?/gi}]}async scanAll(){const e=Date.now(),t=await this._discoverFiles(this.config.rootPath),i=[];for(const e of t)i.push(...await this.scanFile(e));return this.config.includeGitHistory&&i.push(...this.scanGitHistory()),{filesScanned:t.length,issues:i,summary:this._summarizeIssues(i),duration:Date.now()-e}}async scanFiles(e){const t=[];for(const i of e)t.push(...await this.scanFile(i));return this.config.includeGitHistory&&t.push(...this.scanGitHistory()),{filesScanned:e.length,issues:t,summary:this._summarizeIssues(t)}}async scanFile(e){const t=[],i=path.relative(this.config.rootPath,e).replace(/\\/g,"/");try{if(fs.statSync(e).size>this.config.maxFileSize)return t;const s=path.basename(e),n=fs.readFileSync(e,"utf8").split(/\r?\n/);for(const r of this.secretPatterns)if(r.filePattern&&r.filePattern.test(e)){if(this._shouldSkipLine(s,i,r))continue;this._isGitIgnored(i)||t.push(this._createIssue({file:e,line:1,type:r.type,severity:r.severity,recommendation:"Add this file to .gitignore and move any production secrets to a cloud secret manager.",id:r.id,match:s,category:"secret"}))}else if(r.pattern)for(let s=0;s<n.length;s++){const o=n[s];if(this._shouldSkipLine(o,i,r))continue;let a;for(r.pattern.lastIndex=0;null!==(a=r.pattern.exec(o));){const i=a[1]||a[0];this._isLikelyPlaceholder(i)||t.push(this._createIssue({file:e,line:s+1,column:a.index,type:r.type,severity:r.severity,recommendation:r.recommendation,id:r.id,match:a[0],category:"secret"}))}}return t.push(...this._scanEntropy(e,n,i)),this._dedupeIssues(t)}catch(t){return[{file:e,line:1,category:"secret",severity:"medium",id:"SECRET_SCAN_ERROR",type:"Secret Scan Error",recommendation:"Verify file permissions and encoding, then rerun the scan.",message:`Failed to scan file for secrets: ${t.message}`}]}}scanGitHistory(){try{const e=execFileSync("git",["log","-p","--all","--full-history","--","."],{cwd:this.config.rootPath,encoding:"utf8",maxBuffer:20971520}),t=[],i=e.split(/\r?\n/);let s=null,n=null;for(const e of i){if(e.startsWith("commit ")){s=e.replace("commit ","").trim();continue}if(e.startsWith("+++ b/")){n=e.replace("+++ b/","").trim();continue}if(!e.startsWith("+")||e.startsWith("+++"))continue;const i=e.slice(1);if(!this._shouldSkipLine(i,n||""))for(const e of this.secretPatterns){if(!e.pattern)continue;let r;for(e.pattern.lastIndex=0;null!==(r=e.pattern.exec(i));){const o=r[1]||r[0];this._isLikelyPlaceholder(o)||t.push({file:n||"unknown",line:null,category:"secret-history",severity:"medium"===e.severity?"high":e.severity,id:`${e.id}_HISTORY`,type:`${e.type} in Git History`,recommendation:"Purge the secret from git history, rotate it, and move it to a cloud secret manager.",message:`Potential secret remains in git history (${s||"unknown commit"})`,commit:s,code:i.trim().slice(0,140)})}}}return this._dedupeIssues(t)}catch(e){return[]}}_scanEntropy(e,t,i){const s=[],n=/['"`]([A-Za-z0-9+/=_\-]{20,})['"`]/g;for(let r=0;r<t.length;r++){const o=t[r];if(this._shouldSkipLine(o,i))continue;let a;for(n.lastIndex=0;null!==(a=n.exec(o));){const t=a[1];if(t.length<this.config.minEntropyLength)continue;if(!/[A-Z]/.test(t)||!/[a-z]/.test(t)||!/[0-9]/.test(t))continue;const i=this._calculateEntropy(t);i>=this.config.entropyThreshold&&s.push(this._createIssue({file:e,line:r+1,column:a.index,type:"High Entropy Secret Candidate",severity:"medium",recommendation:"Review this value, move it to a cloud secret manager if it is a credential, and reference it via environment variables.",id:"SECRET_ENTROPY",match:t,category:"secret",extraMessage:`High-entropy string detected (entropy ${i.toFixed(2)})`}))}}return s}_createIssue({file:e,line:t,column:i,type:s,severity:n,recommendation:r,id:o,match:a,category:c,extraMessage:l}){return{file:e,line:t,column:i,category:c,severity:n,id:o,type:s,recommendation:r,message:l||`${s} detected`,code:"string"==typeof a?a.slice(0,140):""}}async _discoverFiles(e,t=[]){let i=[];try{i=fs.readdirSync(e,{withFileTypes:!0})}catch{return t}for(const s of i){const i=path.join(e,s.name),n=path.relative(this.config.rootPath,i).replace(/\\/g,"/");if(this._shouldIgnore(n))continue;if(s.isDirectory()){await this._discoverFiles(i,t);continue}if(!s.isFile())continue;const r=path.extname(s.name).toLowerCase();(this.scanExtensions.has(r)||s.name.startsWith(".env"))&&t.push(i)}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),i=this.ignoredDirectories;for(const e of i)if(t===e||t.startsWith(`${e}/`)||t.includes(`/${e}/`))return!0;return this.config.ignorePatterns.some(e=>{const i=e.replace(/\\/g,"/"),s=i.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");return new RegExp(`^${s}$`).test(t)||t.startsWith(i.replace("/**",""))})}_shouldSkipLine(e,t,i=null){if(!e||!e.trim())return!0;const s=(t||"").replace(/\\/g,"/").toLowerCase();return!!(s.includes("__tests__")||s.includes("/test/")||s.includes("/tests/")||s.endsWith(".spec.js")||s.endsWith(".test.js")||s.endsWith(".spec.ts")||s.endsWith(".test.ts")||s.endsWith(".example")||s.endsWith(".env.example"))||(!i||"SECRET015"!==i.id)&&this.safeLinePatterns.some(t=>t.test(e))}_calculateEntropy(e){const t=new Map;for(const i of e)t.set(i,(t.get(i)||0)+1);let i=0;for(const s of t.values()){const t=s/e.length;i-=t*Math.log2(t)}return i}_isLikelyPlaceholder(e){return!!e&&[/replace[_-\s]?me/i,/replace[_-\s]?with/i,/example/i,/dummy/i,/placeholder/i,/changeme/i,/your[_-\s]?(key|secret|token|password)/i,/local-dev-token/i,/x{8,}/i].some(t=>t.test(e))}_isGitIgnored(e){try{return execFileSync("git",["check-ignore",e],{cwd:this.config.rootPath,stdio:"ignore"}),!0}catch{return!1}}_dedupeIssues(e){const t=new Set;return e.filter(e=>{const i=crypto.createHash("sha1").update(JSON.stringify([e.file,e.line,e.id,e.code,e.commit||""])).digest("hex");return!t.has(i)&&(t.add(i),!0)})}_summarizeIssues(e){return{total:e.length,bySeverity:{critical:e.filter(e=>"critical"===e.severity).length,high:e.filter(e=>"high"===e.severity).length,medium:e.filter(e=>"medium"===e.severity).length},byType:e.reduce((e,t)=>(e[t.type]=(e[t.type]||0)+1,e),{})}}}module.exports=SecretScanner;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");let FileWatcher,CodeScanner,DriftDetector,HealthChecker,AlertManager,SecretScanner;class SentinelCore extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),watchPatterns:e.watchPatterns||["**/*.js","**/*.ts","**/*.json"],ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],scanIntervalHours:e.scanIntervalHours||6,dailyReportHour:e.dailyReportHour||8,weeklyReportDay:e.weeklyReportDay||1,enableRealtime:!1!==e.enableRealtime,enableScheduled:!1!==e.enableScheduled,alertThreshold:e.alertThreshold||"warning",...e},this.state={isRunning:!1,lastFullScan:null,lastHealthCheck:null,lastDriftCheck:null,filesWatched:0,issuesFound:[],metrics:{totalScans:0,issuesDetected:0,issuesResolved:0,averageScanTime:0}},this.components={fileWatcher:null,codeScanner:null,driftDetector:null,healthChecker:null,alertManager:null},this.scheduledJobs=[]}async initialize(){console.log("[SENTINEL] Initializing Sentinel Core...");try{return FileWatcher=require("./file-watcher.js"),CodeScanner=require("./code-scanner.js"),DriftDetector=require("./drift-detector.js"),HealthChecker=require("./health-checker.js"),AlertManager=require("./alert-manager.js"),SecretScanner=require("./secret-scanner.js"),this.components.fileWatcher=new FileWatcher(this.config),this.components.codeScanner=new CodeScanner(this.config),this.components.driftDetector=new DriftDetector(this.config),this.components.healthChecker=new HealthChecker(this.config),this.components.alertManager=new AlertManager(this.config),this.components.secretScanner=new SecretScanner(this.config),this._setupEventHandlers(),console.log("[SENTINEL] All components initialized"),{success:!0}}catch(e){return console.error("[SENTINEL] Initialization failed:",e.message),{success:!1,error:e.message}}}async start(){if(this.state.isRunning)return console.log("[SENTINEL] Already running"),{success:!0,message:"Already running"};console.log("[SENTINEL] Starting Sentinel monitoring...");try{this.config.enableRealtime&&this.components.fileWatcher&&(await this.components.fileWatcher.start(),this.state.filesWatched=this.components.fileWatcher.getWatchedCount()),this.config.enableScheduled&&this._setupScheduledJobs(),await this.runHealthCheck();const e=await this.runFullScan();return this.state.isRunning=!0,this.emit("started",{timestamp:(new Date).toISOString()}),console.log("[SENTINEL] Monitoring active"),console.log(`[SENTINEL] Watching ${this.state.filesWatched} files`),console.log(`[SENTINEL] Initial scan found ${e.issues?.length||0} issues`),{success:!0,filesWatched:this.state.filesWatched,initialIssues:e.issues?.length||0}}catch(e){return console.error("[SENTINEL] Start failed:",e.message),{success:!1,error:e.message}}}async stop(){return this.state.isRunning?(console.log("[SENTINEL] Stopping Sentinel monitoring..."),this.components.fileWatcher&&await this.components.fileWatcher.stop(),this.scheduledJobs.forEach(e=>clearInterval(e)),this.scheduledJobs=[],this.state.isRunning=!1,this.emit("stopped",{timestamp:(new Date).toISOString()}),console.log("[SENTINEL] Monitoring stopped"),{success:!0}):{success:!0,message:"Not running"}}async runFullScan(e={}){const t=Date.now();console.log("[SENTINEL] Starting full codebase scan...");const s={timestamp:(new Date).toISOString(),duration:0,filesScanned:0,issues:[],summary:{}};try{if(!this.components.codeScanner)throw new Error("Code scanner not initialized");const e=await this.components.codeScanner.scanAll();if(s.filesScanned=e.filesScanned||0,s.issues.push(...e.issues||[]),this.components.secretScanner){const e=await this.components.secretScanner.scanAll();s.filesScanned+=e.filesScanned||0,s.issues.push(...e.issues||[])}if(this.components.driftDetector){const e=await this.components.driftDetector.detectAll();s.issues.push(...e.issues||[]),this.state.lastDriftCheck=(new Date).toISOString()}return s.duration=Date.now()-t,this.state.lastFullScan=s.timestamp,this.state.metrics.totalScans++,this.state.metrics.issuesDetected+=s.issues.length,this.state.metrics.averageScanTime=(this.state.metrics.averageScanTime*(this.state.metrics.totalScans-1)+s.duration)/this.state.metrics.totalScans,s.summary=this._generateScanSummary(s.issues),this.state.issuesFound=s.issues,await this._processIssues(s.issues),console.log(`[SENTINEL] Scan complete: ${s.filesScanned} files, ${s.issues.length} issues, ${s.duration}ms`),this.emit("scanComplete",s),s}catch(e){return console.error("[SENTINEL] Full scan failed:",e.message),s.error=e.message,s}}async runQuickScan(e){if(!this.components.codeScanner)return{success:!1,error:"Code scanner not initialized"};console.log(`[SENTINEL] Quick scan on ${e.length} files...`);const t=await this.components.codeScanner.scanFiles(e);return t.issues&&t.issues.length>0&&await this._processIssues(t.issues),t}async runHealthCheck(){if(console.log("[SENTINEL] Running health check..."),!this.components.healthChecker)return{success:!1,error:"Health checker not initialized"};const e=await this.components.healthChecker.checkAll();return this.state.lastHealthCheck=(new Date).toISOString(),e.unhealthySystems&&e.unhealthySystems.length>0&&await(this.components.alertManager?.sendAlert({level:"warning",title:"System Health Issues Detected",message:`${e.unhealthySystems.length} systems are unhealthy`,details:e.unhealthySystems})),this.emit("healthCheck",e),e}async generateDailyDigest(){console.log("[SENTINEL] Generating daily digest...");const e={date:(new Date).toISOString().split("T")[0],timestamp:(new Date).toISOString(),summary:{issuesFound:this.state.issuesFound.length,criticalIssues:this.state.issuesFound.filter(e=>"critical"===e.severity).length,warningIssues:this.state.issuesFound.filter(e=>"warning"===e.severity).length,filesWatched:this.state.filesWatched,lastScan:this.state.lastFullScan,lastHealthCheck:this.state.lastHealthCheck},topIssues:this.state.issuesFound.sort((e,t)=>this._severityScore(t.severity)-this._severityScore(e.severity)).slice(0,10),metrics:this.state.metrics,recommendations:this._generateRecommendations()};return this.components.alertManager&&await this.components.alertManager.sendDigest(e),this.emit("dailyDigest",e),e}async generateWeeklyReport(){console.log("[SENTINEL] Generating weekly report...");const e={weekOf:(new Date).toISOString().split("T")[0],timestamp:(new Date).toISOString(),summary:this.state.metrics,issuesTrend:this._calculateTrend(),codeQuality:await this._assessCodeQuality(),technicalDebt:await this._assessTechnicalDebt(),recommendations:this._generateRecommendations(),fullIssueList:this.state.issuesFound};return this.components.alertManager&&await this.components.alertManager.sendWeeklyReport(e),this.emit("weeklyReport",e),e}getStatus(){return{isRunning:this.state.isRunning,config:this.config,state:{...this.state,componentsLoaded:{fileWatcher:!!this.components.fileWatcher,codeScanner:!!this.components.codeScanner,driftDetector:!!this.components.driftDetector,healthChecker:!!this.components.healthChecker,alertManager:!!this.components.alertManager}},uptime:this.state.isRunning?Date.now()-new Date(this.state.lastFullScan).getTime():0}}getIssues(e={}){let t=[...this.state.issuesFound];return e.severity&&(t=t.filter(t=>t.severity===e.severity)),e.category&&(t=t.filter(t=>t.category===e.category)),e.file&&(t=t.filter(t=>t.file&&t.file.includes(e.file))),t}_setupEventHandlers(){this.components.fileWatcher&&(this.components.fileWatcher.on("change",async e=>{console.log(`[SENTINEL] File changed: ${e.filePath}`),await this.runQuickScan([e.filePath])}),this.components.fileWatcher.on("error",e=>{console.error("[SENTINEL] File watcher error:",e)})),this.components.codeScanner&&this.components.codeScanner.on("issueFound",async e=>{await this._processIssues([e])})}_setupScheduledJobs(){const e=60*this.config.scanIntervalHours*60*1e3;this.scheduledJobs.push(setInterval(()=>this.runFullScan(),e)),this.scheduledJobs.push(setInterval(()=>this.runHealthCheck(),36e5)),this.scheduledJobs.push(setInterval(()=>{const e=new Date;e.getHours()===this.config.dailyReportHour&&e.getMinutes()<5&&this.generateDailyDigest()},36e5)),this.scheduledJobs.push(setInterval(()=>{const e=new Date;e.getDay()===this.config.weeklyReportDay&&e.getHours()===this.config.dailyReportHour&&e.getMinutes()<5&&this.generateWeeklyReport()},36e5)),console.log(`[SENTINEL] Scheduled jobs: scan every ${this.config.scanIntervalHours}h, daily digest at ${this.config.dailyReportHour}:00`)}async _processIssues(e){for(const t of e)this._meetsThreshold(t.severity)&&this.components.alertManager&&await this.components.alertManager.sendAlert({level:t.severity,title:`[${t.category}] ${t.title||t.message}`,message:t.message,file:t.file,line:t.line,details:t})}_meetsThreshold(e){const t=["info","warning","error","critical"];return t.indexOf(e)>=t.indexOf(this.config.alertThreshold)}_severityScore(e){return{info:1,warning:2,error:3,critical:4}[e]||0}_generateScanSummary(e){return{total:e.length,bySeverity:{critical:e.filter(e=>"critical"===e.severity).length,error:e.filter(e=>"error"===e.severity).length,warning:e.filter(e=>"warning"===e.severity).length,info:e.filter(e=>"info"===e.severity).length},byCategory:e.reduce((e,t)=>(e[t.category]=(e[t.category]||0)+1,e),{})}}_generateRecommendations(){const e=[],t=this.state.issuesFound,s=t.filter(e=>"security"===e.category);s.length>0&&e.push({priority:"high",category:"security",message:`Address ${s.length} security issues before deploying`,issues:s.slice(0,5)});const n=t.filter(e=>"quality"===e.category);n.length>10&&e.push({priority:"medium",category:"quality",message:`Consider refactoring - ${n.length} quality issues detected`,issues:n.slice(0,5)});const i=t.filter(e=>"drift"===e.category);return i.length>0&&e.push({priority:"medium",category:"drift",message:`${i.length} files have drifted from their declared contracts`,issues:i.slice(0,5)}),e}_calculateTrend(){return{direction:"stable",change:0,note:"Trend analysis requires historical data"}}async _assessCodeQuality(){return{score:75,grade:"B",factors:{complexity:70,maintainability:80,documentation:65,testCoverage:40}}}async _assessTechnicalDebt(){return{estimatedHours:40,categories:{refactoring:20,documentation:10,testing:10},priorityItems:[]}}}function createSentinel(e={}){return new SentinelCore(e)}async function main(){const e=createSentinel({rootPath:path.resolve(__dirname,".."),enableRealtime:!0,enableScheduled:!1}),t=await e.initialize();t.success||(console.error("Failed to initialize Sentinel:",t.error),process.exit(1)),await e.start(),console.log("Sentinel status:",e.getStatus()),process.on("SIGINT",async()=>{console.log("\nShutting down Sentinel..."),await e.stop(),process.exit(0)})}require.main===module&&main().catch(console.error),module.exports={SentinelCore:SentinelCore,createSentinel:createSentinel};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const path=require("path"),fs=require("fs"),DependencyGraph=require("./dependency-graph"),WidgetGenerator=require("./widget-generator"),CodeScanner=require("./code-scanner");let dependencyGraph=null,widgetGenerator=null,codeScanner=null,initialized=!1,rootPath=null;async function initialize(e={}){return rootPath=e.rootPath||process.cwd(),console.log("[SENTINEL-KNOWLEDGE] Initializing codebase knowledge..."),dependencyGraph=new DependencyGraph({rootPath:rootPath}),await dependencyGraph.build(),widgetGenerator=new WidgetGenerator({rootPath:rootPath,dependencyGraph:dependencyGraph,dryRun:!0}),codeScanner=new CodeScanner({rootPath:rootPath}),initialized=!0,console.log("[SENTINEL-KNOWLEDGE] Codebase knowledge ready"),getStats()}async function ensureInitialized(){initialized||await initialize()}async function getImpact(e){await ensureInitialized();const t=path.isAbsolute(e)?e:path.join(rootPath,e);return dependencyGraph.getImpactAnalysis(t)}async function getDependencies(e){await ensureInitialized();const t=path.isAbsolute(e)?e:path.join(rootPath,e);return dependencyGraph.getDependencies(t).map(e=>({module:e.module,resolved:e.resolved?path.relative(rootPath,e.resolved):null}))}async function getDependents(e){await ensureInitialized();const t=path.isAbsolute(e)?e:path.join(rootPath,e);return dependencyGraph.getDependents(t).map(e=>path.relative(rootPath,e))}async function getCriticalFiles(e=10){return await ensureInitialized(),dependencyGraph.getMostCriticalFiles(e)}async function getModules(){return await ensureInitialized(),dependencyGraph.getModuleSummary()}async function getOrphans(){return await ensureInitialized(),dependencyGraph.getOrphanFiles()}async function getStats(){return await ensureInitialized(),{...dependencyGraph.getStats(),initialized:initialized,rootPath:rootPath}}async function query(e){await ensureInitialized();const t=e.toLowerCase();if(t.includes("break")||t.includes("impact")||t.includes("affect")){const t=e.match(/['"]?([a-zA-Z0-9\-_./\\]+\.js)['"]?/);if(t){const e=await getImpact(t[1]);return{type:"impact",file:t[1],result:e,summary:`Changing ${t[1]} could affect ${e.totalImpact} files. Risk level: ${e.riskLevel.toUpperCase()}.`}}}if(t.includes("depend")||t.includes("import")||t.includes("use")){const n=e.match(/['"]?([a-zA-Z0-9\-_./\\]+\.js)['"]?/);if(n){if(t.includes("on")||t.includes("import")){const e=await getDependencies(n[1]);return{type:"dependencies",file:n[1],result:e,summary:`${n[1]} depends on ${e.length} files: ${e.slice(0,5).map(e=>e.module).join(", ")}${e.length>5?"...":""}`}}{const e=await getDependents(n[1]);return{type:"dependents",file:n[1],result:e,summary:`${e.length} files depend on ${n[1]}: ${e.slice(0,5).join(", ")}${e.length>5?"...":""}`}}}}if(t.includes("critical")||t.includes("important")||t.includes("core")){const e=await getCriticalFiles(10);return{type:"critical",result:e,summary:`Top critical files: ${e.slice(0,5).map(e=>e.relativePath).join(", ")}`}}if(t.includes("module")||t.includes("system")||t.includes("component")){const e=await getModules();return{type:"modules",result:e,summary:`Codebase has ${e.length} modules. Largest: ${e.slice(0,3).map(e=>`${e.name} (${e.fileCount} files)`).join(", ")}`}}if(t.includes("orphan")||t.includes("dead")||t.includes("unused")){const e=await getOrphans();return{type:"orphans",result:e,summary:`Found ${e.length} potentially unused files: ${e.slice(0,5).map(e=>e.relativePath).join(", ")}${e.length>5?"...":""}`}}if(t.includes("stat")||t.includes("overview")||t.includes("summary")){const e=await getStats();return{type:"stats",result:e,summary:`Codebase: ${e.totalFiles} files, ${e.totalImports} imports, ${e.circularCount} circular dependencies`}}return{type:"unknown",result:null,summary:"I can answer questions about: file impact, dependencies, critical files, modules, orphan/dead code, and codebase stats."}}function hasWidget(e){try{const t=path.isAbsolute(e)?e:path.join(rootPath,e),n=fs.readFileSync(t,"utf8");return/\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//.test(n)}catch(e){return!1}}async function generateWidget(e,t=null){await ensureInitialized();const n=path.isAbsolute(e)?e:path.join(rootPath,e);return(await widgetGenerator.generateWidget(n,t)).widgetString}async function addWidgetToContent(e,t){return await generateWidget(e,t)+"\n\n"+t}async function refresh(){dependencyGraph&&await dependencyGraph.build()}module.exports={initialize:initialize,refresh:refresh,query:query,getImpact:getImpact,getDependencies:getDependencies,getDependents:getDependents,getCriticalFiles:getCriticalFiles,getModules:getModules,getOrphans:getOrphans,getStats:getStats,hasWidget:hasWidget,generateWidget:generateWidget,addWidgetToContent:addWidgetToContent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),CodeScanner=require("./code-scanner"),DependencyGraph=require("./dependency-graph"),DriftDetector=require("./drift-detector"),WidgetGenerator=require("./widget-generator");function formatWidgetComment(e,t){const s=path.extname(t).toLowerCase();return".py"===s?e.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"# ")).join("\n"):".go"===s?e.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"// ")).join("\n"):e}class TechDebtAnalyzer{constructor(e={}){this.config={rootPath:e.rootPath||process.cwd(),...e},this.scanner=new CodeScanner(this.config),this.depGraph=new DependencyGraph(this.config),this.driftDetector=new DriftDetector(this.config),this.widgetGen=new WidgetGenerator({...this.config,dryRun:!0})}async analyze(e){const t=Date.now();await this.depGraph.build(e),this.widgetGen.dependencyGraph=this.depGraph;const s=await this.scanner.scanFiles(e),i=(await this.driftDetector.detectAll(),{scores:{overall:100,security:100,architecture:100,codeQuality:100,documentation:100,dependencies:100,motherCode:100},fixable:[],manual:[],info:[],stats:{totalFiles:e.length,totalIssues:0,fixableCount:0,manualCount:0,estimatedFixTime:"0 minutes",scanTime:0},categories:{}}),a=[];for(const e of s.issues||[])(e.id?.startsWith("SEC")||"security"===e.category)&&a.push({file:path.relative(this.config.rootPath,e.file),...e,fixable:"SEC001"===e.id||"SEC002"===e.id,fixAction:"SEC001"===e.id||"SEC002"===e.id?"extract_to_env":null});i.scores.security=Math.max(0,100-25*a.filter(e=>"critical"===e.severity).length-10*a.filter(e=>"high"===e.severity).length),i.categories.security={score:i.scores.security,issues:a,label:"Security",icon:"shield"};const o=[],r=this.depGraph.circularDeps||[],n=this.depGraph.getOrphanFiles()||[],c=this.depGraph.getMostCriticalFiles(5)||[];for(const e of r)o.push({type:"circular_dependency",severity:"high",files:Array.isArray(e)?e.map(e=>path.relative(this.config.rootPath,e)):[String(e)],message:"Circular dependency detected",fixable:!0,fixAction:"break_circular",suggestion:"Extract shared code into a new module to break the cycle"});for(const e of n){const t="string"==typeof e?e:e.file||e.path||String(e);o.push({type:"orphan_file",severity:"low",file:path.relative(this.config.rootPath,t),message:"File has no imports and no exports consumed — potential dead code",fixable:!0,fixAction:"flag_for_removal",suggestion:"Review and remove if truly unused"})}for(const e of c){const t="string"==typeof e?e:e.file||e.path||String(e),s="object"==typeof e&&(e.dependents||e.score||e.count)||0;s>10&&o.push({type:"god_file",severity:"medium",file:path.relative(this.config.rootPath,t),dependents:s,message:`${s} files depend on this — high blast radius if changed`,fixable:!1,suggestion:"Consider splitting into smaller, focused modules"})}i.scores.architecture=Math.max(0,100-15*r.length-1*n.length-5*o.filter(e=>"god_file"===e.type).length),i.categories.architecture={score:i.scores.architecture,issues:o,label:"Architecture",icon:"building"};const l=[],d=new(require("./hallucination-detector"))(this.config),u=await d.scan(e);for(const e of u.deprecatedAPIs||[])l.push({file:path.relative(this.config.rootPath,e.file),type:"deprecated_api",severity:"high",message:`${e.name} — deprecated since ${e.since}`,fixable:!0,fixAction:"update_deprecated_api",suggestion:e.fix});for(const e of u.aiSmells||[])e.message&&e.message.includes("localhost")&&l.push({file:path.relative(this.config.rootPath,e.file),type:"hardcoded_localhost",severity:"medium",message:e.message,fixable:!0,fixAction:"wrap_localhost_url",suggestion:"Use environment variable with localhost fallback"});for(const e of s.issues||[])(e.id?.startsWith("QUAL")||"quality"===e.category||"maintainability"===e.category)&&l.push({file:path.relative(this.config.rootPath,e.file),...e,fixable:["QUAL001","QUAL003","QUAL005"].includes(e.id),fixAction:"QUAL001"===e.id?"remove_console_log":null});i.scores.codeQuality=Math.max(0,100-5*l.filter(e=>"high"===e.severity).length-2*l.filter(e=>"medium"===e.severity).length),i.categories.codeQuality={score:i.scores.codeQuality,issues:l,label:"Code Quality",icon:"code"};const f=[];let h=0,p=0,g=0;for(const t of e)try{const e=fs.readFileSync(t,"utf-8");if(/\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//.test(e)){h++;const e=await this.driftDetector.analyzeFile(t);if(e.issues&&e.issues.length>0){g++;for(const s of e.issues)f.push({file:path.relative(this.config.rootPath,t),...s,fixable:!0,fixAction:"regenerate_widget"})}}else p++,f.push({file:path.relative(this.config.rootPath,t),type:"missing_mother_code",severity:"info",message:"No Mother Code DNA — file lacks contextual awareness",fixable:!0,fixAction:"inject_widget"})}catch(e){}const m=e.length>0?Math.round(h/e.length*100):0;i.scores.motherCode=Math.min(100,m+(g>0?-10:0)),i.scores.documentation=i.scores.motherCode,i.categories.motherCode={score:i.scores.motherCode,coverage:m,annotated:h,missing:p,drifted:g,issues:f,label:"Mother Code",icon:"dna"},this.depGraph.getModuleSummary&&this.depGraph.getModuleSummary(),i.scores.dependencies=Math.max(0,100-10*r.length-.5*n.length),i.categories.dependencies={score:i.scores.dependencies,issues:[],label:"Dependencies",icon:"link"};i.scores.overall=Math.round(.25*i.scores.security+.2*i.scores.architecture+.2*i.scores.codeQuality+.2*i.scores.motherCode+.15*i.scores.dependencies);for(const e of Object.values(i.categories))for(const t of e.issues||[])i.stats.totalIssues++,t.fixable?(i.fixable.push(t),i.stats.fixableCount++):"info"===t.severity?i.info.push(t):(i.manual.push(t),i.stats.manualCount++);const $=.5*i.stats.fixableCount+15*i.stats.manualCount;return i.stats.estimatedFixTime=$<60?`${Math.round($)} minutes`:`${Math.round($/60)} hours`,i.stats.scanTime=Date.now()-t,i}async fix(e,t={}){const s=await this.analyze(e),i=[],a=[];for(const e of s.fixable)if(t.dryRun)i.push({...e,status:"would_fix"});else try{switch(e.fixAction){case"inject_widget":{const t=path.resolve(this.config.rootPath,e.file),s=fs.readFileSync(t,"utf-8"),a=await this.widgetGen.generateWidget(t,s);if(a&&a.widgetString){const o=formatWidgetComment(a.widgetString,t);fs.writeFileSync(t,o+"\n\n"+s,"utf-8"),i.push({...e,status:"fixed"})}break}case"regenerate_widget":{const t=path.resolve(this.config.rootPath,e.file),s=fs.readFileSync(t,"utf-8").replace(/\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\/\s*\n?\s*\n?/,""),a=await this.widgetGen.generateWidget(t,s);if(a&&a.widgetString){const o=formatWidgetComment(a.widgetString,t);fs.writeFileSync(t,o+"\n\n"+s,"utf-8"),i.push({...e,status:"fixed"})}break}case"remove_console_log":{const t=path.resolve(this.config.rootPath,e.file);let s=fs.readFileSync(t,"utf-8");const a=s;s=s.replace(/^\s*console\.log\(.*?\);\s*$/gm,""),s!==a&&(fs.writeFileSync(t,s,"utf-8"),i.push({...e,status:"fixed"}));break}case"update_deprecated_api":{const t=path.resolve(this.config.rootPath,e.file);let s=fs.readFileSync(t,"utf-8");const a=s;s=s.replace(/url\.parse\(([^)]+)\)/g,"new URL($1)"),s=s.replace(/new Buffer\(([^)]+)\)/g,"Buffer.from($1)"),s=s.replace(/require\(['"]sys['"]\)/g,"require('util')"),s=s.replace(/util\.isArray\(/g,"Array.isArray("),s=s.replace(/fs\.exists\(([^,]+),\s*/g,"fs.access($1, "),s!==a?(fs.writeFileSync(t,s,"utf-8"),i.push({...e,status:"fixed",detail:"Replaced deprecated API with modern equivalent"})):i.push({...e,status:"skipped",reason:"Pattern not matched for auto-replacement"});break}case"wrap_localhost_url":{const t=path.resolve(this.config.rootPath,e.file);let s=fs.readFileSync(t,"utf-8");const a=s;s=s.replace(/(['"`])(https?:\/\/localhost:\d{2,5}(?:\/[^'"`]*)?)\1/g,(e,t,s)=>{const i=s.match(/:(\d+)/);return`(process.env.SERVICE_URL_${i?i[1]:"PORT"} || ${t}${s}${t})`}),s!==a?(fs.writeFileSync(t,s,"utf-8"),i.push({...e,status:"fixed",detail:"Wrapped localhost URL in env var fallback"})):i.push({...e,status:"skipped",reason:"Pattern not matched for auto-replacement"});break}case"extract_to_env":i.push({...e,status:"flagged_for_review",reason:"Secrets require manual extraction to .env"});break;case"flag_for_removal":i.push({...e,status:"confirmed_orphan",reason:"Verified no imports. Safe to remove manually."});break;default:i.push({...e,status:"skipped",reason:"No auto-fix available"})}}catch(t){a.push({...e,status:"failed",error:t.message})}return{totalFixable:s.fixable.length,fixed:i.filter(e=>"fixed"===e.status).length,flagged:i.filter(e=>"flagged_for_review"===e.status||"confirmed_orphan"===e.status).length,skipped:i.filter(e=>"skipped"===e.status).length,failed:a.length,fixes:i,failures:a,debtBefore:s.scores.overall,...t.dryRun?{}:{note:"Run thuban report again to see updated scores"}}}formatReport(e){const t="[0m",s="[1m",i="[31m",a="[32m",o="[33m",r="[36m",n="[90m",c=e=>{const s=Math.round(e/5),r=20-s,c=e>=80?a:e>=60?o:i;return`${c}${"█".repeat(s)}${n}${"░".repeat(r)}${t} ${c}${e}%${t}`};let l="";var d;l+=`\n${r} ╔══════════════════════════════════════════════╗${t}\n`,l+=`${r} ║${s} THUBAN TECH DEBT REPORT ${t}${r}║${t}\n`,l+=`${r} ╚══════════════════════════════════════════════╝${t}\n\n`,l+=` ${s}Overall Health:${t} ${c(e.scores.overall)} Grade: ${d=e.scores.overall,d>=90?`${a}A${t}`:d>=80?`${a}B${t}`:d>=70?`${o}C${t}`:d>=60?`${o}D${t}`:`${i}F${t}`}\n\n`,l+=` ${s}Category Breakdown:${t}\n\n`;const u=[["Security",e.scores.security,"shield"],["Architecture",e.scores.architecture,"building"],["Code Quality",e.scores.codeQuality,"code"],["Mother Code",e.scores.motherCode,"dna"],["Dependencies",e.scores.dependencies,"link"]];for(const[e,t]of u)l+=` ${e.padEnd(18)} ${c(t)}\n`;if(l+="\n",e.categories.motherCode){const n=e.categories.motherCode;l+=` ${s}Mother Code Coverage:${t}\n`,l+=` Annotated: ${r}${n.annotated}${t} | Missing: ${o}${n.missing}${t} | Drifted: ${n.drifted>0?i:a}${n.drifted}${t} | Coverage: ${n.coverage>=80?a:n.coverage>=40?o:i}${n.coverage}%${t}\n\n`}if(l+=` ${s}Tech Debt Summary:${t}\n`,l+=` Total issues: ${s}${e.stats.totalIssues}${t}\n`,l+=` Auto-fixable: ${a}${e.stats.fixableCount}${t} ${n}← run 'thuban fix' to resolve${t}\n`,l+=` Manual review: ${o}${e.stats.manualCount}${t}\n`,l+=` Est. manual time: ${n}${e.stats.estimatedFixTime}${t}\n`,l+=` Scan time: ${n}${e.stats.scanTime}ms${t}\n\n`,e.fixable.length>0){l+=` ${s}Top Auto-Fixable Items:${t}\n`;const i={};for(const t of e.fixable){const e=t.fixAction||"unknown";i[e]||(i[e]={count:0,label:e}),i[e].count++}for(const[e,o]of Object.entries(i).sort((e,t)=>t[1].count-e[1].count))l+=` ${a}→${t} ${{inject_widget:"Inject Mother Code DNA",regenerate_widget:"Update stale annotations",remove_console_log:"Remove debug console.logs",extract_to_env:"Extract hardcoded secrets to .env",flag_for_removal:"Remove orphan files",break_circular:"Break circular dependencies"}[e]||e}: ${s}${o.count}${t} items\n`;l+=`\n ${r}Run 'thuban fix [path]' to auto-fix all ${e.stats.fixableCount} items${t}\n\n`}if(e.manual.length>0){l+=` ${s}Manual Review Required:${t}\n`;for(const s of e.manual.slice(0,5))l+=` ${"critical"===s.severity||"high"===s.severity?i:o}${(s.severity||"").toUpperCase().padEnd(8)}${t} ${r}${s.file||""}${t}\n`,l+=` ${n} ${s.message||s.suggestion||""}${t}\n`;e.manual.length>5&&(l+=` ${n} ... and ${e.manual.length-5} more${t}\n`),l+="\n"}return l}toJSON(e){return{timestamp:(new Date).toISOString(),scores:e.scores,categories:Object.fromEntries(Object.entries(e.categories).map(([e,t])=>[e,{score:t.score,label:t.label,issueCount:t.issues?.length||0,coverage:t.coverage}])),stats:e.stats,fixable:e.fixable.map(e=>({file:e.file,action:e.fixAction,severity:e.severity})),manual:e.manual.map(e=>({file:e.file,message:e.message,severity:e.severity}))}}}module.exports=TechDebtAnalyzer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),COST_MODEL={hourlyRate:80,weeklyDriftHours:3,manualFixMinutes:{hallucination:30,deprecated:15,security:45,circular_dep:60,orphan_file:5,missing_annotation:2,copy_paste:20,ghost_code:10,code_smell:10,drift:15},thubanFixMinutes:{hallucination:.5,deprecated:.5,security:5,circular_dep:5,orphan_file:.5,missing_annotation:.1,copy_paste:2,ghost_code:.5,code_smell:1,drift:.5}};class TechDebtCostCalculator{constructor(t={}){this.hourlyRate=t.hourlyRate||COST_MODEL.hourlyRate,this.currency=t.currency||"£"}calculate(t,a=0){const n={};let e=0,o=0;for(const a of t){const t=this._categorize(a);n[t]||(n[t]={count:0,manualMinutes:0,thubanMinutes:0});const u=COST_MODEL.manualFixMinutes[t]||10,s=COST_MODEL.thubanFixMinutes[t]||1;n[t].count++,n[t].manualMinutes+=u,n[t].thubanMinutes+=s,e+=u,o+=s}const u=e/60,s=o/60,r=u*this.hourlyRate,h=s*this.hourlyRate,i=r-h,l=r>0?Math.round(i/r*100):0,c=a/1e3*COST_MODEL.weeklyDriftHours,d=4*c*this.hourlyRate,p=i>0?Math.round(i/19):0;return{summary:{totalIssues:t.length,manualHours:Math.round(10*u)/10,manualCost:Math.round(r),thubanHours:Math.round(10*s)/10,thubanCost:Math.round(h),savings:Math.round(i),savingsPercent:l,timeSaved:this._formatTime(e-o)},breakdown:Object.entries(n).map(([t,a])=>({category:t,count:a.count,manualTime:this._formatTime(a.manualMinutes),thubanTime:this._formatTime(a.thubanMinutes),manualCost:Math.round(a.manualMinutes/60*this.hourlyRate),thubanCost:Math.round(a.thubanMinutes/60*this.hourlyRate)})).sort((t,a)=>a.manualCost-t.manualCost),projection:{weeklyNewDebtHours:Math.round(10*c)/10,monthlyNewDebtCost:Math.round(d),monthlyThubanCost:19,monthlyROI:`${p}x`,annualSavings:Math.round(i+12*d),verdict:p>=5?"Thuban pays for itself within the first scan.":p>=2?"Thuban pays for itself within the first week.":"Thuban will save you money within the first month."}}}formatCLI(t){const a=[],n=t.summary,e=t.projection,o=this.currency;a.push(""),a.push(" TECH DEBT COST ESTIMATE"),a.push(""),a.push(` Current debt: ${n.manualHours} developer-hours to fix manually`),a.push(` At ${o}${this.hourlyRate}/hr: ${o}${n.manualCost.toLocaleString()} if you fix it yourself`),a.push(` With Thuban: ${n.thubanHours} hours (${n.timeSaved} saved)`),a.push(` You save: ${o}${n.savings.toLocaleString()} (${n.savingsPercent}%)`),a.push(""),a.push(" BREAKDOWN"),a.push(` ${"Category".padEnd(20)} ${"Count".padEnd(8)} ${"Manual".padEnd(12)} ${"Thuban".padEnd(12)} ${"Saving".padEnd(10)}`),a.push(" "+"-".repeat(62));for(const n of t.breakdown){const t=`${o}${n.manualCost-n.thubanCost}`;a.push(` ${n.category.padEnd(20)} ${String(n.count).padEnd(8)} ${(o+n.manualCost).padEnd(12)} ${(o+n.thubanCost).padEnd(12)} ${t.padEnd(10)}`)}return a.push(""),a.push(" PROJECTION"),a.push(` Every week you wait, ${e.weeklyNewDebtHours} hours of new debt accumulates.`),a.push(` That's ${o}${e.monthlyNewDebtCost}/month in growing technical debt.`),a.push(` Thuban Pro costs ${o}${e.monthlyThubanCost}/month. ROI: ${e.monthlyROI}.`),a.push(""),a.push(` ${e.verdict}`),a.push(""),a.join("\n")}_categorize(t){const a=(t.category||"").toLowerCase(),n=(t.id||"").toLowerCase();return"hallucination"===a||n.startsWith("hall")?"hallucination":"deprecated"===a||n.startsWith("depr")?"deprecated":"security"===a?"security":"circular"===a||n.includes("circular")?"circular_dep":"orphan"===a||n.includes("orphan")?"orphan_file":"drift"===a?"drift":"ghost"===a||n.includes("ghost")?"ghost_code":"copy_paste"===a||n.includes("duplicate")?"copy_paste":"annotation"===a||n.includes("mother")?"missing_annotation":"code_smell"}_formatTime(t){if(t<60)return`${Math.round(t)} mins`;const a=Math.floor(t/60),n=Math.round(t%60);return 0===n?`${a}h`:`${a}h ${n}m`}}module.exports=TechDebtCostCalculator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),DependencyGraph=require("./dependency-graph");class WidgetGenerator{constructor(e={}){this.config={rootPath:e.rootPath||process.cwd(),dryRun:!1!==e.dryRun,...e},this.dependencyGraph=e.dependencyGraph||null,this.modulePatterns={sentinel:/^sentinel[\\\/]/,citadel:/^citadel[\\\/]/,forge:/^forge[\\\/]/,server:/^server[\\\/]/,"build-engine":/^build-engine[\\\/]/,autopilot:/^autopilot[\\\/]/,silverwings:/^silverwings[\\\/]/,output:/^output[\\\/]/},this.layerPatterns={presentation:/routes|api|handlers|controllers|views/i,application:/service|manager|orchestrat|core|engine/i,domain:/model|entity|domain|types/i,infrastructure:/utils|helpers|lib|data|store|memory/i},this.sideEffectPatterns=[{pattern:/fs\.(write|append|unlink|mkdir|rmdir|rename)/i,effect:"File system writes"},{pattern:/fs\.read/i,effect:"File system reads"},{pattern:/fetch\(|axios\.|http\.|https\./i,effect:"HTTP requests"},{pattern:/process\.exit/i,effect:"Process termination"},{pattern:/child_process|spawn|exec\(/i,effect:"Spawns child processes"},{pattern:/console\.(log|error|warn)/i,effect:"Console output"},{pattern:/\.emit\(|EventEmitter/i,effect:"Event emission"},{pattern:/supabase|createClient/i,effect:"Database operations"},{pattern:/twilio|sendgrid|resend/i,effect:"External API calls"}],this.purposePatterns=[{pattern:/class\s+(\w+).*extends.*Router/i,purpose:e=>`Express router for ${e[1]}`},{pattern:/class\s+(\w+).*extends.*EventEmitter/i,purpose:e=>`Event-driven ${e[1]} component`},{pattern:/class\s+(\w+)/i,purpose:e=>`${e[1]} class implementation`},{pattern:/module\.exports\s*=\s*\{([^}]+)\}/i,purpose:e=>`Exports: ${e[1].match(/\w+/g)?.slice(0,5).join(", ")}`},{pattern:/router\.(get|post|put|delete)/i,purpose:()=>"API route handlers"},{pattern:/async function\s+(\w+)/i,purpose:e=>`Provides ${e[1]} functionality`},{pattern:/function\s+(\w+)/i,purpose:e=>`Provides ${e[1]} functionality`}]}async generateAll(){console.log("[WIDGET-GENERATOR] Starting widget generation..."),this.dependencyGraph||(this.dependencyGraph=new DependencyGraph({rootPath:this.config.rootPath}),await this.dependencyGraph.build());const e=await this._discoverFiles(this.config.rootPath),t={total:e.length,generated:0,skipped:0,alreadyHasWidget:0,widgets:[]};for(const s of e){const e=fs.readFileSync(s,"utf8");if(this._hasWidget(e)){t.alreadyHasWidget++;continue}const r=await this.generateWidget(s,e);t.generated++,t.widgets.push({file:s,relativePath:path.relative(this.config.rootPath,s),widget:r}),this.config.dryRun||await this._applyWidget(s,e,r)}return console.log(`[WIDGET-GENERATOR] Generated ${t.generated} widgets (${t.alreadyHasWidget} already had widgets)`),t}async generateWidget(e,t=null){t||(t=fs.readFileSync(e,"utf8"));const s=path.relative(this.config.rootPath,e),r=path.basename(e,path.extname(e)),i={purpose:this._detectPurpose(t,r,s),module:this._detectModule(s),layer:this._detectLayer(s,t),importsFrom:this._getImportsFrom(e),exportsTo:this._getExportsTo(e),sideEffects:this._detectSideEffects(t),criticalFor:this._detectCriticalFor(e)};return{analysis:i,widgetString:this._buildWidgetString(i,r),fileName:r,relativePath:s}}async preview(e=20){const t=await this.generateAll();console.log("\n"+"=".repeat(70)),console.log("WIDGET GENERATION PREVIEW"),console.log("=".repeat(70)),console.log(`Total files: ${t.total}`),console.log(`Already have widgets: ${t.alreadyHasWidget}`),console.log(`Will generate: ${t.generated}`),console.log("");for(const s of t.widgets.slice(0,e))console.log("-".repeat(70)),console.log(`FILE: ${s.relativePath}`),console.log("-".repeat(70)),console.log(s.widget.widgetString),console.log("");return t.widgets.length>e&&console.log(`... and ${t.widgets.length-e} more files`),t}async apply(){this.config.dryRun=!1;const e=await this.generateAll();return console.log(`[WIDGET-GENERATOR] Applied ${e.generated} widgets to files`),e}_hasWidget(e){return/\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//.test(e)}_detectPurpose(e,t,s){for(const{pattern:t,purpose:s}of this.purposePatterns){const r=e.match(t);if(r)return"function"==typeof s?s(r):s}const r=t.replace(/[-_]/g," ").replace(/([A-Z])/g," $1").trim();return t.includes("route")?`Route handlers for ${r}`:t.includes("api")?`API endpoints for ${r}`:t.includes("service")?`Service layer for ${r}`:t.includes("manager")?`Manager for ${r}`:t.includes("store")?`State store for ${r}`:t.includes("util")?`Utility functions for ${r}`:t.includes("helper")?`Helper functions for ${r}`:t.includes("test")?`Tests for ${r}`:t.includes("config")?`Configuration for ${r}`:t.includes("index")?`Entry point for ${path.dirname(s).split(path.sep).pop()}`:`Provides ${r} functionality`}_detectModule(e){const t=e.replace(/\\/g,"/");for(const[e,s]of Object.entries(this.modulePatterns))if(s.test(t))return e;return t.split("/")[0]||"root"}_detectLayer(e,t){for(const[t,s]of Object.entries(this.layerPatterns))if(s.test(e))return t;return/router\.(get|post|put|delete)|app\.(get|post)/i.test(t)?"presentation":/class.*Service|class.*Manager/i.test(t)?"application":/class.*Model|schema|entity/i.test(t)?"domain":"application"}_getImportsFrom(e){if(!this.dependencyGraph)return[];const t=this.dependencyGraph.getDependencies(e),s=[];for(const e of t.slice(0,10))if(e.resolved){const t=path.relative(this.config.rootPath,e.resolved).split(path.sep),r=t[0],i=t[t.length-1];s.push(`${r}/${i}`)}else e.module.startsWith(".")||s.push(e.module);return s}_getExportsTo(e){if(!this.dependencyGraph)return[];const t=this.dependencyGraph.getDependents(e),s=[];for(const e of t.slice(0,10)){const t=path.relative(this.config.rootPath,e).split(path.sep)[0];s.push(t)}return[...new Set(s)]}_detectSideEffects(e){const t=[];for(const{pattern:s,effect:r}of this.sideEffectPatterns)s.test(e)&&t.push(r);return t.length>0?t:["None detected"]}_detectCriticalFor(e){if(!this.dependencyGraph)return[];const t=this.dependencyGraph.getImpactAnalysis(e);return"critical"===t.riskLevel?[`CRITICAL: ${t.totalImpact} files depend on this`]:"high"===t.riskLevel?[`High impact: ${t.totalImpact} files depend on this`]:t.affectedModules.length>0?t.affectedModules.slice(0,5):[]}_buildWidgetString(e,t){const s=["/**",` * ${this._toTitleCase(t)}`," *",` * @purpose ${e.purpose}`,` * @module ${e.module}`,` * @layer ${e.layer}`];return e.importsFrom.length>0&&s.push(` * @imports-from ${e.importsFrom.join(", ")}`),e.exportsTo.length>0&&s.push(` * @exports-to ${e.exportsTo.join(", ")}`),s.push(` * @side-effects ${e.sideEffects.join(", ")}`),e.criticalFor.length>0&&s.push(` * @critical-for ${e.criticalFor.join(", ")}`),s.push(" */"),s.join("\n")}_toTitleCase(e){return e.replace(/[-_]/g," ").replace(/([A-Z])/g," $1").trim().split(" ").map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(" ")}async _applyWidget(e,t,s){const r=s.widgetString+"\n\n"+t;try{return fs.writeFileSync(e,r,"utf8"),!0}catch(t){return console.error(`[WIDGET-GENERATOR] Failed to write widget to ${e}:`,t.message),!1}}async _discoverFiles(e,t=[]){const s=["node_modules",".git","dist",".vite",".orion"];try{const r=fs.readdirSync(e,{withFileTypes:!0});for(const i of r){if(s.some(e=>i.name===e||i.name.startsWith(e)))continue;const r=path.join(e,i.name);i.isDirectory()?await this._discoverFiles(r,t):i.isFile()&&i.name.endsWith(".js")&&t.push(r)}}catch(e){}return t}}module.exports=WidgetGenerator;
|