thuban 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -102
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +16 -8
- package/cli.js +0 -2412
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -713
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -250
- package/packages/scanner/dependency-graph.js +0 -536
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -226
- package/packages/scanner/hallucination-detector.js +0 -652
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -92
- package/packages/scanner/license-manager.js +0 -318
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/sentinel-core.js +0 -608
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -565
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path=require("path"),fs=require("fs"),os=require("os"),CodeScanner=require("./packages/scanner/code-scanner.js"),DependencyGraph=require("./packages/scanner/dependency-graph.js"),DriftDetector=require("./packages/scanner/drift-detector.js"),WidgetGenerator=require("./packages/scanner/widget-generator.js"),AlertManager=require("./packages/scanner/alert-manager.js"),TechDebtAnalyzer=require("./packages/scanner/tech-debt-analyzer.js"),HallucinationDetector=require("./packages/scanner/hallucination-detector.js"),HtmlReportGenerator=require("./packages/scanner/html-report.js"),FileWatcher=require("./packages/scanner/file-watcher.js"),LicenseManager=require("./packages/scanner/license-manager.js"),PreCommitGate=require("./packages/scanner/pre-commit-gate.js"),TechDebtCostCalculator=require("./packages/scanner/tech-debt-cost.js"),GhostCodeDetector=require("./packages/scanner/ghost-code-detector.js"),AIConfidenceScorer=require("./packages/scanner/ai-confidence-scorer.js"),CopyPasteDriftDetector=require("./packages/scanner/copy-paste-detector.js"),CodebasePassport=require("./packages/scanner/codebase-passport.js"),ExecutiveReport=require("./packages/scanner/executive-report.js"),BaselineManager=require("./packages/scanner/baseline-manager.js"),MonitorStore=require("./packages/scanner/monitor-store.js"),{MonitorService:MonitorService,resolveIntervalMs:resolveIntervalMs}=require("./packages/scanner/monitor-service.js"),{collectFiles:collectFiles}=require("./packages/scanner/file-collector.js"),COLORS={reset:"[0m",bold:"[1m",red:"[31m",green:"[32m",yellow:"[33m",blue:"[34m",cyan:"[36m",gray:"[90m",white:"[37m",bgRed:"[41m",bgGreen:"[42m",bgYellow:"[43m"};function c(e,o){return`${COLORS[e]}${o}${COLORS.reset}`}function banner(){console.log(""),console.log(c("cyan"," ╔══════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," THUBAN Code Health Engine ")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," v0.3.3 · Public Beta · thuban.dev ")+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════╝")),console.log("")}function printHelp(){banner();const e=(new LicenseManager).getStatus(),o=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8"));console.log(c("gray",` v${o.version} · ${e.tierName} tier`)+(e.licensed?c("green"," ✓"):"")+c("gray"," · https://thuban.dev")),console.log(""),console.log(c("bold"," QUICK START")),console.log(""),console.log(c("gray"," Run these in order to see what Thuban does:")),console.log(""),console.log(` ${c("cyan","npx thuban scan .")} Scan your project`),console.log(` ${c("cyan","npx thuban fix .")} Preview what can be fixed`),console.log(` ${c("cyan","npx thuban fix . --fix")} Apply all safe fixes`),console.log(` ${c("cyan","npx thuban dashboard .")} Open visual HTML report`),console.log(""),console.log(c("bold"," SCAN & DETECT")),console.log(""),console.log(` ${c("cyan","scan")} [path|url] Full health scan — score, grade, all issues`),console.log(` ${c("cyan","compare")} [a] [b] Compare two codebases side by side`),console.log(` ${c("cyan","hallucinate")} [path] Find phantom APIs and invented imports`),console.log(` ${c("cyan","ghosts")} [path] Dead functions nobody calls`),console.log(` ${c("cyan","ai-score")} [path] Probability each function is AI-generated`),console.log(` ${c("cyan","clones")} [path] Copy-pasted code that has drifted apart`),console.log(` ${c("cyan","drift")} [path] Files that changed since Mother Code was set`),console.log(` ${c("cyan","deps")} [path] Dependency map — circular, orphan, critical`),console.log(` ${c("cyan","diff")} [path] Scan only git-changed files`),console.log(` ${c("cyan","debt")} [path] Detailed tech debt breakdown`),console.log(` ${c("cyan","cost")} [path] Tech debt translated to £ and hours`),console.log(""),console.log(c("bold"," FIX & PROTECT")),console.log(""),console.log(` ${c("cyan","fix")} [path] Preview fixable issues ${c("gray","(safe, no changes)")}`),console.log(` ${c("cyan","fix")} [path] ${c("gray","--fix")} Apply all safe fixes`),console.log(` ${c("cyan","fix")} [path] ${c("gray","--fix --commit")} Each fix = 1 git commit ${c("gray","(revert any)")}`),console.log(` ${c("cyan","inject")} [path] Add Mother Code DNA to every file`),console.log(` ${c("cyan","gate")} Install pre-commit hook ${c("gray","(blocks hallucinations)")}`),console.log(` ${c("cyan","watch")} [path] Live sentinel — scans files as you save`),console.log(` ${c("cyan","monitor")} [path] Recurring scans with saved history and alerts ${c("gray","(Pro)")}`),console.log(` ${c("cyan","history")} [path] View recurring scan history for a repo ${c("gray","(Pro)")}`),console.log(` ${c("cyan","trend")} [path] Show issue trends across recurring scans ${c("gray","(Pro)")}`),console.log(` ${c("cyan","baseline")} [path] Snapshot issues so future scans show only NEW ones`),console.log(""),console.log(c("bold"," REPORTS")),console.log(""),console.log(` ${c("cyan","dashboard")} [path] Interactive HTML report ${c("gray","(share with your team)")}`),console.log(` ${c("cyan","executive")} [path] CTO-ready report ${c("gray","(print to PDF from browser)")}`),console.log(` ${c("cyan","report")} [path] Combined scan + deps + drift summary`),console.log(` ${c("cyan","passport")} [path] Codebase identity card ${c("gray","(for new team members)")}`),console.log(""),console.log(c("bold"," LICENSE")),console.log(""),console.log(` ${c("cyan","activate")} <key> Activate a license key`),console.log(` ${c("cyan","status")} Show license, usage, and features`),console.log(` ${c("cyan","upgrade")} Open pricing page`),console.log(""),console.log(c("bold"," OPTIONS")),console.log(""),console.log(` ${c("gray","--fix")} Apply changes ${c("gray","(default is dry-run preview)")}`),console.log(` ${c("gray","--commit")} Auto-commit each fix for easy rollback`),console.log(` ${c("gray","--unsafe")} Include fixes that change runtime behaviour`),console.log(` ${c("gray","--json")} Machine-readable JSON output`),console.log(` ${c("gray","--baseline")} Only show NEW issues vs saved baseline`),console.log(` ${c("gray","--verbose")} Show detailed output`),console.log(` ${c("gray","--ignore <pat>")} Skip files matching pattern`),console.log(` ${c("gray","--max-issues <n>")} Limit displayed issues ${c("gray","(default: 50)")}`),console.log(""),console.log(c("bold"," EXAMPLES")),console.log(""),console.log(c("gray"," # Scan the current project")),console.log(` ${c("white","npx thuban scan .")}`),console.log(""),console.log(c("gray"," # Find and fix all AI hallucinations")),console.log(` ${c("white","npx thuban hallucinate .")}`),console.log(` ${c("white","npx thuban fix . --fix")}`),console.log(""),console.log(c("gray"," # Generate a report for your CTO")),console.log(` ${c("white","npx thuban executive .")}`),console.log(""),console.log(c("gray"," # Block hallucinated code on every commit")),console.log(` ${c("white","npx thuban gate --fix")}`),console.log(""),console.log(c("gray"," # Scan a specific folder, ignore tests")),console.log(` ${c("white",'npx thuban scan ./src --ignore "**/*.test.js"')}`),console.log(""),console.log(c("gray"," Report a bug or request a feature:")),console.log(` ${c("cyan","https://github.com/SilverwingsBenefitsGit/thuban/issues")}`),console.log("")}function parseArgs(e){const o={command:null,targetPath:process.cwd(),json:!1,fix:!1,dryRun:!1,safe:!0,gitCommit:!1,verbose:!1,ignore:[],maxIssues:50,licenseKey:null,baseline:!1,baselineCreate:!1,frequency:"daily",intervalMs:null,notify:"inbox",once:!1},n=["activate","status","upgrade","help","version"];let c=0;for(;c<e.length;){const l=e[c];"--json"===l?o.json=!0:"--fix"===l?o.fix=!0:"--dry-run"===l||"--dryrun"===l?o.dryRun=!0:"--unsafe"===l?o.safe=!1:"--git-commit"===l||"--commit"===l?o.gitCommit=!0:"--baseline"===l?o.baseline=!0:"--baseline-create"===l?o.baselineCreate=!0:"--frequency"===l&&e[c+1]?o.frequency=e[++c]:"--interval-ms"===l&&e[c+1]?o.intervalMs=parseInt(e[++c],10):"--notify"===l&&e[c+1]?o.notify=e[++c]:"--once"===l?o.once=!0:"--verbose"===l?o.verbose=!0:"--ignore"===l&&e[c+1]?o.ignore.push(e[++c]):"--max-issues"===l&&e[c+1]?o.maxIssues=parseInt(e[++c],10):"--help"===l||"-h"===l?o.command="help":"--version"===l||"-v"===l?o.command="version":o.command?"activate"!==o.command||o.licenseKey?n.includes(o.command)||(l.match(/^(https?:\/\/|git@)/)?o.targetPath=l:o.targetPath=path.resolve(l)):o.licenseKey=l:o.command=l,c++}return o}function summarizeRemainingFiles(e,o,n){const c=new Set(e),l=o.filter(e=>!c.has(e)),s=new Map;for(const e of l){const o=path.relative(n,e).split(path.sep).filter(Boolean),c=o.length>1?o[0]:"(root)";s.set(c,(s.get(c)||0)+1)}const t=Array.from(s.entries()).sort((e,o)=>o[1]-e[1]||e[0].localeCompare(o[0])).slice(0,3).map(([e,o])=>({directory:e,count:o}));return{remainingFiles:l,remainingCount:l.length,topDirectories:t,previewFiles:l.slice(0,5).map(e=>path.relative(n,e))}}function requireProFeature(e){const o=new LicenseManager;if(!o.canUseProFeature(e).allowed){const o="https://thuban.dev/pricing";console.log(""),console.log(c("yellow"," ╔══════════════════════════════════════════════════════════════╗")),console.log(c("yellow"," ║")+c("bold"," PRO FEATURE REQUIRED ")+c("yellow","║")),console.log(c("yellow"," ╠══════════════════════════════════════════════════════════════╣")),console.log(c("yellow"," ║")+` ${e} is available on Thuban Pro.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Free Thuban gives you a one-off checkup. Pro gives you".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" a pair of eyes on your codebase all day every day with".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" recurring scans, saved history, trends, and alerts.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" "+c("yellow","║")),console.log(c("yellow"," ║")+` Upgrade now: ${o}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" CLI shortcut: thuban upgrade".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ╚══════════════════════════════════════════════════════════════╝")),console.log(""),process.exit(0)}return o}function resolveRepoConfigOrExit(e,o){const n=e.getRepo(o);return n||(console.log(""),console.log(c("yellow"," No monitor configuration found for this repo.")),console.log(c("gray"," Run `thuban monitor <path> --once` or `thuban monitor <path>` first.")),console.log(""),process.exit(0)),n}function printFileCapUpgradePrompt({scannedCount:e,totalCount:o,remainingSummary:n,upgradeUrl:l}){const s=n.topDirectories.length;if(console.log(c("yellow"," ╔══════════════════════════════════════════════════════════════╗")),console.log(c("yellow"," ║")+c("bold"," FREE TIER LIMIT REACHED ")+c("yellow","║")),console.log(c("yellow"," ╠══════════════════════════════════════════════════════════════╣")),console.log(c("yellow"," ║")+` You have scanned ${e} of ${o} files.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Upgrade to Thuban Pro for continuous monitoring —".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" a pair of eyes on your codebase, all day, every day.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Your always-on guardian with unlimited scans.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+` ${n.remainingCount} more files remaining including ${s}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+` critical director${1===s?"y":"ies"}.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" "+c("yellow","║")),console.log(c("yellow"," ║")+` Upgrade now: ${l}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" CLI shortcut: thuban upgrade".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ╚══════════════════════════════════════════════════════════════╝")),console.log(""),n.topDirectories.length>0){console.log(c("bold"," Remaining scan preview"));for(const e of n.topDirectories)console.log(` ${c("yellow","•")} ${c("cyan",e.directory)} ${c("gray",`(${e.count} files)`)}`);if(n.previewFiles.length>0){console.log(""),console.log(c("gray"," Next files that would be scanned:"));for(const e of n.previewFiles)console.log(` ${c("gray","·")} ${e}`)}console.log("")}}async function cmdScan(e){const o=Date.now(),n=/^(https?:\/\/|git@)/.test(e.targetPath);let l=n?e.targetPath:path.resolve(e.targetPath),s=null;if(n){e.json||(console.log(""),console.log(c("cyan"," Cloning repository...")),console.log(c("gray",` ${e.targetPath}`)));const o=path.join(os.tmpdir(),"thuban-scan-"+Date.now()),{execFileSync:n}=require("child_process");try{n("git",["clone","--depth","1",e.targetPath,o],{stdio:"pipe",timeout:6e4}),l=o,s=o,e.json||(console.log(c("cyan"," Cloned successfully")),console.log(""))}catch(e){console.error(c("red",` Failed to clone repository: ${e.message}`)),console.error(c("gray"," Check the URL is correct and the repo is accessible")),process.exit(1)}}const t=new LicenseManager,a=t.getTierConfig(),r=t.canScan(),i="https://thuban.dev/pricing";r.allowed||e.json||(console.log(""),console.log(c("red"," ╔══════════════════════════════════════════════════╗")),console.log(c("red"," ║")+c("bold"," Monthly scan limit reached ")+c("red","║")),console.log(c("red"," ╚══════════════════════════════════════════════════╝")),console.log(""),console.log(` You've used ${c("bold",r.used+"/"+r.limit)} free scans this month.`),console.log(` Resets on ${c("cyan",r.resetsOn)}.`),console.log(""),console.log(` ${c("cyan","Upgrade to Pro")} for unlimited scans, full reports, auto-fix, and an always-on pair of eyes via thuban monitor:`),console.log(` ${c("bold",i)}`),console.log(""),console.log(` Or activate a key: ${c("cyan","thuban activate <your-key>")}`),console.log(""),process.exit(0)),e.json||(banner(),0===t.usage.totalScans&&(console.log(c("cyan"," ┌──────────────────────────────────────────────────────┐")),console.log(c("cyan"," │")+c("bold"," Welcome to Thuban! ")+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Thuban scans your codebase for AI hallucinations,")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","dead code, tech debt, and architecture drift.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","After this scan, try:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix .")+" "+c("gray","Preview fixes")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban dashboard .")+" "+c("gray","Visual report")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban --help")+" "+c("gray","All commands")+" "+c("cyan","│")),console.log(c("cyan"," └──────────────────────────────────────────────────────┘")),console.log("")),console.log(c("bold"," SCANNING: ")+c("cyan",l)),console.log(""));const g=collectFiles(l,e.ignore),d=g.length,h=a.maxFiles<1/0?g.slice(0,a.maxFiles):g,y=d>a.maxFiles,u=y?summarizeRemainingFiles(h,g,l):null;!e.json&&y?(console.log(c("yellow",` Free tier: scanning ${a.maxFiles} of ${d} files`)),console.log(c("gray"," Partial results below. Upgrade to scan your entire codebase.")),console.log("")):e.json||(console.log(c("gray",` Found ${h.length} files to scan...`)),console.log(""));const p=new CodeScanner({rootPath:l,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),f=new HallucinationDetector({rootPath:l}),[m,b]=await Promise.all([p.scanFiles(h),f.scan(h)]),$=Date.now()-o;t.recordScan();const w=[];for(const e of m.issues||[])w.push({file:path.relative(l,e.file),...e});for(const e of b.phantomAPIs)w.push({file:e.file,line:e.line,severity:"critical",category:"hallucination",id:"HALL_API",message:`${e.name} — this API does not exist`,suggestion:e.suggestion,crash:!0});for(const e of b.phantomImports)w.push({file:e.file,line:e.line,severity:"critical",category:"hallucination",id:"HALL_IMPORT",message:`${e.module} — ${e.reason}`,crash:!0});for(const e of b.deprecatedAPIs)w.push({file:e.file,line:e.line,severity:"high",category:"deprecated",id:"DEPR_API",message:`${e.name} — deprecated since ${e.since}`,suggestion:e.suggestion});for(const e of b.aiSmells)w.push({file:e.file,line:e.line,severity:"warning",category:"ai_smell",id:e.id,message:e.message});if(e.json)return void console.log(JSON.stringify({files:d,filesScanned:h.length,elapsed_ms:$,tier:t.getTier(),issues:a.showFullDetails?w:w.slice(0,a.teaserIssues),totalIssues:w.length,gated:!a.showFullDetails,fileLimitReached:y,upgradeUrl:y?i:null,remainingFiles:u?u.remainingCount:0,remainingDirectories:u?u.topDirectories:[],remainingPreview:u?u.previewFiles:[]},null,2));const v={critical:0,high:1,error:2,medium:3,warning:4,low:5,info:6};w.sort((e,o)=>(v[e.severity]||99)-(v[o.severity]||99));const S=w.filter(e=>"critical"===e.severity),x=w.filter(e=>"high"===e.severity||"error"===e.severity),C=w.filter(e=>"medium"===e.severity||"warning"===e.severity),E=w.filter(e=>"low"===e.severity||"info"===e.severity),P=w.filter(e=>"hallucination"===e.category),I=w.filter(e=>"deprecated"===e.category),A=w.filter(e=>"ai_smell"===e.category),D=C.filter(e=>"ai_smell"!==e.category),T=Math.max(0,100-15*S.length-5*x.length-2*D.length-.5*A.length-.25*E.length),N=T>=90?"A":T>=80?"B":T>=70?"C+":T>=60?"C":T>=50?"D+":T>=40?"D":"F",F=T>=80?"green":T>=60?"yellow":"red",R=w.filter(e=>!1!==e.fixable).length;if(console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," THUBAN SCAN RESULTS ")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," v0.3.3 · Public Beta ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` Files scanned: ${c("bold",String(h.length).padEnd(6))}`+(y?c("yellow",` of ${d}`):" ")+c("cyan"," ║")),console.log(c("cyan"," ║")+` Health score: ${c(F,(N+" ("+Math.round(T)+"/100)").padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Issues found: ${c(w.length>0?"yellow":"green",String(w.length).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Auto-fixable: ${c("green",String(R).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Scan time: ${c("gray",($+"ms").padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),h.some(e=>!(e.endsWith(".js")||e.endsWith(".ts")||e.endsWith(".jsx")||e.endsWith(".tsx")||e.endsWith(".mjs")))){const e=[...new Set(h.filter(e=>!/\.(js|ts|jsx|tsx|mjs)$/.test(e)).map(e=>path.extname(e)).filter(Boolean))];console.log(c("gray"," Note: Score includes Security + Hallucination + Code Quality categories.")),console.log(c("gray",` Architecture + Dependency analysis evaluated for JS/TS only. Detected non-JS: ${e.join(", ")}`)),console.log("")}if(P.length>0){console.log(c("bgRed"," CRITICAL ")+c("red",c("bold",` ${P.length} Hallucinated API${P.length>1?"s":""}`))),console.log(c("gray"," These imports or method calls do not exist. Your code WILL crash when it hits them.")),console.log("");const e=a.showFullDetails?P.length:Math.min(a.teaserIssues,P.length);for(let o=0;o<e;o++){const e=P[o],n=e.line?`:${e.line}`:"";console.log(` ${c("cyan",e.file+n)}`);try{const o=path.resolve(l,e.file),n=fs.readFileSync(o,"utf-8").split("\n");e.line&&n[e.line-1]&&console.log(` ${c("red",n[e.line-1].trim())}`)}catch(e){}console.log(` ${c("yellow","^^^")} ${e.message}`),e.suggestion&&console.log(` ${c("green","Fix:")} ${e.suggestion}`),console.log("")}P.length>e&&(console.log(c("gray",` ... ${P.length-e} more hallucinations hidden`)),console.log(c("cyan"," Upgrade to Pro to see all: ")+c("bold","thuban upgrade")),console.log(""))}if(I.length>0){console.log(c("red",c("bold",` DEPRECATED: ${I.length} API${I.length>1?"s":""} that will break on upgrade`))),console.log(c("gray"," These work NOW but will stop working when you upgrade Node.js or your dependencies.")),console.log(c("gray"," Fix: ")+c("cyan","npx thuban fix . --fix --unsafe")),console.log("");const e=a.showFullDetails?I.length:Math.min(a.teaserIssues,I.length);for(let o=0;o<e;o++){const e=I[o],n=e.line?`:${e.line}`:"";console.log(` ${c("cyan",e.file+n)}`),console.log(` ${c("yellow",e.message)}`),e.suggestion&&console.log(` ${c("green","→")} ${e.suggestion}`),console.log("")}I.length>e&&(console.log(c("gray",` ... ${I.length-e} more hidden`)),console.log(""))}const O=w.filter(e=>"security"===e.category),j=w.filter(e=>"quality"===e.category||"performance"===e.category||"ai"===e.category||"ai_smell"===e.category);if(O.length>0){console.log(c("red",c("bold",` SECURITY: ${O.length} issue${O.length>1?"s":""}`)));const e=a.showFullDetails?Math.min(O.length,10):Math.min(a.teaserIssues,O.length);for(let o=0;o<e;o++){const e=O[o],n=e.line?`:${e.line}`:"";console.log(` ${c("red","✗")} ${c("cyan",e.file+n)} ${c("gray",e.message)}`)}O.length>e&&console.log(c("gray",` ... ${O.length-e} more hidden`)),console.log("")}if(j.length>0){const e={};for(const o of j){const n=(o.message||o.type||"other").replace(/\s*—.*$/,"").trim();e[n]||(e[n]=[]),e[n].push(o)}const o=Object.entries(e).sort((e,o)=>o[1].length-e[1].length);console.log(c("yellow",c("bold",` CODE QUALITY: ${j.length} issue${j.length>1?"s":""}`))),console.log(c("gray"," Patterns that reduce code quality, maintainability, or production readiness.")),console.log("");const n=a.showFullDetails?o.length:Math.min(3,o.length);for(let e=0;e<n;e++){const[n,l]=o[e],s="high"===l[0].severity||"error"===l[0].severity?"red":"yellow";if(console.log(` ${c(s,"!")} ${c("bold",String(l.length))} x ${n}`),l[0].file){const e=l[0].line?`:${l[0].line}`:"";console.log(` ${c("gray","e.g.")} ${c("cyan",l[0].file+e)}`)}}o.length>n&&console.log(c("gray",` ... ${o.length-n} more issue types`)),console.log("");const l=j.filter(e=>!1!==e.fixable).length;l>0&&(console.log(c("gray",` ${l} of these can be auto-fixed: `)+c("cyan","npx thuban fix . --fix --unsafe")),console.log(""))}if(console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),T<60?console.log(c("bold"," │ ")+c("red","YOUR CODEBASE HAS A SERIOUS HEALTH PROBLEM ")+c("bold"," │")):T<80?console.log(c("bold"," │ ")+c("yellow","YOUR CODEBASE NEEDS ATTENTION ")+c("bold"," │")):console.log(c("bold"," │ ")+c("green","YOUR CODEBASE IS IN GOOD SHAPE ")+c("bold"," │")),console.log(c("bold"," │")+` Score: ${c(F,N+" ("+Math.round(T)+"/100)")} `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log(""),S.length>0){console.log(c("red",` ${S.length} function${S.length>1?"s":""} will crash in production.`));const e=S.length<=3?"30 seconds":S.length<=10?"2 minutes":"10 minutes";console.log(c("gray",` Estimated cleanup: ${e} with Thuban auto-fix.`)),console.log(c("gray"," Estimated cost if shipped: your weekend.")),console.log("")}if(a.showFullDetails)w.length>0&&(w.filter(e=>!1!==e.fixable).length,console.log(c("cyan"," ┌──────────────────────────────────────────────────────┐")),console.log(c("cyan"," │")+c("bold"," WHAT TO DO NEXT ")+c("cyan","│")),console.log(c("cyan"," ├──────────────────────────────────────────────────────┤")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","1. See what would be fixed (safe preview):")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . ")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","2. Apply all safe fixes:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . --fix")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","3. Apply fixes with git rollback:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . --fix --commit")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Each fix = 1 git commit. Undo any with git revert.")+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","4. Generate visual report (HTML):")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban dashboard .")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Opens a dashboard you can share with your team.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","5. Show tech debt cost in £:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban cost .")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","6. Block bad code on every commit:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban gate")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Installs a pre-commit hook. Set once, forget.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","All commands: npx thuban --help")+" "+c("cyan","│")),console.log(c("cyan"," └──────────────────────────────────────────────────────┘")),console.log(""));else{const e=w.length-a.teaserIssues;e>0&&(console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold",` ${e} more issues hidden `)+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," Upgrade to see every issue, fix them automatically,")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," and get the full HTML dashboard report. ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` ${c("bold","thuban upgrade")} Open pricing page `+c("cyan","║")),console.log(c("cyan"," ║")+` ${c("bold","thuban activate")} <key> Activate a license key `+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""));const o=t.canScan(),n=o.remaining;console.log(c("gray",` Free scans remaining this month: ${n}/${o.limit}`)),console.log("")}y&&printFileCapUpgradePrompt({scannedCount:h.length,totalCount:d,remainingSummary:u,upgradeUrl:i}),w.length>0&&!y&&(console.log(c("gray"," ─────────────────────────────────────────────────────")),console.log(c("gray"," Thuban runs 100% on your machine. No data leaves.")),console.log(c("gray"," No cloud costs. As your codebase grows, Thuban grows with it.")),console.log(""),console.log(c("gray"," Keep your codebase healthy continuously:")),console.log(c("gray"," ")+c("cyan","npx thuban gate --fix")+c("gray"," Block bad code on every commit")),console.log(c("gray"," ")+c("cyan","npx thuban watch .")+c("gray"," Scan files as you save them")),console.log(c("gray"," ")+c("cyan","npx thuban baseline .")+c("gray"," Only alert on NEW issues")),console.log(""))}async function cmdDeps(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," MAPPING DEPENDENCIES: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new DependencyGraph({rootPath:n});await s.build(l);const t=Date.now()-o,a=s.getModuleSummary(),r=s.getMostCriticalFiles(10),i=s.getOrphanFiles(),g=s.circularDeps||[];if(e.json)console.log(JSON.stringify({elapsed_ms:t,summary:a,critical:r,orphans:i,circular:g,graph:s.toJSON()},null,2));else{if(console.log(c("gray",` Analysed ${l.length} files in ${t}ms`)),console.log(""),console.log(c("bold"," DEPENDENCY SUMMARY")),console.log(` Files: ${a.totalFiles||l.length}`),console.log(` Import links: ${a.totalImports||0}`),console.log(` Circular deps: ${g.length>0?c("red",g.length):c("green","0")}`),console.log(` Orphan files: ${i.length>0?c("yellow",i.length):c("green","0")}`),console.log(""),r.length>0){console.log(c("bold"," MOST CRITICAL FILES")+c("gray"," (most dependents)"));for(const e of r){const o="string"==typeof e?e:e.file||e.path||String(e),l=path.relative(n,o),s="object"==typeof e&&(e.dependents||e.score||e.count)||"?";console.log(` ${c("cyan",l)} ${c("gray","→ "+s+" dependents")}`)}console.log("")}if(g.length>0){console.log(c("red"," CIRCULAR DEPENDENCIES"));for(const e of g.slice(0,5)){const o=Array.isArray(e)?e.map(e=>path.relative(n,e)).join(" → "):String(e);console.log(` ${c("yellow","⟲")} ${o}`)}console.log("")}if(i.length>0){console.log(c("yellow"," ORPHAN FILES")+c("gray"," (no imports, no exports consumed)"));for(const e of i.slice(0,10)){const o="string"==typeof e?e:e.file||e.path||String(e);console.log(` ${c("gray","○")} ${path.relative(n,o)}`)}i.length>10&&console.log(c("gray",` ... and ${i.length-10} more`)),console.log("")}}}async function cmdMonitor(e){requireProFeature("Scheduled monitoring");const o=path.resolve(e.targetPath),n=new MonitorService,l=n.configureRepo({repoPath:o,frequency:e.frequency,intervalMs:resolveIntervalMs(e.frequency,e.intervalMs),notification:{channels:String(e.notify||"inbox").split(",").map(e=>e.trim()).filter(Boolean)}});if(e.once){const o=await n.runRepoScan(l);return e.json?void console.log(JSON.stringify(o.run,null,2)):(console.log(""),console.log(c("bold"," THUBAN MONITOR RUN")),console.log(` Repo: ${l.repoPath}`),console.log(` New issues: ${o.diff.added.length}`),console.log(` Resolved issues: ${o.diff.resolved.length}`),console.log(` History saved: ${o.runPath}`),void console.log(""))}console.log(""),console.log(c("bold"," THUBAN MONITOR ACTIVE")),console.log(` Repo: ${l.repoPath}`),console.log(` Frequency: ${l.frequency} (${l.intervalMs}ms)`),console.log(` Notifications: ${(l.notification.channels||[]).join(", ")}`),console.log(c("gray"," Press Ctrl+C to stop.")),console.log(""),n.start(l),process.on("SIGINT",()=>{n.stopAll(),process.exit(0)})}async function cmdHistory(e){requireProFeature("Scan history");const o=new MonitorStore,n=resolveRepoConfigOrExit(o,e.targetPath),l=o.listRuns(n.repoId);if(e.json)return void console.log(JSON.stringify({repo:n,runs:l},null,2));console.log(""),console.log(c("bold"," THUBAN HISTORY")),console.log(` Repo: ${n.repoPath}`),console.log(` Runs: ${l.length}`),console.log("");for(const e of l.slice(-10).reverse())console.log(` ${c("cyan",e.timestamp)} issues=${e.metrics.issueCount} new=${e.diff?.added?.length||0} resolved=${e.diff?.resolved?.length||0}`);const s=o.getNotifications(10).filter(e=>e.repoId===n.repoId);if(s.length){console.log(""),console.log(c("bold"," RECENT ALERTS"));for(const e of s)console.log(` ${c("yellow",e.timestamp)} ${e.summary}`)}console.log("")}async function cmdTrend(e){requireProFeature("Trend reporting");const o=new MonitorStore,n=resolveRepoConfigOrExit(o,e.targetPath),l=o.listRuns(n.repoId).map(e=>({timestamp:e.timestamp,issueCount:e.metrics.issueCount,hallucinations:e.metrics.hallucinations,secrets:e.metrics.secrets,techDebtScore:e.metrics.techDebtScore,techDebtHours:e.metrics.techDebtHours}));if(e.json)console.log(JSON.stringify({repo:n,trend:l},null,2));else{console.log(""),console.log(c("bold"," THUBAN TREND")),console.log(` Repo: ${n.repoPath}`),console.log("");for(const e of l.slice(-12))console.log(` ${c("cyan",e.timestamp)} issues=${e.issueCount} hallucinations=${e.hallucinations} secrets=${e.secrets} debtScore=${e.techDebtScore??"n/a"} debtHours=${e.techDebtHours??"n/a"}`);console.log("")}}async function cmdDrift(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," DETECTING DRIFT: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new DriftDetector({rootPath:n}),t=[];let a=0,r=0;for(const e of l)try{const o=await s.analyzeFile(e);o&&o.hasWidget&&(a++,o.drifts&&o.drifts.length>0&&(r++,t.push({file:path.relative(n,e),...o})))}catch(e){}const i=Date.now()-o;if(e.json)return void console.log(JSON.stringify({elapsed_ms:i,total:l.length,annotated:a,drifted:r,results:t},null,2));if(console.log(c("gray",` Checked ${l.length} files in ${i}ms`)),console.log(""),console.log(` Files with Mother Code: ${c("cyan",a)}`),console.log(` Files without: ${c("yellow",l.length-a)}`),console.log(` Files with drift: ${r>0?c("red",r):c("green","0")}`),console.log(""),t.length>0){console.log(c("bold"," DRIFT DETECTED"));for(const e of t.slice(0,20)){console.log(` ${c("yellow","⚠")} ${c("cyan",e.file)}`);for(const o of e.drifts)console.log(` ${c("gray","→")} ${o.type}: ${o.message||o.detail||JSON.stringify(o)}`)}console.log("")}const g=l.length>0?Math.round(a/l.length*100):0,d=g>=80?"green":g>=40?"yellow":"red";console.log(` Mother Code coverage: ${c(d,g+"%")}`),console.log("")}async function cmdInject(e){const o=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," INJECTING MOTHER CODE: ")+c("cyan",o)),console.log(""));const n=collectFiles(o,e.ignore).filter(e=>e.endsWith(".js")||e.endsWith(".ts")||e.endsWith(".jsx")||e.endsWith(".tsx")||e.endsWith(".py")||e.endsWith(".java")||e.endsWith(".cs")||e.endsWith(".go")||e.endsWith(".kt")||e.endsWith(".rs")||e.endsWith(".php")||e.endsWith(".rb")),l=new DependencyGraph({rootPath:o});await l.build(n);const s=new WidgetGenerator({rootPath:o,dependencyGraph:l});let t=0,a=0;const r=[];for(const l of n)try{const n=fs.readFileSync(l,"utf-8");if(n.includes("@purpose")&&n.includes("@module")){a++;continue}const i=await s.generateWidget(l,n);if(!i){a++;continue}const g=i&&i.widgetString?i.widgetString:String(i),d=path.extname(l).toLowerCase();let h;if(h=".py"===d||".rb"===d?g.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"# ")).join("\n"):".go"===d||".rs"===d?g.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"// ")).join("\n"):g,e.fix){const s=h+"\n\n"+n;fs.writeFileSync(l,s,"utf-8"),t++,e.json||console.log(` ${c("green","✓")} ${path.relative(o,l)}`)}else r.push({file:path.relative(o,l),widget:h}),t++}catch(e){a++}if(e.json)console.log(JSON.stringify({injected:t,skipped:a,dryRun:!e.fix,previews:r},null,2));else{if(console.log(""),e.fix)console.log(c("green",` ✓ Injected Mother Code into ${t} files`));else if(console.log(c("yellow",` DRY RUN — ${t} files would be annotated. Use --fix to apply.`)),e.verbose&&r.length>0){console.log("");for(const e of r.slice(0,5))console.log(` ${c("cyan",e.file)}:`),console.log(c("gray",e.widget.split("\n").map(e=>" "+e).join("\n"))),console.log("")}console.log(` ${c("gray",`Skipped: ${a} (already annotated or unsupported)`)}`),console.log("")}}async function cmdReport(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," FULL REPORT: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new CodeScanner({rootPath:n}),t=new DependencyGraph({rootPath:n}),a=new DriftDetector({rootPath:n}),[r]=await Promise.all([s.scanFiles(l),t.build(l)]);let i=0,g=0;for(const e of l)try{const o=await a.analyzeFile(e);o&&o.hasWidget&&(g++,o.drifts&&o.drifts.length>0&&i++)}catch(e){}const d=Date.now()-o;let h=0,y=0,u=0;for(const e of r.issues||[])h++,"critical"===e.severity&&y++,"high"===e.severity&&u++;const p=t.getOrphanFiles(),f=t.circularDeps||[],m=l.length>0?Math.round(g/l.length*100):0,b=Math.max(0,Math.round(100-15*y-5*u-10*f.length-1*p.length-.1*(100-m)-3*i));if(e.json)return void console.log(JSON.stringify({elapsed_ms:d,files:l.length,health_score:b,issues:{total:h,critical:y,high:u},dependencies:{circular:f.length,orphans:p.length},mother_code:{annotated:g,coverage_pct:m,drifted:i}},null,2));const $=b>=80?"green":b>=60?"yellow":"red";console.log(c("bold"," ╔══════════════════════════════════════╗")),console.log(c("bold"," ║ THUBAN HEALTH REPORT ║")),console.log(c("bold"," ╚══════════════════════════════════════╝")),console.log(""),console.log(` ${c("bold","Health Score:")} ${c($,b+"/100")}`),console.log(` ${c("bold","Files Scanned:")} ${l.length}`),console.log(` ${c("bold","Time:")} ${d}ms`),console.log(""),console.log(c("bold"," ── Code Quality ──────────────────────")),console.log(` Total issues: ${h}`),console.log(` Critical: ${y>0?c("red",y):c("green","0")}`),console.log(` High: ${u>0?c("red",u):c("green","0")}`),console.log(""),console.log(c("bold"," ── Dependencies ─────────────────────")),console.log(` Circular deps: ${f.length>0?c("red",f.length):c("green","0")}`),console.log(` Orphan files: ${p.length>0?c("yellow",p.length):c("green","0")}`),console.log(""),console.log(c("bold"," ── Mother Code ──────────────────────")),console.log(` Annotated files: ${g}`),console.log(` Coverage: ${c(m>=80?"green":m>=40?"yellow":"red",m+"%")}`),console.log(` Drifted files: ${i>0?c("yellow",i):c("green","0")}`),console.log("");const w=b>=90?"A":b>=80?"B":b>=70?"C":b>=60?"D":"F",v=b>=80?"green":b>=60?"yellow":"red";console.log(` ${c("bold","Grade:")} ${c(v,w)}`),console.log(""),b<80&&(console.log(c("bold"," RECOMMENDATIONS:")),y>0&&console.log(` ${c("red","→")} Fix ${y} critical issues immediately`),f.length>0&&console.log(` ${c("yellow","→")} Resolve ${f.length} circular dependencies`),m<50&&console.log(` ${c("yellow","→")} Run ${c("cyan","thuban inject --fix")} to improve Mother Code coverage`),i>0&&console.log(` ${c("yellow","→")} ${i} files have drifted from their annotations`),p.length>5&&console.log(` ${c("gray","→")} Consider removing ${p.length} orphan files`),console.log(""))}async function cmdDebt(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," TECH DEBT ANALYSIS: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files...`)),console.log(""));const l=new TechDebtAnalyzer({rootPath:o}),s=await l.analyze(n);e.json?console.log(JSON.stringify(l.toJSON(s),null,2)):console.log(l.formatReport(s))}async function cmdFix(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore),{execSync:l}=require("child_process");e.json||(banner(),console.log(c("bold"," AUTO-FIX TECH DEBT: ")+c("cyan",o)),console.log(""));const s=new TechDebtAnalyzer({rootPath:o});if(!e.fix){const o=await s.fix(n,{dryRun:!0});if(e.json)console.log(JSON.stringify(o,null,2));else{console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," DRY RUN — no files will be changed ")+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),console.log(` Fixable items: ${c("bold",o.totalFixable)}`),console.log("");const e={},n=["break_circular","extract_to_env","update_deprecated_api","wrap_localhost_url"];let l=0,s=0;for(const c of o.fixes){const o=c.fixAction||"unknown";e[o]||(e[o]=0),e[o]++,n.includes(o)?s++:l++}console.log(c("green",c("bold",` SAFE FIXES (${l})`))+c("gray"," — no runtime behaviour change"));for(const[o,l]of Object.entries(e).sort((e,o)=>o[1]-e[1])){if(n.includes(o))continue;const e={inject_widget:"Inject Mother Code DNA annotations",regenerate_widget:"Update stale annotations",remove_console_log:"Remove debug console.logs",flag_for_removal:"Confirm orphan files for removal"}[o]||o;console.log(` ${c("green","→")} ${e}: ${c("bold",l)}`)}if(console.log(""),s>0){console.log(c("yellow",c("bold",` UNSAFE FIXES (${s})`))+c("gray"," — may change runtime behaviour"));for(const[o,l]of Object.entries(e).sort((e,o)=>o[1]-e[1])){if(!n.includes(o))continue;const e={break_circular:"Restructure circular dependencies",extract_to_env:"Extract hardcoded secrets to env vars",update_deprecated_api:"Replace deprecated API calls with modern equivalents",wrap_localhost_url:"Wrap hardcoded localhost URLs in env var fallback"}[o]||o;console.log(` ${c("yellow","!")} ${e}: ${c("bold",l)}`)}console.log(""),console.log(c("gray"," Unsafe fixes require --unsafe flag:")),console.log(` ${c("cyan","thuban fix [path] --fix --unsafe")}`),console.log("")}console.log(c("bold"," TO APPLY:")),console.log(` ${c("cyan","thuban fix [path] --fix")} Apply safe fixes only`),console.log(` ${c("cyan","thuban fix [path] --fix --unsafe")} Apply all fixes`),console.log(` ${c("cyan","thuban fix [path] --fix --commit")} Each fix = separate git commit`),console.log("")}return}e.json||(e.safe?console.log(c("green"," SAFE MODE")+c("gray"," — only non-runtime-changing fixes will be applied")):console.log(c("yellow"," UNSAFE MODE")+c("gray"," — all fixes including runtime changes")),console.log(""));const t=await s.fix(n,{dryRun:!1,safeOnly:e.safe});if(e.gitCommit&&t.fixed>0)try{l("git rev-parse --git-dir",{cwd:o,stdio:"pipe"}),l("git add -A",{cwd:o,stdio:"pipe"});const n=`fix(thuban): auto-fix ${t.fixed} issues [safe=${e.safe}]`;l(`git commit -m "${n}"`,{cwd:o,stdio:"pipe"}),e.json||(console.log(c("green",` ✓ Changes committed: "${n}"`)),console.log(c("gray"," Revert with: git revert HEAD")),console.log(""))}catch(o){e.json||(console.log(c("yellow"," ⚠ Could not auto-commit: "+(o.message||"not a git repo"))),console.log(""))}if(e.json)console.log(JSON.stringify(t,null,2));else{console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," FIX RESULTS ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣"));const o=(t.fixes||[]).filter(e=>"fixed"===e.status&&("inject_widget"===e.fixAction||"regenerate_widget"===e.fixAction)).length,n=t.fixed-o;o>0&&console.log(c("cyan"," ║")+` Annotations added: ${c("green",String(o).padEnd(19))}`+c("cyan"," ║")),n>0&&console.log(c("cyan"," ║")+` Issues fixed: ${c("green",String(n).padEnd(20))}`+c("cyan"," ║")),0===o&&0===n&&console.log(c("cyan"," ║")+` Fixed: ${c("green",String(t.fixed).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Flagged for review: ${c("yellow",String(t.flagged).padEnd(19))}`+c("cyan"," ║")),t.failed>0&&console.log(c("cyan"," ║")+` Failed: ${c("red",String(t.failed).padEnd(20))}`+c("cyan"," ║")),t.skipped>0&&console.log(c("cyan"," ║")+` Skipped: ${c("gray",String(t.skipped).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),t.fixed>0&&(console.log(c("green"," Your files have been updated. Changes are saved to disk.")),e.gitCommit||console.log(c("gray"," Tip: use --commit next time to auto-commit each fix for easy rollback.")),console.log(""),console.log(c("gray"," Verify the changes:")),console.log(c("gray"," ")+c("cyan","npx thuban scan .")+c("gray"," Re-scan to see your new score")),console.log(c("gray"," ")+c("cyan","npx thuban dashboard .")+c("gray"," Generate updated report")),console.log("")),t.skipped>0&&(console.log(c("gray",` ${t.skipped} items were skipped (no auto-fix available or pattern not matched).`)),console.log(c("gray"," Review them manually or run: ")+c("cyan","npx thuban dashboard .")),console.log("")),t.flagged>0&&(console.log(c("gray",` ${t.flagged} items need human review (secrets, orphan files, complex patterns).`)),console.log(c("gray"," See details: ")+c("cyan","npx thuban dashboard .")),console.log(""))}}async function cmdHallucinate(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," HALLUCINATION SCAN: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files for AI hallucinations...`)),console.log(""));const l=new HallucinationDetector({rootPath:o}),s=await l.scan(n);e.json?console.log(JSON.stringify(s,null,2)):console.log(l.formatReport(s))}async function cmdWatch(e){const o=path.resolve(e.targetPath),n=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),c=new HallucinationDetector({rootPath:o}),l=new FileWatcher({rootPath:o,ignorePatterns:["node_modules",".git","dist",".next","__pycache__",...e.ignore]});process.on("SIGINT",()=>{l.stop(),process.exit(0)}),l.start(async e=>{const o=[],l=await n.scanFile(e);l&&o.push(...l);const s=await c.scan([e]);if(s.phantomAPIs.length>0)for(const e of s.phantomAPIs)o.push({severity:"high",message:`Phantom API: ${e.name} — ${e.suggestion}`,line:e.line});if(s.phantomImports.length>0)for(const e of s.phantomImports)o.push({severity:"critical",message:`Phantom import: ${e.module} — ${e.reason}`,line:e.line});if(s.aiSmells.length>0)for(const e of s.aiSmells)o.push({severity:"warning",message:`AI smell: ${e.message}`,line:e.line});return o})}async function cmdDashboard(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," GENERATING DASHBOARD: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files...`)),console.log(""));const l=new TechDebtAnalyzer({rootPath:o}),s=await l.analyze(n),t=new DependencyGraph({rootPath:o});await t.build(n);const a={circular:t.circularDeps||[],orphans:t.getOrphanFiles()||[],critical:t.getMostCriticalFiles(5)||[]},r=new HallucinationDetector({rootPath:o}),i=await r.scan(n),g=new HtmlReportGenerator({rootPath:o,outputDir:o}),d=path.basename(o),h=await g.writeReport(s,a,{projectName:d,hallucinations:i});e.json?console.log(JSON.stringify({outputPath:h,scores:s.scores,stats:s.stats},null,2)):(console.log(c("green"," ✓ Dashboard generated:")),console.log(` ${c("cyan",h)}`),console.log(""),console.log(c("gray"," Open in your browser to view the interactive report")),console.log(""))}async function cmdDiff(e){const o=path.resolve(e.targetPath),{execSync:n}=require("child_process");e.json||(banner(),console.log(c("bold"," DIFF SCAN: ")+c("cyan",o)));let l=[];try{let e="main";try{n("git rev-parse --verify main",{cwd:o,stdio:"pipe"})}catch{try{n("git rev-parse --verify master",{cwd:o,stdio:"pipe"}),e="master"}catch{e="HEAD~1"}}const c=n(`git diff --name-only ${e}...HEAD`,{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),s=n("git diff --name-only HEAD",{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),t=n("git ls-files --others --exclude-standard",{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),a=new Set([...c.split("\n").filter(Boolean),...s.split("\n").filter(Boolean),...t.split("\n").filter(Boolean)]);for(const e of a){const n=path.resolve(o,e),c=path.extname(e).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".json"].includes(c)&&fs.existsSync(n)&&l.push(n)}}catch(o){e.json||console.log(c("red",` ✗ Not a git repository or git error: ${o.message}`)),process.exit(1)}if(0===l.length)return void(e.json?console.log(JSON.stringify({changedFiles:0,issues:0})):console.log(c("green"," ✓ No changed files to scan")));e.json||console.log(c("gray",` Scanning ${l.length} changed file${1===l.length?"":"s"}...\n`));const s=new CodeScanner({rootPath:o}),t=new HallucinationDetector({rootPath:o}),a=await s.scanFiles(l),r=await t.scan(l);let i=0;const g={};for(const e of a.issues||[]){const n=path.relative(o,e.file);g[n]||(g[n]=[]),g[n].push(e),i++}if(i+=r.stats.totalIssues,e.json)console.log(JSON.stringify({changedFiles:l.length,totalIssues:i,codeIssues:g,hallucinations:r},null,2));else{for(const[e,o]of Object.entries(g)){console.log(` ${c("cyan",e)}`);for(const e of o.slice(0,5)){const o="critical"===e.severity||"high"===e.severity?"red":"warning"===e.severity?"yellow":"gray";console.log(` ${c(o,"✗")} ${c("gray",e.message||e.id||"")}`)}o.length>5&&console.log(` ${c("gray",`... and ${o.length-5} more`)}`),console.log("")}r.stats.totalIssues>0&&console.log(t.formatReport(r));const e=0===i?"green":"yellow";console.log(` ${c(e,`${l.length} files changed, ${i} new issues introduced`)}\n`)}}async function cmdActivate(e){banner();const o=new LicenseManager,n=e.licenseKey;if(!n)return console.log(c("red"," Usage: thuban activate <license-key>")),console.log(""),console.log(c("gray"," Get a key at: ")+c("cyan","https://thuban.dev/pricing")),void console.log("");const l=o.activate(n);l.success?(console.log(c("green"," ╔══════════════════════════════════════╗")),console.log(c("green"," ║")+c("bold"," License activated! ")+c("green","║")),console.log(c("green"," ╚══════════════════════════════════════╝")),console.log(""),console.log(` Tier: ${c("bold",l.tierName)}`),console.log(` Status: ${c("green","Active")}`),console.log(""),console.log(c("gray"," All features unlocked. Run ")+c("cyan","thuban scan .")+c("gray"," to get started.")),console.log("")):(console.log(c("red"," Invalid license key.")),console.log(""),console.log(c("gray"," Check for typos or get a new key at: ")+c("cyan","https://thuban.dev/pricing")),console.log(""))}async function cmdStatus(e){banner();const o=(new LicenseManager).getStatus();console.log(c("bold"," LICENSE STATUS")),console.log(""),console.log(` Tier: ${c("bold",o.tierName)}${o.licensed?c("green"," (active)"):""}`),console.log(` Machine ID: ${c("gray",o.machineId)}`),console.log(` Scans this month: ${c("bold",o.scansUsed)}${o.scansLimit<1/0?c("gray"," / "+o.scansLimit):""}`),console.log(` Lifetime scans: ${c("gray",o.totalScans)}`),console.log(""),console.log(c("bold"," FEATURES")),console.log(` Full scan details: ${o.features.fullDetails?c("green","Yes"):c("red","No")}`),console.log(` HTML dashboard: ${o.features.dashboard?c("green","Yes"):c("red","No")}`),console.log(` Auto-fix: ${o.features.autoFix?c("green","Yes"):c("red","No")}`),console.log(` CI Action: ${o.features.ciAction?c("green","Yes"):c("red","No")}`),console.log(""),o.licensed||(console.log(c("cyan"," Upgrade at: ")+c("bold","https://thuban.dev/pricing")),console.log(""))}function cmdUpgrade(){banner(),console.log(c("bold"," THUBAN PRICING")),console.log(""),console.log(` ${c("gray","FREE")} ${c("bold","£0/mo")}`),console.log(" 5 scans/month, 100 files, summary only"),console.log(""),console.log(` ${c("cyan","PRO")} ${c("bold","£19/mo")} ${c("gray","(or £149/year — save 35%)")}`),console.log(" Unlimited scans, full details, auto-fix, HTML dashboard"),console.log(""),console.log(` ${c("cyan","TEAM")} ${c("bold","£99/mo")}`),console.log(" Everything in Pro + 5 seats, CI Action, team reports"),console.log(""),console.log(` ${c("cyan","ENTERPRISE")} ${c("bold","£299/mo")}`),console.log(" Everything in Team + unlimited seats, priority support, SLA"),console.log(""),console.log(c("bold"," Subscribe at: ")+c("cyan","https://thuban.dev/pricing")),console.log("");try{const{exec:e}=require("child_process"),o=process.platform;e(("win32"===o?"start":"darwin"===o?"open":"xdg-open")+" https://thuban.dev/pricing")}catch(e){}}async function cmdGate(e){const o=path.resolve(e.targetPath);if(e.fix){const e=PreCommitGate.install(o);return banner(),e.success?(console.log(c("green"," ✓ Pre-commit gate installed!")),console.log(""),console.log(c("gray"," Every commit will now be checked for hallucinated APIs.")),console.log(c("gray"," Commits with phantom APIs will be blocked automatically.")),console.log(""),console.log(` To remove: ${c("cyan","thuban gate --uninstall")}`)):console.log(c("red",` Error: ${e.error}`)),void console.log("")}if(process.argv.includes("--uninstall")){const e=PreCommitGate.uninstall(o);return banner(),console.log(c("green",` ✓ ${e.message}`)),void console.log("")}const n=new PreCommitGate({rootPath:o,strict:!process.argv.includes("--warn")}).run();if(e.json)return void console.log(JSON.stringify(n,null,2));const l=process.argv.includes("--ci");if(l||banner(),0===n.files)return console.log(c("gray"," No staged files to check.")),console.log(""),console.log(` Install the gate: ${c("cyan","thuban gate --fix")}`),void console.log("");if(n.passed)console.log(c("green",` ✓ ${n.message}`)),console.log(c("gray",` ${n.elapsed}ms`)),l||console.log(""),process.exit(0);else{console.log(c("red",` ✗ ${n.message}`)),console.log("");for(const e of n.issues)console.log(` ${c("red",e.file)}:${e.line}`),console.log(` ${c("yellow",e.code)}`),console.log(` ${c("red","^^^")} ${e.message}`),e.fix&&console.log(` ${c("green","Fix:")} ${e.fix}`),console.log("");process.exit(1)}}async function cmdCost(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," SCANNING: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),s=new HallucinationDetector({rootPath:o}),[t,a]=await Promise.all([l.scanFiles(n),s.scan(n)]),r=[];for(const e of t.issues||[])r.push({file:path.relative(o,e.file),...e});for(const e of a.phantomAPIs)r.push({category:"hallucination",severity:"critical",message:e.name});for(const e of a.deprecatedAPIs)r.push({category:"deprecated",severity:"high",message:e.name});const i=new TechDebtCostCalculator,g=i.calculate(r,n.length);e.json?console.log(JSON.stringify(g,null,2)):console.log(i.formatCLI(g))}async function cmdGhosts(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," SCANNING FOR GHOST CODE: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new GhostCodeDetector({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.ghosts.length)return console.log(c("green"," No ghost code detected. Every function is referenced.")),void console.log("");console.log(c("yellow",c("bold",` GHOST CODE: ${l.totalGhosts} function${l.totalGhosts>1?"s":""} exist but are never called`))),console.log("");for(const o of l.ghosts.slice(0,e.maxIssues)){const e="high"===o.severity?"red":"yellow";console.log(` ${c(e,"⊘")} ${c("cyan",o.file)}:${o.line}`),console.log(` ${c("bold",o.name+"()")} — ${o.wastedLines} lines, ${o.references} references`),console.log("")}console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),console.log(c("bold"," │")+` Wasted code: ${c("red",l.totalWastedLines+" lines")} (${l.wastedPercent}% of codebase) `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log("")}}async function cmdAIScore(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," AI CONFIDENCE SCORING: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new AIConfidenceScorer({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.highConfidence.length&&0===l.mediumConfidence.length)return console.log(c("green"," No AI-generated code patterns detected.")),void console.log("");console.log(c("red",c("bold",` HIGH CONFIDENCE AI-GENERATED (${l.highConfidence.length} functions)`))),console.log("");for(const e of l.highConfidence.slice(0,15)){console.log(` ${c("red",e.confidence+"%")} AI ${c("cyan",e.file)}:${e.line}`),console.log(` ${c("bold",e.name+"()")} — ${e.lineCount} lines | Test coverage: ${e.testCoverage}`);for(const o of e.signals.slice(0,2))console.log(` ${c("gray","• "+o)}`);console.log("")}if(l.mediumConfidence.length>0){console.log(c("yellow",c("bold",` POSSIBLE AI-GENERATED (${l.mediumConfidence.length} functions)`))),console.log("");for(const e of l.mediumConfidence.slice(0,10))console.log(` ${c("yellow",e.confidence+"%")} AI ${c("cyan",e.file)}:${e.line} ${c("gray",e.name+"()")}`);console.log("")}console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),console.log(c("bold"," │")+` ${c("red",l.aiPercentage+"%")} of functions are likely AI-generated `+c("bold","│")),console.log(c("bold"," │")+` ${c("yellow",l.aiLikelyCount)} high confidence | ${l.aiPossibleCount} possible `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log("")}}async function cmdClones(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," COPY-PASTE DRIFT DETECTION: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CopyPasteDriftDetector({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.clusters.length)return console.log(c("green"," No copy-paste drift detected.")),void console.log("");console.log(c("yellow",c("bold",` COPY-PASTE CLUSTERS: ${l.totalClusters} found`))),console.log("");for(const e of l.clusters.slice(0,10)){const o="high"===e.severity?"red":"yellow";console.log(` ${c(o,"◈")} ${c("bold",e.pattern+" pattern")} — ${e.totalInstances} instances`);for(const o of e.members){const e=o.similarity<100?c("gray",` (${o.similarity}% similar)`):"";console.log(` ${c("cyan",o.file)}:${o.line} ${c("gray",o.name+"()")}${e}`)}console.log(` ${c("green","→")} ${e.recommendation}`),console.log("")}console.log(c("bold",` Total duplicate lines: ${c("red",l.totalDuplicateLines)}`)),console.log(c("gray",` ${l.recommendation}`)),console.log("")}}async function cmdPassport(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," GENERATING CODEBASE PASSPORT: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CodebasePassport({rootPath:o}),s=l.generate(n);if(e.json)return void console.log(JSON.stringify(s,null,2));const t=l.save(s),a=s.project,r=s.onboarding;console.log(c("green",` ✓ Passport generated: ${c("bold",path.relative(o,t))}`)),console.log(""),console.log(c("bold"," PROJECT IDENTITY")),console.log(` Name: ${c("cyan",a.name)}`),console.log(` Files: ${c("bold",a.totalFiles)}`),console.log(` Lines: ${c("bold",a.totalLines.toLocaleString())}`),console.log(` Architecture: ${c("bold",a.architecture)}`),console.log(` Frameworks: ${a.frameworks.join(", ")||"vanilla"}`),console.log(""),console.log(c("bold"," LANGUAGES"));for(const e of a.languages.slice(0,5)){const o="█".repeat(Math.max(1,Math.round(e.percentage/5)));console.log(` ${e.extension.padEnd(6)} ${c("cyan",o)} ${e.percentage}% (${e.lines} lines)`)}if(console.log(""),console.log(c("bold"," ONBOARDING")),console.log(` Read time: ~${r.estimatedReadTimeMinutes} minutes`),console.log(` Start here: ${r.startHere.slice(0,3).join(", ")}`),r.doNotTouch.length>0&&console.log(` Do not touch: ${c("red",r.doNotTouch.join(", "))}`),console.log(` Summary: ${r.quickSummary}`),console.log(""),s.sacred.length>0){console.log(c("bold"," SACRED FILES (high impact if changed)"));for(const e of s.sacred.slice(0,5))console.log(` ${c("red","!")} ${c("cyan",e.file)} — imported by ${e.importedBy} files`);console.log("")}console.log(c("gray",` Full passport: ${t}`)),console.log(c("gray"," Share with new team members for instant onboarding.")),console.log("")}async function cmdExecutive(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," GENERATING EXECUTIVE REPORT: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore);console.log(c("gray",` Scanning ${n.length} files...`));const l=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),s=new HallucinationDetector({rootPath:o}),t=new GhostCodeDetector({rootPath:o}),a=new AIConfidenceScorer({rootPath:o}),r=new CopyPasteDriftDetector({rootPath:o}),[i,g,d,h,y]=await Promise.all([l.scanFiles(n),s.scan(n),t.scan(n),a.scan(n),r.scan(n)]);let u=0;for(const e of n)try{u+=fs.readFileSync(e,"utf-8").split("\n").length}catch{}const p=[];for(const e of i.issues||[])p.push({file:path.relative(o,e.file),...e});for(const e of g.phantomAPIs)p.push({category:"hallucination",severity:"critical",message:e.name});for(const e of g.deprecatedAPIs)p.push({category:"deprecated",severity:"high",message:e.name});const f=(new TechDebtCostCalculator).calculate(p,n.length),m=new ExecutiveReport({rootPath:o,companyName:e.companyName||void 0}),b=m.generate({scanResults:i,hallResults:g,debtCost:f,ghostResults:d,aiScoreResults:h,cloneResults:y,fileCount:n.length,lineCount:u}),$=m.save(b,o);console.log(c("green"," ✓ Executive report generated!")),console.log(""),console.log(` ${c("bold","Report:")} ${c("cyan",$)}`),console.log(""),console.log(c("gray"," Open in browser → Ctrl+P → Save as PDF")),console.log(c("gray",' Or click the "Save as PDF" button in the report.')),console.log("");try{const{exec:e}=require("child_process"),o=process.platform;e(`${"win32"===o?"start":"darwin"===o?"open":"xdg-open"} "${$}"`),console.log(c("green"," ✓ Opened in your default browser.")),console.log("")}catch(e){}}async function cmdBaseline(e){const o=path.resolve(e.targetPath),n=new BaselineManager(o);if(banner(),e.baselineCreate||!n.exists()){console.log(c("bold"," CREATING BASELINE: ")+c("cyan",o)),console.log("");const l=collectFiles(o,e.ignore),s=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),t=new HallucinationDetector({rootPath:o}),[a,r]=await Promise.all([s.scanFiles(l),t.scan(l)]),i=[];for(const e of a.issues||[])i.push({file:path.relative(o,e.file),...e});for(const e of r.phantomAPIs)i.push({file:e.file,category:"hallucination",severity:"critical",id:e.id,message:e.name});for(const e of r.deprecatedAPIs)i.push({file:e.file,category:"deprecated",severity:"high",id:e.id,message:e.name});for(const e of r.aiSmells)i.push({file:e.file,category:"ai_smell",severity:"warning",id:e.id,message:e.message});const g=n.create(i);console.log(c("green",` ✓ Baseline created: ${c("bold",g.count)} known issues recorded`)),console.log(` ${c("cyan",g.path)}`),console.log(""),console.log(c("gray"," Future scans with --baseline will only report NEW issues.")),console.log(c("gray"," Commit .thuban-baseline.json to share baseline with your team.")),console.log("")}else{const e=n.load();console.log(c("bold"," BASELINE STATUS")),console.log(""),console.log(` Created: ${c("cyan",e.created)}`),console.log(` Known issues: ${c("bold",e.issueCount)}`),console.log(""),console.log(c("gray"," Use --baseline-create to update the baseline")),console.log(c("gray"," Use --baseline with scan to only see new issues")),console.log("")}}function getTelemetryConfigPath(){const e=process.env.HOME||process.env.USERPROFILE||os.homedir();return path.join(e,".thuban-telemetry.json")}function isTelemetryEnabled(){try{return!0===JSON.parse(fs.readFileSync(getTelemetryConfigPath(),"utf-8")).enabled}catch{return!1}}function sendAnonymousTelemetry(e){if(isTelemetryEnabled())try{const o=JSON.stringify({v:e.version||"0.3.1",cmd:e.command,files:e.fileCount,score:e.score,grade:e.grade,langs:e.languages,issues:e.issueCount,duration:e.duration,os:process.platform,node:process.version,ts:(new Date).toISOString()}),n=require("https").request({hostname:"thuban-telemetry.silverwingsbenefits.workers.dev",path:"/report",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(o)},timeout:3e3});n.on("error",()=>{}),n.write(o),n.end()}catch{}}async function cmdTelemetry(e){const o=getTelemetryConfigPath(),n=process.argv.slice(2);if(n.includes("--on"))fs.writeFileSync(o,JSON.stringify({enabled:!0,updated:(new Date).toISOString()})),console.log(""),console.log(c("cyan"," Telemetry enabled")),console.log(c("gray"," Anonymous usage stats will be sent (no code, no paths, no PII)")),console.log(c("gray"," Turn off anytime: npx thuban telemetry --off")),console.log("");else if(n.includes("--off"))fs.writeFileSync(o,JSON.stringify({enabled:!1,updated:(new Date).toISOString()})),console.log(""),console.log(c("cyan"," Telemetry disabled")),console.log(c("gray"," No data will be collected")),console.log("");else{const e=isTelemetryEnabled();console.log(""),console.log(c("bold"," TELEMETRY STATUS")),console.log(""),console.log(` Status: ${e?c("cyan","Enabled"):c("gray","Disabled (default)")}`),console.log(""),console.log(c("gray"," Thuban telemetry is 100% opt-in and anonymous.")),console.log(c("gray"," We collect: file count, score, grade, languages, OS, scan duration.")),console.log(c("gray"," We never collect: code, file names, paths, project names, or PII.")),console.log(""),console.log(` Enable: ${c("cyan","npx thuban telemetry --on")}`),console.log(` Disable: ${c("cyan","npx thuban telemetry --off")}`),console.log("")}}async function runScanSilent(e){const o=/^(https?:\/\/|git@)/.test(e);let n=o?e:path.resolve(e),c=null,l=e;if(o){const o=path.join(os.tmpdir(),"thuban-compare-"+Date.now()+"-"+Math.random().toString(36).slice(2,6)),{execFileSync:s}=require("child_process");s("git",["clone","--depth","1",e,o],{stdio:"pipe",timeout:6e4}),n=o,c=o;const t=e.replace(/\.git$/,"").split("/");l=t[t.length-1]||e}else l=path.basename(path.resolve(e))||e;const s=Date.now(),t=new CodeScanner,a=new DependencyGraph,r=new HallucinationDetector({rootPath:n}),i=collectFiles(n,[]),g={};let d=0,h=0;for(const e of i)try{const o=t.scanFile(e);path.relative(n,e),g[e]=o,a.addFile(e,n),o.motherCode&&o.motherCode.hasDNA&&d++,o.drifts&&o.drifts.length>0&&h++}catch(e){}const y=r.scan(i,n),u=Date.now()-s;let p=0,f=0,m=0;const b={};for(const[,e]of Object.entries(g))if(e.issues){p+=e.issues.length;for(const o of e.issues){"critical"===o.severity&&f++,"high"===o.severity&&m++;const e=o.category||o.type||"other";b[e]=(b[e]||0)+1}}const $=(y.phantomAPIs||[]).length+(y.phantomImports||[]).length+(y.deprecatedAPIs||[]).length;p+=$,f+=(y.phantomAPIs||[]).length+(y.phantomImports||[]).length,m+=(y.deprecatedAPIs||[]).length,$>0&&(b.hallucination=$);const w=a.getOrphanFiles(),v=a.circularDeps||[],S=i.length>0?Math.round(d/i.length*100):0,x=Math.max(0,Math.round(100-15*f-5*m-10*v.length-1*w.length-.1*(100-S)-3*h)),C=x>=90?"A":x>=80?"B":x>=70?"C":x>=60?"D":"F";if(c)try{fs.rmSync(c,{recursive:!0,force:!0})}catch{}return{label:l,files:i.length,score:x,grade:C,elapsed:u,issues:{total:p,critical:f,high:m},categories:b,dependencies:{circular:v.length,orphans:w.length},motherCode:{annotated:d,coverage:S,drifted:h}}}async function cmdCompare(e){const o=process.argv.slice(2).filter(e=>"compare"!==e&&!e.startsWith("--"));o.length<2&&(console.log(""),console.log(c("red"," Compare requires two paths or URLs")),console.log(""),console.log(c("gray"," Usage:")),console.log(` ${c("cyan","npx thuban compare ./project-a ./project-b")}`),console.log(` ${c("cyan","npx thuban compare . https://github.com/user/repo")}`),console.log(` ${c("cyan","npx thuban compare https://github.com/a/repo https://github.com/b/repo")}`),console.log(""),process.exit(1)),console.log(""),console.log(c("bold"," ╔══════════════════════════════════════╗")),console.log(c("bold"," ║ THUBAN — Codebase Comparison ║")),console.log(c("bold"," ╚══════════════════════════════════════╝")),console.log(""),console.log(c("cyan",` Scanning: ${o[0]}`));const n=await runScanSilent(o[0]);console.log(c("cyan",` Scanning: ${o[1]}`));const l=await runScanSilent(o[1]);console.log("");const s=(e,o)=>String(e).padEnd(o),t=(e,o)=>String(e).padStart(o),a=20,r=n.label.length>a?n.label.slice(0,18)+"..":n.label,i=l.label.length>a?l.label.slice(0,18)+"..":l.label,g=n.score>=80?"green":n.score>=60?"yellow":"red",d=l.score>=80?"green":l.score>=60?"yellow":"red";console.log(c("bold"," ┌──────────────────────┬──────────────────────┬──────────────────────┐")),console.log(c("bold",` │ ${s("METRIC",a)} │ ${s(r,a)} │ ${s(i,a)} │`)),console.log(c("bold"," ├──────────────────────┼──────────────────────┼──────────────────────┤"));const h=(e,o,n)=>{console.log(` │ ${c("gray",s(e,a))} │ ${s(o,a)} │ ${s(n,a)} │`)};h("Health Score",`${c(g,n.score+"/100")}`,`${c(d,l.score+"/100")}`),h("Grade",`${c(g,n.grade)}`,`${c(d,l.grade)}`),h("Files Scanned",String(n.files),String(l.files)),h("Total Issues",String(n.issues.total),String(l.issues.total)),h("Critical Issues",String(n.issues.critical),String(l.issues.critical)),h("High Issues",String(n.issues.high),String(l.issues.high)),h("Circular Deps",String(n.dependencies.circular),String(l.dependencies.circular)),h("Orphan Files",String(n.dependencies.orphans),String(l.dependencies.orphans)),h("Mother Code DNA",`${n.motherCode.coverage}%`,`${l.motherCode.coverage}%`),h("Drift Issues",String(n.motherCode.drifted),String(l.motherCode.drifted)),h("Scan Time",`${n.elapsed}ms`,`${l.elapsed}ms`),console.log(c("bold"," └──────────────────────┴──────────────────────┴──────────────────────┘")),console.log("");const y=n.score-l.score;if(0===y)console.log(c("cyan",` VERDICT: Both codebases score identically (${n.score}/100)`));else{const e=y>0?n:l,o=y>0?l:n;console.log(c("cyan",` VERDICT: ${e.label} is healthier`)),console.log(c("gray",` ${e.label} scores ${e.score}/100 (${e.grade}) vs ${o.label} at ${o.score}/100 (${o.grade})`)),console.log(c("gray",` ${Math.abs(y)} point difference — ${Math.abs(y)>20?"significant gap":Math.abs(y)>10?"moderate gap":"close race"}`))}const u=new Set([...Object.keys(n.categories),...Object.keys(l.categories)]);if(u.size>0){console.log(""),console.log(c("bold"," ISSUE BREAKDOWN"));for(const e of u){const o=n.categories[e]||0,a=l.categories[e]||0,g=e.charAt(0).toUpperCase()+e.slice(1);console.log(c("gray",` ${s(g,22)} ${t(o,5)} vs ${t(a,5)} ${o<a?c("green",`← ${r}`):o>a?c("green",`→ ${i}`):c("gray","tied")}`))}}console.log(""),console.log(c("gray",` Full scan: npx thuban scan ${o[0]}`)),console.log(c("gray",` Full scan: npx thuban scan ${o[1]}`)),console.log("")}async function main(){const e=parseArgs(process.argv.slice(2));if(e.command&&"help"!==e.command||(printHelp(),process.exit(0)),"version"===e.command){const e=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8"));console.log(`thuban v${e.version}`),process.exit(0)}["activate","status","upgrade","telemetry","compare"].includes(e.command)||e.targetPath.match(/^(https?:\/\/|git@)/)||fs.existsSync(e.targetPath)||(console.error(c("red",` Error: Path not found: ${e.targetPath}`)),process.exit(1));try{switch(e.command){case"scan":await cmdScan(e);break;case"deps":await cmdDeps(e);break;case"drift":await cmdDrift(e);break;case"inject":await cmdInject(e);break;case"health":case"report":await cmdReport(e);break;case"debt":await cmdDebt(e);break;case"fix":await cmdFix(e);break;case"hallucinate":case"hal":await cmdHallucinate(e);break;case"watch":await cmdWatch(e);break;case"monitor":await cmdMonitor(e);break;case"history":await cmdHistory(e);break;case"trend":await cmdTrend(e);break;case"dashboard":case"dash":await cmdDashboard(e);break;case"diff":await cmdDiff(e);break;case"activate":await cmdActivate(e);break;case"status":await cmdStatus(e);break;case"upgrade":cmdUpgrade();break;case"gate":await cmdGate(e);break;case"cost":await cmdCost(e);break;case"ghosts":await cmdGhosts(e);break;case"ai-score":case"aiscore":await cmdAIScore(e);break;case"clones":case"duplicates":await cmdClones(e);break;case"passport":await cmdPassport(e);break;case"executive":case"exec-report":case"pdf":await cmdExecutive(e);break;case"baseline":await cmdBaseline(e);break;case"telemetry":await cmdTelemetry(e);break;case"compare":await cmdCompare(e);break;default:console.error(""),console.error(c("red",` Unknown command: ${e.command}`)),console.error("");const o=["scan","compare","deps","drift","inject","health","report","debt","fix","hallucinate","watch","dashboard","diff","gate","cost","ghosts","ai-score","clones","passport","executive","baseline","activate","status","upgrade","telemetry"].filter(o=>o.startsWith(e.command.slice(0,2))||o.includes(e.command));o.length>0&&(console.error(c("gray"," Did you mean: ")+c("cyan",o.join(", "))+c("gray","?")),console.error("")),console.error(c("gray"," Quick start:")),console.error(` ${c("cyan","npx thuban scan .")} Scan your project`),console.error(` ${c("cyan","npx thuban --help")} See all commands`),console.error(""),process.exit(1)}}catch(o){console.error(c("red",` Error: ${o.message}`)),e.verbose&&console.error(o.stack),process.exit(1)}}main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),AI_SIGNALS={genericNames:["data","result","response","output","input","temp","item","element","val","obj","arr","str","num","flag","status"],aiCommentPatterns:[/\/\/\s*TODO:?\s*implement/i,/\/\/\s*handle\s+error/i,/\/\/\s*add\s+error\s+handling/i,/\/\/\s*you\s+(can|may|might|should)/i,/\/\/\s*replace\s+(this|with)/i,/\/\/\s*this\s+(function|method|class)\s+(takes|accepts|returns|handles)/i,/\/\/\s*example\s+usage/i,/\/\/\s*note:?\s*this/i,/\/\*\*[\s\S]*?@description\s/,/\/\/\s*import.*if\s+needed/i],aiStructurePatterns:[/try\s*\{[\s\S]*?\}\s*catch\s*\(\s*(?:error|err|e)\s*\)\s*\{[\s\S]*?console\.(log|error)\s*\(\s*(?:error|err|e)\s*\)/,/if\s*\(!.*\)\s*\{?\s*(?:return|throw)\s/]};class AIConfidenceScorer{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}scan(e){const t=[];for(const s of e){let e;try{if(fs.statSync(s).size>524288)continue;e=fs.readFileSync(s,"utf-8")}catch(e){continue}const n=path.relative(this.rootPath,s);if(this._isSkippable(n))continue;const r=this._extractFunctions(e,n);t.push(...r)}t.sort((e,t)=>t.confidence-e.confidence);const s=t.filter(e=>e.confidence>=70),n=t.filter(e=>e.confidence>=40&&e.confidence<70);return{functions:t,highConfidence:s,mediumConfidence:n,totalFunctions:t.length,aiLikelyCount:s.length,aiPossibleCount:n.length,aiPercentage:t.length>0?Math.round(s.length/t.length*100):0}}_extractFunctions(e,t){const s=e.split("\n"),n=[];for(let e=0;e<s.length;e++){const r=this._detectFunction(s,e);if(!r)continue;const o=this._findFunctionEnd(s,e),i=s.slice(e,o+1).join("\n"),c=s.slice(e,o+1),a=this._scoreFunction(r.name,i,c,s,e);n.push({file:t,name:r.name,line:e+1,lineCount:o-e+1,confidence:a.score,signals:a.reasons,testCoverage:this._hasTests(r.name,t)?"found":"none",category:"ai_confidence",severity:a.score>=80?"high":a.score>=50?"medium":"low"}),e=o}return n}_scoreFunction(e,t,s,n,r){let o=0;const i=[],c=AI_SIGNALS.genericNames.filter(e=>{const s=new RegExp(`\\b${e}\\b`,"g");return(t.match(s)||[]).length>=2}).length;c>=3?(o+=15,i.push(`${c} generic variable names (data, result, response...)`)):c>=1&&(o+=8,i.push(`${c} generic variable name(s)`));const a=AI_SIGNALS.aiCommentPatterns.filter(e=>e.test(t)).length;if(a>=2?(o+=20,i.push(`${a} AI-style comments (TODO: implement, handle error, etc.)`)):a>=1&&(o+=10,i.push("AI-style comment detected")),r>0){const e=n.slice(Math.max(0,r-10),r).join("\n");/\/\*\*[\s\S]*@param[\s\S]*@returns?[\s\S]*@(?:throws|example)/s.test(e)&&(o+=10,i.push("Exhaustive JSDoc (params + returns + throws/example)"))}(t.match(/try\s*\{/g)||[]).length>=2&&(o+=10,i.push("Multiple try/catch blocks (AI over-handles errors)"));const l=s.length;return l>=50?(o+=15,i.push(`${l} lines — large self-contained function`)):l>=25&&(o+=8,i.push(`${l} lines`)),0===n.slice(0,r).concat(n.slice(r+l)).filter(t=>t.includes(e)).length&&l>10&&(o+=10,i.push("Isolated — no internal references")),/catch\s*\([^)]*\)\s*\{[^}]*console\.(log|error)\s*\(/.test(t)&&(o+=8,i.push("console.log/error in catch block (AI default pattern)")),/=>\s*\{[\s\n\r]*const\s*\{/.test(t)&&(o+=5,i.push("Immediate destructuring after arrow (common AI pattern)")),(t.match(/`[^`]*\$\{/g)||[]).length>=4&&(o+=5,i.push("Heavy template literal usage")),o=Math.min(100,o),{score:o,reasons:i}}_detectFunction(e,t){const s=e[t].trim();let n=s.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return n?{name:n[1]}:(n=s.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/),n&&(s.includes("=>")||s.includes("function"))?{name:n[1]}:null)}_findFunctionEnd(e,t){let s=0,n=!1;for(let r=t;r<e.length&&r<t+300;r++){for(const t of e[r])"{"===t&&(s++,n=!0),"}"===t&&s--;if(n&&s<=0)return r}return Math.min(t+20,e.length-1)}_hasTests(e,t){return[t.replace(/\.(js|ts)$/,".test.$1"),t.replace(/\.(js|ts)$/,".spec.$1"),t.replace(/src\//,"test/"),t.replace(/src\//,"__tests__/")].some(e=>{try{return fs.existsSync(path.resolve(this.rootPath,e))}catch(e){return!1}})}_isSkippable(e){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/spec[\/\\]/i,/__test__/i,/__mocks__/i,/\.config\./i,/\.d\.ts$/,/node_modules/i,/\.min\./i].some(t=>t.test(e))}}module.exports=AIConfidenceScorer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");class AlertManager extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),alertsDir:e.alertsDir||path.join(e.rootPath||process.cwd(),"sentinel","alerts"),reportsDir:e.reportsDir||path.join(e.rootPath||process.cwd(),"sentinel","reports"),consoleOutput:!1!==e.consoleOutput,fileOutput:!1!==e.fileOutput,maxAlertsPerFile:e.maxAlertsPerFile||1e3,slackWebhook:e.slackWebhook||null,emailConfig:e.emailConfig||null,...e},this._ensureDirectories(),this.recentAlerts=new Map,this.alertDedupeWindowMs=e.alertDedupeWindowMs||6e4}async sendAlert(e){const t=this._normalizeAlert(e);if(this._isDuplicate(t))return{sent:!1,reason:"duplicate"};this._recordAlert(t);const s={console:!1,file:!1,slack:!1,email:!1};return this.config.consoleOutput&&(s.console=this._outputToConsole(t)),this.config.fileOutput&&(s.file=await this._outputToFile(t)),this.config.slackWebhook&&["critical","error"].includes(t.level)&&(s.slack=await this._outputToSlack(t)),this.emit("alertSent",{alert:t,results:s}),{sent:!0,alert:t,results:s}}async sendDigest(e){if(console.log("\n"+"=".repeat(60)),console.log("[SENTINEL] DAILY DIGEST - "+e.date),console.log("=".repeat(60)),console.log("\nSummary:"),console.log(` Issues Found: ${e.summary.issuesFound}`),console.log(` Critical: ${e.summary.criticalIssues}`),console.log(` Warnings: ${e.summary.warningIssues}`),console.log(` Files Watched: ${e.summary.filesWatched}`),console.log(` Last Scan: ${e.summary.lastScan||"N/A"}`),e.topIssues&&e.topIssues.length>0){console.log("\nTop Issues:");for(const t of e.topIssues.slice(0,5))console.log(` [${t.severity.toUpperCase()}] ${t.message}`),t.file&&console.log(` File: ${path.relative(this.config.rootPath,t.file)}`)}if(e.recommendations&&e.recommendations.length>0){console.log("\nRecommendations:");for(const t of e.recommendations)console.log(` [${t.priority.toUpperCase()}] ${t.message}`)}if(console.log("\n"+"=".repeat(60)+"\n"),this.config.fileOutput){const t=path.join(this.config.reportsDir,`digest-${e.date}.json`);fs.writeFileSync(t,JSON.stringify(e,null,2))}return this.emit("digestSent",e),{sent:!0}}async sendWeeklyReport(e){if(console.log("\n"+"=".repeat(70)),console.log("[SENTINEL] WEEKLY REPORT - Week of "+e.weekOf),console.log("=".repeat(70)),console.log("\nMetrics:"),console.log(` Total Scans: ${e.summary.totalScans}`),console.log(` Issues Detected: ${e.summary.issuesDetected}`),console.log(` Issues Resolved: ${e.summary.issuesResolved}`),console.log(` Avg Scan Time: ${e.summary.averageScanTime}ms`),console.log("\nCode Quality:"),console.log(` Score: ${e.codeQuality.score}/100 (${e.codeQuality.grade})`),console.log(` Complexity: ${e.codeQuality.factors.complexity}`),console.log(` Maintainability: ${e.codeQuality.factors.maintainability}`),console.log(` Test Coverage: ${e.codeQuality.factors.testCoverage}%`),console.log("\nTechnical Debt:"),console.log(` Estimated Hours: ${e.technicalDebt.estimatedHours}`),console.log(` Refactoring: ${e.technicalDebt.categories.refactoring}h`),console.log(` Documentation: ${e.technicalDebt.categories.documentation}h`),console.log(` Testing: ${e.technicalDebt.categories.testing}h`),e.recommendations&&e.recommendations.length>0){console.log("\nRecommendations:");for(const t of e.recommendations)console.log(` [${t.priority.toUpperCase()}] ${t.message}`)}if(console.log("\n"+"=".repeat(70)+"\n"),this.config.fileOutput){const t=path.join(this.config.reportsDir,`weekly-report-${e.weekOf}.json`);fs.writeFileSync(t,JSON.stringify(e,null,2))}return this.emit("weeklyReportSent",e),{sent:!0}}getAlertHistory(e={}){const{days:t=7,level:s=null,category:o=null}=e,r=new Date;r.setDate(r.getDate()-t);const l=[];try{if(fs.existsSync(this.config.alertsDir)){const e=fs.readdirSync(this.config.alertsDir).filter(e=>e.startsWith("alerts-")&&e.endsWith(".json")).sort().reverse();for(const t of e){const e=t.match(/alerts-(\d{4}-\d{2}-\d{2})/);if(e&&new Date(e[1])<r)break;try{const e=JSON.parse(fs.readFileSync(path.join(this.config.alertsDir,t),"utf8"));l.push(...e.alerts||[])}catch(e){}}}}catch(e){console.error("[ALERT-MANAGER] Error reading alert history:",e.message)}let n=l;return s&&(n=n.filter(e=>e.level===s)),o&&(n=n.filter(e=>e.category===o)),n}async clearOldAlerts(e=30){const t=new Date;t.setDate(t.getDate()-e);let s=0;try{if(fs.existsSync(this.config.alertsDir)){const e=fs.readdirSync(this.config.alertsDir);for(const o of e){const e=o.match(/(\d{4}-\d{2}-\d{2})/);e&&new Date(e[1])<t&&(fs.unlinkSync(path.join(this.config.alertsDir,o)),s++)}}}catch(e){console.error("[ALERT-MANAGER] Error clearing old alerts:",e.message)}return{deleted:s}}_ensureDirectories(){try{fs.existsSync(this.config.alertsDir)||fs.mkdirSync(this.config.alertsDir,{recursive:!0}),fs.existsSync(this.config.reportsDir)||fs.mkdirSync(this.config.reportsDir,{recursive:!0})}catch(e){console.warn("[ALERT-MANAGER] Could not create directories:",e.message)}}_normalizeAlert(e){return{id:e.id||this._generateAlertId(),timestamp:e.timestamp||(new Date).toISOString(),level:e.level||"info",category:e.category||"general",title:e.title||"Alert",message:e.message||"",file:e.file||null,line:e.line||null,details:e.details||{}}}_generateAlertId(){return`alert-${Date.now()}-${Math.random().toString(36).substr(2,9)}`}_isDuplicate(e){const t=`${e.level}:${e.message}:${e.file}`,s=this.recentAlerts.get(t);return!!(s&&Date.now()-s<this.alertDedupeWindowMs)}_recordAlert(e){const t=`${e.level}:${e.message}:${e.file}`;this.recentAlerts.set(t,Date.now());const s=Date.now()-this.alertDedupeWindowMs;for(const[e,t]of this.recentAlerts)t<s&&this.recentAlerts.delete(e)}_outputToConsole(e){const t={critical:"[31m",error:"[31m",warning:"[33m",info:"[36m"}[e.level]||"";if(console.log(`${t}[SENTINEL:${e.level.toUpperCase()}][0m ${e.title}`),console.log(` ${e.message}`),e.file){const t=path.relative(this.config.rootPath,e.file);console.log(` File: ${t}${e.line?":"+e.line:""}`)}return!0}async _outputToFile(e){try{const t=(new Date).toISOString().split("T")[0],s=path.join(this.config.alertsDir,`alerts-${t}.json`);let o={date:t,alerts:[]};if(fs.existsSync(s))try{o=JSON.parse(fs.readFileSync(s,"utf8"))}catch(e){}return o.alerts.push(e),o.alerts.length>this.config.maxAlertsPerFile&&(o.alerts=o.alerts.slice(-this.config.maxAlertsPerFile)),fs.writeFileSync(s,JSON.stringify(o,null,2)),!0}catch(e){return console.error("[ALERT-MANAGER] File output failed:",e.message),!1}}async _outputToSlack(e){if(!this.config.slackWebhook)return!1;try{return console.log(`[ALERT-MANAGER] Would send to Slack: ${e.title}`),!0}catch(e){return console.error("[ALERT-MANAGER] Slack output failed:",e.message),!1}}async _createGitHubIssue(e){return console.log(`[ALERT-MANAGER] Would create GitHub issue: ${e.title}`),!1}}module.exports=AlertManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto"),BASELINE_FILE=".thuban-baseline.json";class BaselineManager{constructor(e){this.rootPath=e,this.baselinePath=path.join(e,BASELINE_FILE)}create(e){const s={version:1,created:(new Date).toISOString(),generator:"thuban",issueCount:e.length,issues:e.map(e=>({fingerprint:this._fingerprint(e),file:e.file,category:e.category||"unknown",severity:e.severity||"unknown",id:e.id||null,message:(e.message||"").substring(0,200)}))};return fs.writeFileSync(this.baselinePath,JSON.stringify(s,null,2),"utf-8"),{path:this.baselinePath,count:e.length}}load(){try{return JSON.parse(fs.readFileSync(this.baselinePath,"utf-8"))}catch(e){return null}}filterNew(e){const s=this.load();if(!s||!s.issues)return{newIssues:e,baselinedIssues:[],baselineExists:!1};const t=new Set(s.issues.map(e=>e.fingerprint)),n=[],i=[];for(const s of e){const e=this._fingerprint(s);t.has(e)?i.push(s):n.push(s)}return{newIssues:n,baselinedIssues:i,baselineExists:!0,baselineDate:s.created,baselineCount:s.issueCount}}exists(){return fs.existsSync(this.baselinePath)}_fingerprint(e){const s=[e.file||"",e.category||"",e.id||"",(e.message||"").substring(0,100)].join("|");return crypto.createHash("sha256").update(s).digest("hex").substring(0,16)}}module.exports=BaselineManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");class CodeScanner extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],maxFileSize:e.maxFileSize||1048576,complexityThreshold:e.complexityThreshold||15,nestingThreshold:e.nestingThreshold||4,...e},this.safePatterns=[/keyEnv\s*[:=]/i,/envVar\s*[:=]/i,/process\.env\[/,/process\.env\./,/model\s*[:=]\s*['"][^'"]+['"]/i,/version\s*[:=]\s*['"][^'"]+['"]/i,/claude-|gpt-|gemini-|llama-|mistral-|deepseek-|grok-|sonar-/i,/endpoint\s*[:=]/i,/https?:\/\//,/_API_KEY['"]/,/API_KEY['"]\s*\)/,/errors?\.\w+\s*=/i,/Error\s*[:=]/i,/required|invalid|must be|cannot be/i],this.testFilePatterns=[/\.test\.(js|ts|jsx|tsx)$/,/\.spec\.(js|ts|jsx|tsx)$/,/__tests__\//,/\.mock\.(js|ts)$/,/test-.*\.js$/],this.securityPatterns=[{id:"SEC001",name:"Hardcoded API Key",pattern:/['"](?:sk-|pk-|xox[pboa]-|AKIA|ghp_|gho_|github_pat_)[a-zA-Z0-9_-]{10,}['"]|['"][a-zA-Z0-9]{32,}['"]/,context:/(api[_-]?key|apikey|secret[_\s]*key|token|credential|bearer)/i,severity:"critical",message:"Possible hardcoded API key or secret detected",skipIfSafe:!0},{id:"SEC002",name:"Hardcoded Password",pattern:/password\s*[:=]\s*['"][^'"]+['"]/i,severity:"critical",message:"Hardcoded password detected",skipInTests:!0,skipIfSafe:!0},{id:"SEC003",name:"SQL Injection Risk",pattern:/\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WHERE)\b.*(\$\{|\+\s*['"]|\+\s*\w+|f['"].*\{|%s|%d)/i,severity:"error",message:"Potential SQL injection - use parameterized queries"},{id:"SEC004",name:"eval() Usage",pattern:/\beval\s*\(/,severity:"error",message:"eval() is dangerous and should be avoided"},{id:"SEC005",name:"innerHTML Assignment",pattern:/\.innerHTML\s*=/,severity:"warning",message:"innerHTML can cause XSS - consider using textContent or sanitization"},{id:"SEC006",name:"Disabled Security",pattern:/rejectUnauthorized\s*:\s*false|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true/,severity:"error",message:"SSL certificate validation disabled"},{id:"SEC007",name:"Exposed .env Reference",pattern:/\.env['"]/,context:/fs\.(read|write)|path\.join|open\s*\(|os\.Open/i,severity:"warning",message:"Direct .env file manipulation - ensure not exposed"},{id:"SEC008",name:"Unsafe Block (Rust)",pattern:/\bunsafe\s*\{/,severity:"warning",message:"Rust unsafe block - verify memory safety guarantees"},{id:"SEC009",name:"Shell Execution (PHP/Ruby)",pattern:/\bshell_exec\s*\(|\bpassthru\s*\(|`[^`]+`/,severity:"error",message:"Shell execution function - command injection risk if input unsanitized"}],this.qualityPatterns=[{id:"QUAL001",name:"Console.log in Production",pattern:/console\.(log|debug|info)\(/,severity:"info",message:"console.log found - consider using proper logging",skipInTests:!0,skipInDirs:["test","scripts","tools","dev"]},{id:"QUAL002",name:"TODO Comment",pattern:/\/\/\s*TODO|\/\*\s*TODO|\*\s*TODO|#\s*TODO/i,severity:"info",message:"TODO comment found - track in issue system"},{id:"QUAL003",name:"FIXME Comment",pattern:/\/\/\s*FIXME|\/\*\s*FIXME|\*\s*FIXME|#\s*FIXME/i,severity:"warning",message:"FIXME comment found - needs attention"},{id:"QUAL004",name:"Empty Catch Block",pattern:/catch\s*\([^)]*\)\s*\{\s*\}/,severity:"warning",message:"Empty catch block swallows errors"},{id:"QUAL005",name:"Magic Number",pattern:/(?<![0-9a-zA-Z_])(?!(?:1000|2000|3000|3001|4000|5000|8000|8080|9000|80|443|24|60|1024|2048|4096)\b)\d{5,}(?![0-9])/,severity:"info",message:"Magic number detected - consider using named constant",skipInTests:!0},{id:"QUAL006",name:"Callback Hell",pattern:/\)\s*=>\s*\{[^}]*\)\s*=>\s*\{[^}]*\)\s*=>\s*\{/,severity:"warning",message:"Deeply nested callbacks - consider async/await"}],this.performancePatterns=[{id:"PERF001",name:"Sync FS in Async",pattern:/fs\.(readFileSync|writeFileSync|readdirSync|statSync)/,severity:"warning",message:"Synchronous file operation - may block event loop"},{id:"PERF002",name:"Missing Await",pattern:/async\s+function[^{]*\{[^}]*(?<!await\s)new\s+Promise/,severity:"warning",message:"Promise created in async function without await"},{id:"PERF003",name:"Loop Database Query",pattern:/for\s*\([^)]*\)\s*\{[^}]*(query|find|select|update)/i,severity:"error",message:"Possible N+1 query pattern - consider batch operation"},{id:"PERF004",name:"Large Array in Memory",pattern:/new\s+Array\s*\(\s*\d{6,}\s*\)/,severity:"warning",message:"Large array allocation - consider streaming"}],this.aiPatterns=[{id:"AI001",name:"Potentially Hallucinated Import",pattern:/require\(['"](?!\.)[^'"]+['"]\)/,severity:"info",message:"External dependency - verify it exists in package.json",check:(e,s,t)=>this._checkDependencyExists(e,t)},{id:"AI002",name:"Unused Import",pattern:/const\s+(\w+)\s*=\s*require\(['"][^'"]+['"]\)/,severity:"info",message:"Import may be unused - verify usage",check:(e,s)=>this._checkImportUsage(e,s)},{id:"AI003",name:"Over-Engineered Pattern",pattern:/class\s+\w+Factory\s*\{|AbstractFactory|Singleton\.getInstance/,severity:"info",message:"Complex pattern detected - ensure justified by requirements"}]}async scanAll(){console.log("[CODE-SCANNER] Starting full codebase scan...");const e=Date.now(),s={filesScanned:0,issues:[],summary:{},duration:0};try{const t=await this._discoverFiles(this.config.rootPath);console.log(`[CODE-SCANNER] Found ${t.length} files to scan`);for(const e of t){const t=await this.scanFile(e);s.filesScanned++,s.issues.push(...t)}return s.duration=Date.now()-e,s.summary=this._summarizeIssues(s.issues),console.log(`[CODE-SCANNER] Scan complete: ${s.filesScanned} files, ${s.issues.length} issues`),s}catch(e){return console.error("[CODE-SCANNER] Scan failed:",e.message),s.error=e.message,s}}async scanFiles(e){const s={filesScanned:0,issues:[],summary:{}};for(const t of e){const e=await this.scanFile(t);s.filesScanned++,s.issues.push(...e)}return s.summary=this._summarizeIssues(s.issues),s}async scanFile(e){const s=[];try{const t=fs.statSync(e);if(t.isSymbolicLink&&t.isSymbolicLink())return[];if(0===t.size)return[];if(t.size>this.config.maxFileSize)return[{file:e,category:"quality",severity:"info",id:"FILE001",message:`File too large (${Math.round(t.size/1024)}KB) - skipped detailed scan`}];let i;try{i=fs.readFileSync(e)}catch(e){return[]}const n=Math.min(i.length,512);for(let e=0;e<n;e++)if(0===i[e])return[];const r=i.toString("utf8"),a=r.split("\n"),o=path.extname(e).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".py",".pyw",".go",".rs",".java",".kt",".cs",".php",".rb"].includes(o)&&(s.push(...this._runPatternScan(e,r,a,this.securityPatterns,"security")),s.push(...this._runPatternScan(e,r,a,this.qualityPatterns,"quality")),s.push(...this._runPatternScan(e,r,a,this.performancePatterns,"performance")),s.push(...this._runPatternScan(e,r,a,this.aiPatterns,"ai")),s.push(...this._analyzeComplexity(e,r,a)),s.push(...this._analyzeNesting(e,r,a))),".json"===o&&s.push(...this._scanJson(e,r));for(const e of s)this.emit("issueFound",e);return s}catch(s){return[{file:e,category:"error",severity:"error",id:"SCAN001",message:`Failed to scan file: ${s.message}`}]}}getRules(){return{security:this.securityPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),quality:this.qualityPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),performance:this.performancePatterns.map(e=>({id:e.id,name:e.name,severity:e.severity})),ai:this.aiPatterns.map(e=>({id:e.id,name:e.name,severity:e.severity}))}}async _discoverFiles(e,s=[]){try{const t=fs.readdirSync(e,{withFileTypes:!0});for(const i of t){const t=path.join(e,i.name),n=path.relative(this.config.rootPath,t);if(!this._shouldIgnore(n))if(i.isDirectory())await this._discoverFiles(t,s);else if(i.isFile()){const e=path.extname(i.name).toLowerCase();[".js",".ts",".jsx",".tsx",".json",".py",".pyw",".go",".rs",".java",".kt",".cs",".php",".rb"].includes(e)&&s.push(t)}}}catch(e){}return s}_shouldIgnore(e){const s=e.replace(/\\/g,"/"),t=["node_modules",".git","dist",".vite"];for(const e of t)if(s.includes("/"+e+"/")||s.startsWith(e+"/")||s===e||s.endsWith("/"+e))return!0;for(const e of this.config.ignorePatterns){const t=e.replace(/\\/g,"/"),i=t.split("/")[0].replace(/\*/g,"");if(i&&(s===i||s.startsWith(i+"/")))return!0;let n=t.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");if(new RegExp("^"+n).test(s))return!0}return!1}_runPatternScan(e,s,t,i,n){const r=[],a=path.basename(e),o=this.testFilePatterns.some(e=>e.test(a))||this.testFilePatterns.some(s=>s.test(e)&&!s.source.includes("\\.js$")),c=path.relative(this.config.rootPath,e).replace(/\\/g,"/");for(const a of i){if(a.skipInTests&&o)continue;if(a.skipInDirs&&a.skipInDirs.some(e=>c.startsWith(e+"/")||c.includes("/"+e+"/")))continue;if(a.context&&!a.lineContext&&!a.context.test(s))continue;let i=0;for(const o of t){i++;const t=o.match(a.pattern);if(t){if(a.skipIfSafe&&this.safePatterns.some(e=>e.test(o)))continue;if(a.check&&!a.check(t,s,e))continue;r.push({file:e,line:i,column:t.index,category:n,severity:a.severity,id:a.id,name:a.name,message:a.message,code:o.trim().substring(0,100)})}}}return r}_analyzeComplexity(e,s,t){const i=[],n=s.match(/function\s+\w+|=>\s*\{|async\s+\w+/g)||[],r=s.match(/\b(if|else|for|while|switch|case|catch|&&|\|\||\?)\b/g)||[],a=n.length>0?r.length/n.length:0;return a>this.config.complexityThreshold&&i.push({file:e,category:"quality",severity:"warning",id:"CMPLX001",name:"High Complexity",message:`Average cyclomatic complexity is ${a.toFixed(1)} (threshold: ${this.config.complexityThreshold})`}),t.length>500&&i.push({file:e,category:"quality",severity:"info",id:"CMPLX002",name:"Long File",message:`File has ${t.length} lines - consider splitting`}),i}_analyzeNesting(e,s,t){const i=[];let n=0,r=0,a=0,o=0;for(const e of t)o++,r+=(e.match(/\{/g)||[]).length-(e.match(/\}/g)||[]).length,r>n&&(n=r,a=o);return n>this.config.nestingThreshold&&i.push({file:e,line:a,category:"quality",severity:"warning",id:"NEST001",name:"Deep Nesting",message:`Max nesting depth is ${n} at line ${a} (threshold: ${this.config.nestingThreshold})`}),i}_scanJson(e,s){const t=[];try{JSON.parse(s)}catch(s){t.push({file:e,category:"quality",severity:"error",id:"JSON001",name:"Invalid JSON",message:`JSON parse error: ${s.message}`})}return/(password|secret|api[_-]?key|token)/i.test(s)&&t.push({file:e,category:"security",severity:"warning",id:"JSON002",name:"Sensitive Data in JSON",message:"Possible sensitive data in JSON file - verify not committed"}),t}_checkDependencyExists(e,s){const t=e[0].match(/require\(['"]([^'"]+)['"]\)/);if(!t)return!0;const i=t[1];if(i.startsWith("."))return!1;if(["fs","path","os","crypto","http","https","events","util","stream","child_process","url","querystring","buffer"].includes(i.split("/")[0]))return!1;try{let e=path.join(this.config.rootPath,"package.json");if(s.includes("server")){const s=path.join(this.config.rootPath,"server","package.json");fs.existsSync(s)&&(e=s)}const t=JSON.parse(fs.readFileSync(e,"utf8")),n={...t.dependencies||{},...t.devDependencies||{}};return!n[i.split("/")[0]]&&!n[i]}catch(e){return!1}}_checkImportUsage(e,s){const t=e[1];if(!t)return!1;const i=new RegExp("\\b"+t+"\\b","g");return(s.match(i)||[]).length<=1}_summarizeIssues(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,s)=>(e[s.category]=(e[s.category]||0)+1,e),{}),topFiles:this._getTopFiles(e,10)}}_getTopFiles(e,s){const t=e.reduce((e,s)=>(e[s.file]=(e[s.file]||0)+1,e),{});return Object.entries(t).sort((e,s)=>s[1]-e[1]).slice(0,s).map(([e,s])=>({file:path.relative(this.config.rootPath,e),count:s}))}}module.exports=CodeScanner;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");class CodebasePassport{constructor(e={}){this.rootPath=e.rootPath||process.cwd()}generate(e,t={}){const s=Date.now(),r=[],i={},n={},o=[],c=[];let a=0;const l={},u=new Set;for(const t of e){let s;try{if(fs.statSync(t).size>1048576)continue;s=fs.readFileSync(t,"utf-8")}catch(e){continue}const d=path.relative(this.rootPath,t),p=s.split("\n");a+=p.length;const h=path.extname(t);l[h]=(l[h]||0)+p.length,this._detectFrameworks(s,u);const m=this._extractDependencies(s,d),f=this._extractExports(s,d);i[d]=m,f.length>0&&(n[d]=f),this._isEntryPoint(d,s)&&o.push(d);const g=e.filter(e=>{try{const t=fs.readFileSync(e,"utf-8");return t.includes(d.replace(/\\/g,"/"))||t.includes(d.replace(/\//g,"\\"))}catch(e){return!1}}).length;g>5&&c.push({file:d,importedBy:g,risk:g>10?"critical":"high",note:`Changing this file affects ${g} other files. Proceed with caution.`});const y=this._detectPurpose(d,s);r.push({path:d,lines:p.length,purpose:y,imports:m.length,exports:f.length})}const d=this._findCriticalPaths(i),p=this._detectArchitecture(r);return{_version:"1.0.0",_generated:(new Date).toISOString(),_generator:"thuban",project:{name:path.basename(this.rootPath),rootPath:this.rootPath,totalFiles:e.length,totalLines:a,languages:Object.entries(l).sort((e,t)=>t[1]-e[1]).map(([e,t])=>({extension:e,lines:t,percentage:Math.round(t/a*100)})),frameworks:[...u],architecture:p,entryPoints:o},files:r.sort((e,t)=>t.imports-e.imports).slice(0,200),sacred:c.sort((e,t)=>t.importedBy-e.importedBy),dependencies:{totalChains:Object.keys(i).length,criticalPaths:d.slice(0,20),circularRisks:this._findCircularRisks(i)},onboarding:{estimatedReadTimeMinutes:Math.ceil(a/200),startHere:o.slice(0,5),keyFiles:c.slice(0,10).map(e=>e.file),doNotTouch:c.filter(e=>"critical"===e.risk).map(e=>e.file),quickSummary:this._generateQuickSummary(r,u,p)},generatedIn:Date.now()-s+"ms"}}save(e,t){const s=t||path.join(this.rootPath,".thuban-passport.json");return fs.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}_extractDependencies(e,t){const s=[],r=[/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,/import\s+.*?from\s+['"]([^'"]+)['"]/g,/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g];for(const t of r){let r;for(;null!==(r=t.exec(e));)s.push(r[1])}return[...new Set(s)]}_extractExports(e,t){const s=[];e.match(/module\.exports\s*=\s*(\{[\s\S]*?\}|[a-zA-Z_$]\w*)/)&&s.push("module.exports");const r=e.match(/exports\.([a-zA-Z_$]\w*)\s*=/g);r&&r.forEach(e=>s.push(e.replace(/\s*=\s*$/,"")));const i=e.match(/export\s+(?:default\s+)?(?:const|let|var|function|class)\s+([a-zA-Z_$]\w*)/g);return i&&i.forEach(e=>s.push(e)),s}_isEntryPoint(e,t){return!!["index.","main.","app.","server.","cli.","bin/"].some(t=>e.includes(t))||!!t.includes("#!/usr/bin/env")||!(!t.includes("createServer")&&!t.includes(".listen("))}_detectPurpose(e,t){const s=e.toLowerCase();return/route/i.test(s)?"routing":/middleware/i.test(s)?"middleware":/model/i.test(s)?"data model":/controller/i.test(s)?"controller":/service/i.test(s)?"service":/util|helper|lib/i.test(s)?"utility":/config/i.test(s)?"configuration":/test|spec/i.test(s)?"test":/component/i.test(s)?"UI component":/hook/i.test(s)?"React hook":/store|redux|state/i.test(s)?"state management":/api/i.test(s)?"API layer":/db|database|migration/i.test(s)?"database":/auth/i.test(s)?"authentication":/cli|command/i.test(s)?"CLI":t.includes("app.get(")||t.includes("router.")?"routing":t.includes("mongoose.model")||t.includes("Schema")?"data model":t.includes("React")||t.includes("jsx")?"React component":"application logic"}_detectFrameworks(e,t){(e.includes("require('express'")||e.includes("from 'express'"))&&t.add("Express"),(e.includes("require('react'")||e.includes("from 'react'"))&&t.add("React"),(e.includes("require('next'")||e.includes("from 'next'"))&&t.add("Next.js"),(e.includes("require('mongoose'")||e.includes("from 'mongoose'"))&&t.add("Mongoose"),(e.includes("require('fastify'")||e.includes("from 'fastify'"))&&t.add("Fastify"),(e.includes("require('vue'")||e.includes("from 'vue'"))&&t.add("Vue"),(e.includes("require('stripe'")||e.includes("from 'stripe'"))&&t.add("Stripe"),e.includes("@angular")&&t.add("Angular")}_detectArchitecture(e){const t=e.map(e=>e.path.toLowerCase());return t.some(e=>e.includes("controller"))&&t.some(e=>e.includes("model"))?"MVC":t.some(e=>e.includes("service"))&&t.some(e=>e.includes("repository"))?"Service-Repository":t.some(e=>e.includes("component"))&&t.some(e=>e.includes("store"))?"Component-Store (SPA)":t.some(e=>e.includes("pages/"))&&t.some(e=>e.includes("api/"))?"Next.js (Pages Router)":t.some(e=>e.includes("app/"))&&t.some(e=>e.includes("route"))?"App Router":t.some(e=>e.includes("packages/"))?"Monorepo":"Flat"}_findCriticalPaths(e){return Object.entries(e).filter(([,e])=>e.length>3).map(([e,t])=>({file:e,dependsOn:t.length,topDeps:t.slice(0,5)})).sort((e,t)=>t.dependsOn-e.dependsOn).slice(0,20)}_findCircularRisks(e){const t=[];for(const[s,r]of Object.entries(e))for(const i of r){const r=this._resolveRelative(s,i);r&&e[r]&&e[r].some(e=>this._resolveRelative(r,e)===s)&&t.push({fileA:s,fileB:r,risk:"circular dependency"})}return t.slice(0,10)}_resolveRelative(e,t){if(t.startsWith(".")){const s=path.dirname(e);let r=path.join(s,t).replace(/\\/g,"/");return r.match(/\.\w+$/)||(r+=".js"),r}return null}_generateQuickSummary(e,t,s){return`${e.length}-file ${s} project using ${[...t].join(", ")||"vanilla"}. Start with entry points listed above.`}}module.exports=CodebasePassport;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");class CopyPasteDriftDetector{constructor(t={}){this.rootPath=t.rootPath||process.cwd(),this.minLines=t.minLines||5,this.similarityThreshold=t.similarity||.7}scan(t){const e=[];for(const n of t){let t;try{if(fs.statSync(n).size>524288)continue;t=fs.readFileSync(n,"utf-8")}catch(t){continue}const s=path.relative(this.rootPath,n);if(this._isSkippable(s))continue;const i=this._extractFunctions(t,s);e.push(...i)}const n=this._findClusters(e),s=n.reduce((t,e)=>t+e.members.reduce((t,e)=>t+e.lineCount,0)-e.members[0].lineCount,0);return{clusters:n,totalClusters:n.length,totalDuplicateInstances:n.reduce((t,e)=>t+e.members.length,0),totalDuplicateLines:s,recommendation:n.length>0?`Extract ${n.length} shared utilities to eliminate ${s} duplicate lines.`:"No copy-paste drift detected."}}_extractFunctions(t,e){const n=t.split("\n"),s=[];for(let t=0;t<n.length;t++){const i=this._detectFunction(n[t]);if(!i)continue;const r=this._findFunctionEnd(n,t),a=n.slice(t,r+1).join("\n"),o=r-t+1;if(o<this.minLines)continue;const l=this._normalize(a),c=crypto.createHash("md5").update(l).digest("hex"),h=this._tokenize(l);s.push({file:e,name:i.name,line:t+1,lineCount:o,hash:c,tokens:h,normalized:l}),t=r}return s}_findClusters(t){const e=new Set,n=[];for(let s=0;s<t.length;s++){if(e.has(s))continue;const i=[t[s]];e.add(s);for(let n=s+1;n<t.length;n++){if(e.has(n))continue;if(t[s].hash===t[n].hash){i.push(t[n]),e.add(n);continue}const r=this._tokenSimilarity(t[s].tokens,t[n].tokens);r>=this.similarityThreshold&&(i.push({...t[n],similarity:Math.round(100*r)}),e.add(n))}if(i.length>=2){const t=this._findDiffs(i);n.push({pattern:i[0].name,members:i.map(t=>({file:t.file,name:t.name,line:t.line,lineCount:t.lineCount,similarity:t.similarity||100})),totalInstances:i.length,differences:t,severity:i.length>=4?"high":i.length>=3?"medium":"low",recommendation:`Extract to shared utility. ${i.length} functions → 1.`})}}return n.sort((t,e)=>e.totalInstances-t.totalInstances)}_normalize(t){const e=[],n=/(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;let s;for(;null!==(s=n.exec(t));)e.includes(s[1])||e.push(s[1]);let i=t.replace(/\/\/.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"").replace(/'[^']*'/g,"STR").replace(/"[^"]*"/g,"STR").replace(/`[^`]*`/g,"STR").replace(/\b\d+\.?\d*\b/g,"NUM");return e.forEach((t,e)=>{const n=new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`,"g");i=i.replace(n,`VAR_${e}`)}),i.replace(/\s+/g," ").trim()}_tokenize(t){return t.split(/[\s{}()\[\];,=+\-*/<>!&|?:.]+/).filter(t=>t.length>0)}_tokenSimilarity(t,e){if(0===t.length&&0===e.length)return 1;if(0===t.length||0===e.length)return 0;const n=new Set(t),s=new Set(e),i=[...n].filter(t=>s.has(t)).length,r=new Set([...t,...e]).size;return.7*(r>0?i/r:0)+Math.min(t.length,e.length)/Math.max(t.length,e.length)*.3}_findDiffs(t){if(t.length<2)return[];const e=[],n=t[0].normalized;for(let s=1;s<t.length;s++){const i=t[s].normalized;if(n!==i){const r=new Set(this._tokenize(n)),a=new Set(this._tokenize(i)),o=[...a].filter(t=>!r.has(t)),l=[...r].filter(t=>!a.has(t));(o.length>0||l.length>0)&&e.push({file:t[s].file,addedTokens:o.slice(0,5),removedTokens:l.slice(0,5)})}}return e}_detectFunction(t){const e=t.trim();let n=e.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);return n?{name:n[1]}:(n=e.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/),n?{name:n[1]}:(n=e.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=e.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/),n?{name:n[1]}:(n=e.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/),n?{name:n[1]}:(n=e.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/),n?{name:n[1]}:null)))))}_findFunctionEnd(t,e){let n=0,s=!1;for(let i=e;i<t.length&&i<e+200;i++){for(const e of t[i])"{"===e&&(n++,s=!0),"}"===e&&n--;if(s&&n<=0)return i}return Math.min(e+20,t.length-1)}_isSkippable(t){return[/\.test\./i,/\.spec\./i,/test[\/\\]/i,/node_modules/i,/\.min\./i,/\.d\.ts$/,/\.config\./i].some(e=>e.test(t))}}module.exports=CopyPasteDriftDetector;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");class DependencyGraph extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),ignorePatterns:e.ignorePatterns||["node_modules",".git","dist",".vite",".orion/snapshots"],extensions:e.extensions||[".js",".ts",".jsx",".tsx",".php",".rb"],...e},this.graph=new Map,this.resolveCache=new Map,this.stats={filesAnalyzed:0,totalImports:0,unresolvedImports:0,circularDependencies:[]}}get circularDeps(){return this.stats.circularDependencies}async build(){Date.now();this.graph.clear(),this.resolveCache.clear(),this.stats={filesAnalyzed:0,totalImports:0,unresolvedImports:0,circularDependencies:[]};const e=await this._discoverFiles(this.config.rootPath);for(const t of e)await this._analyzeFile(t);return this._buildReverseDependencies(),this._detectCircularDependencies(),Date.now(),this.getStats()}getDependencies(e){const t=this._normalizePath(e),s=this.graph.get(t);return s?s.imports:[]}getDependents(e){const t=this._normalizePath(e),s=this.graph.get(t);return s?s.importedBy:[]}getFullDependencyChain(e,t=new Set){const s=this._normalizePath(e);if(t.has(s))return[];t.add(s);const i=this.getDependencies(s),r=[...i];for(const e of i)e.resolved&&r.push(...this.getFullDependencyChain(e.resolved,t));return r}getImpactAnalysis(e){const t=this._normalizePath(e),s={file:t,relativePath:path.relative(this.config.rootPath,t),directDependents:[],indirectDependents:[],totalImpact:0,riskLevel:"low",affectedModules:new Set};s.directDependents=this.getDependents(t).map(e=>({file:e,relativePath:path.relative(this.config.rootPath,e)}));const i=new Set([t]),r=[...s.directDependents.map(e=>e.file)];for(;r.length>0;){const e=r.shift();if(i.has(e))continue;i.add(e);const t=this.getDependents(e);for(const o of t)i.has(o)||(s.indirectDependents.push({file:o,relativePath:path.relative(this.config.rootPath,o),via:path.relative(this.config.rootPath,e)}),r.push(o))}s.totalImpact=s.directDependents.length+s.indirectDependents.length,0===s.totalImpact?s.riskLevel="none":s.totalImpact<=3?s.riskLevel="low":s.totalImpact<=10?s.riskLevel="medium":s.totalImpact<=25?s.riskLevel="high":s.riskLevel="critical";for(const e of[...s.directDependents,...s.indirectDependents]){const t=e.relativePath.split(path.sep);t.length>0&&s.affectedModules.add(t[0])}return s.affectedModules=Array.from(s.affectedModules),s}getMostCriticalFiles(e=20){const t=[];for(const[e,s]of this.graph)t.push({file:e,relativePath:path.relative(this.config.rootPath,e),dependentCount:s.importedBy.length,dependencyCount:s.imports.length});return t.sort((e,t)=>t.dependentCount-e.dependentCount).slice(0,e)}getOrphanFiles(){const e=[];for(const[t,s]of this.graph)0===s.imports.length&&0===s.importedBy.length&&e.push({file:t,relativePath:path.relative(this.config.rootPath,t)});return e}getModuleSummary(){const e=new Map;for(const[t,s]of this.graph){const i=path.relative(this.config.rootPath,t),r=i.split(path.sep)[0];e.has(r)||e.set(r,{name:r,files:[],internalDeps:0,externalDeps:0,dependedOnBy:new Set});const o=e.get(r);o.files.push(i);for(const e of s.imports)e.resolved&&(path.relative(this.config.rootPath,e.resolved).split(path.sep)[0]===r?o.internalDeps++:o.externalDeps++);for(const e of s.importedBy){const t=path.relative(this.config.rootPath,e).split(path.sep)[0];t!==r&&o.dependedOnBy.add(t)}}const t=[];for(const[s,i]of e)t.push({name:s,fileCount:i.files.length,internalDeps:i.internalDeps,externalDeps:i.externalDeps,dependedOnByCount:i.dependedOnBy.size,dependedOnBy:Array.from(i.dependedOnBy)});return t.sort((e,t)=>t.fileCount-e.fileCount)}getStats(){return{...this.stats,totalFiles:this.graph.size,circularCount:this.stats.circularDependencies.length}}toJSON(){const e={generated:(new Date).toISOString(),rootPath:this.config.rootPath,stats:this.getStats(),files:{}};for(const[t,s]of this.graph){const i=path.relative(this.config.rootPath,t);e.files[i]={imports:s.imports.map(e=>({module:e.module,resolved:e.resolved?path.relative(this.config.rootPath,e.resolved):null,specifiers:e.specifiers})),importedBy:s.importedBy.map(e=>path.relative(this.config.rootPath,e))}}return e}async _discoverFiles(e,t=[]){try{const s=fs.readdirSync(e,{withFileTypes:!0});for(const i of s){const s=path.join(e,i.name);if(!this._shouldIgnore(i.name))if(i.isDirectory())await this._discoverFiles(s,t);else if(i.isFile()){const e=path.extname(i.name).toLowerCase();this.config.extensions.includes(e)&&t.push(s)}}}catch(e){}return t}_shouldIgnore(e){return this.config.ignorePatterns.some(t=>e===t||e.startsWith(t))}async _analyzeFile(e){try{const t=fs.readFileSync(e,"utf8"),s=this._extractImports(t,e);this.graph.set(e,{imports:s,importedBy:[]}),this.stats.filesAnalyzed++,this.stats.totalImports+=s.length,this.stats.unresolvedImports+=s.filter(e=>!e.resolved).length}catch(e){}}_extractImports(e,t){const s=[],i=path.dirname(t),r=/(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;let o;for(;null!==(o=r.exec(e));){const e=o[1]?o[1].split(",").map(e=>e.trim().split(":")[0].trim()).filter(Boolean):[o[2]],t=o[3];s.push({module:t,specifiers:e,resolved:this._resolveModule(t,i),type:"require"})}const n=/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;for(;null!==(o=n.exec(e));){const e=o[1];s.some(t=>t.module===e)||s.push({module:e,specifiers:[],resolved:this._resolveModule(e,i),type:"require"})}const a=/import\s+(?:(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*)?)+\s+from\s+['"]([^'"]+)['"]/g;for(;null!==(o=a.exec(e));){const e=[];o[1]&&e.push(...o[1].split(",").map(e=>e.trim().split(" as ")[0].trim()).filter(Boolean)),o[2]&&e.push(o[2]),o[3]&&e.push(o[3]);const t=o[4];s.push({module:t,specifiers:e,resolved:this._resolveModule(t,i),type:"import"})}return s}_resolveModule(e,t){const s=`${t}:${e}`;if(this.resolveCache.has(s))return this.resolveCache.get(s);let i=null;if(!e.startsWith(".")&&!e.startsWith("/"))return this.resolveCache.set(s,null),null;const r=path.resolve(t,e),o=["",".js",".ts",".jsx",".tsx",".php",".rb","/index.js","/index.ts"];for(const e of o){const t=r+e;if(fs.existsSync(t)&&fs.statSync(t).isFile()){i=t;break}}return this.resolveCache.set(s,i),i}_buildReverseDependencies(){for(const[e,t]of this.graph)for(const s of t.imports)if(s.resolved&&this.graph.has(s.resolved)){const t=this.graph.get(s.resolved);t.importedBy.includes(e)||t.importedBy.push(e)}}_detectCircularDependencies(){const e=new Set,t=new Set,s=(i,r=[])=>{if(t.has(i)){const e=r.indexOf(i),t=r.slice(e).map(e=>path.relative(this.config.rootPath,e));t.push(path.relative(this.config.rootPath,i));const s=t.sort().join(" -> ");return void(this.stats.circularDependencies.some(e=>e.sort().join(" -> ")===s)||this.stats.circularDependencies.push(t))}if(e.has(i))return;e.add(i),t.add(i);const o=this.graph.get(i);if(o)for(const e of o.imports)e.resolved&&s(e.resolved,[...r,i]);t.delete(i)};for(const e of this.graph.keys())s(e)}_normalizePath(e){return path.isAbsolute(e)?e:path.resolve(this.config.rootPath,e)}}module.exports=DependencyGraph;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),EventEmitter=require("events");class DriftDetector extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),widgetPattern:e.widgetPattern||/\/\*\*[\s\S]*?@(purpose|module|layer|imports-from|exports-to|depends-on|critical-for)[\s\S]*?\*\//,ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],...e},this.analysisCache=new Map}async detectAll(){const e=Date.now(),t={filesAnalyzed:0,filesWithWidgets:0,filesWithoutWidgets:0,issues:[],moduleMap:{},duration:0};try{const s=await this._discoverFiles(this.config.rootPath);for(const e of s){const s=await this.analyzeFile(e);t.filesAnalyzed++,s.hasWidget?t.filesWithWidgets++:(t.filesWithoutWidgets++,t.issues.push({file:e,category:"drift",severity:"info",id:"DRIFT001",name:"Missing Widget",message:"File has no widget declaration - add @purpose, @module, @layer etc."})),t.issues.push(...s.issues),s.module&&(t.moduleMap[s.module]||(t.moduleMap[s.module]=[]),t.moduleMap[s.module].push(e))}return t.issues.push(...this._analyzeCrossFile(t.moduleMap)),t.duration=Date.now()-e,t}catch(e){return console.error("[DRIFT-DETECTOR] Detection failed:",e.message),t.error=e.message,t}}async analyzeFile(e){const t={file:e,hasWidget:!1,widget:null,actualExports:[],actualImports:[],module:null,issues:[]};try{const s=fs.readFileSync(e,"utf8");return t.widget=this._parseWidget(s),t.hasWidget=!!t.widget,t.actualExports=this._extractExports(s),t.actualImports=this._extractImports(s),t.widget&&(t.module=t.widget.module,t.issues.push(...this._compareWidgetToCode(e,t.widget,{exports:t.actualExports,imports:t.actualImports}))),this.analysisCache.set(e,t),t}catch(s){return t.issues.push({file:e,category:"drift",severity:"error",id:"DRIFT000",message:`Failed to analyze file: ${s.message}`}),t}}async checkFileDrift(e){const t=await this.analyzeFile(e);return{hasDrift:t.issues.length>0,issues:t.issues,widget:t.widget}}async suggestWidget(e){try{const t=fs.readFileSync(e,"utf8"),s=this._extractExports(t),i=this._extractImports(t),o=path.relative(this.config.rootPath,e),r=o.split(path.sep)[0];let n="application";return o.includes("routes")||o.includes("api")?n="presentation":o.includes("core")||o.includes("engine")?n="infrastructure":(o.includes("model")||o.includes("domain"))&&(n="domain"),{purpose:s.length>0?`Provides ${s.join(", ")}`:"Unknown purpose - needs description",module:r,layer:n,importsFrom:i.map(e=>`${e.module} - ${e.specifiers.join(", ")}`),exportsTo:"To be determined",dataFlow:"To be documented",sideEffects:t.includes("fs.")?"File system operations":"None documented",criticalFor:"To be documented"}}catch(e){return{error:e.message}}}async _discoverFiles(e,t=[]){try{const s=fs.readdirSync(e,{withFileTypes:!0});for(const i of s){const s=path.join(e,i.name),o=path.relative(this.config.rootPath,s);this._shouldIgnore(o)||(i.isDirectory()?await this._discoverFiles(s,t):i.isFile()&&i.name.endsWith(".js")&&t.push(s))}}catch(e){}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),s=["node_modules",".git","dist",".vite"];for(const e of s)if(t.includes("/"+e+"/")||t.startsWith(e+"/")||t===e||t.endsWith("/"+e))return!0;for(const e of this.config.ignorePatterns){const s=e.replace(/\\/g,"/"),i=s.split("/")[0].replace(/\*/g,"");if(i&&(t===i||t.startsWith(i+"/")))return!0;let o=s.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");if(new RegExp("^"+o).test(t))return!0}return!1}_parseWidget(e){const t=e.match(/\/\*\*[\s\S]*?\*\//);if(!t)return null;const s=t[0],i={},o=["purpose","module","layer","imports-from","exports-to","data-flow","side-effects","depends-on-env","critical-for","known-issues","last-audit","last-modified"];for(const e of o){const t=s.match(new RegExp(`@${e}\\s+(.+?)(?=\\n\\s*\\*\\s*@|\\n\\s*\\*\\/|$)`,"s"));t&&(i[e.replace(/-/g,"_")]=t[1].trim().replace(/\n\s*\*\s*/g," ").trim())}return Object.keys(i).length>0?i:null}_extractExports(e){const t=[],s=e.match(/module\.exports\s*=\s*\{([^}]+)\}/);if(s){const e=s[1].match(/\w+/g)||[];t.push(...e)}const i=e.match(/module\.exports\s*=\s*(class\s+)?(\w+)/);i&&i[2]&&t.push(i[2]);const o=e.match(/exports\.(\w+)\s*=/g)||[];for(const e of o){const s=e.match(/exports\.(\w+)/);s&&t.push(s[1])}return[...new Set(t)]}_extractImports(e){const t=[],s=e.matchAll(/(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\(['"]([^'"]+)['"]\)/g);for(const e of s){const s=e[1]?e[1].split(",").map(e=>e.trim().split(":")[0].trim()):[e[2]];t.push({module:e[3],specifiers:s.filter(Boolean)})}return t}_compareWidgetToCode(e,t,s){const i=[];if(t.exports_to){const o=t.exports_to.split(",").map(e=>e.trim().split(" ")[0]).filter(e=>!s.exports.some(t=>t.toLowerCase().includes(e.toLowerCase())));o.length>0&&i.push({file:e,category:"drift",severity:"warning",id:"DRIFT002",name:"Missing Declared Export",message:`Widget declares exports not found in code: ${o.join(", ")}`})}if(t.imports_from){const o=t.imports_from.split(",").map(e=>e.trim().split(" ")[0]),r=s.imports.map(e=>e.module),n=o.filter(e=>!r.some(t=>t.includes(e)||e.includes(t.split("/").pop())));n.length>0&&i.push({file:e,category:"drift",severity:"warning",id:"DRIFT003",name:"Missing Declared Import",message:`Widget declares imports not found in code: ${n.join(", ")}`})}return!/fs\.(write|append|unlink|mkdir|rmdir)/i.test(fs.readFileSync(e,"utf8"))||t.side_effects&&"None"!==t.side_effects||i.push({file:e,category:"drift",severity:"warning",id:"DRIFT004",name:"Undeclared Side Effects",message:"File has file system operations but widget declares no side effects"}),i}_analyzeCrossFile(e){const t=[];for(const[s,i]of Object.entries(e))1===i.length&&t.push({file:i[0],category:"drift",severity:"info",id:"DRIFT005",name:"Orphaned Module",message:`Module "${s}" only has one file - verify module organization`});return t}}module.exports=DriftDetector;
|
|
@@ -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;
|