gitfamiliar 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cli/index.ts","../../src/core/types.ts","../../src/cli/options.ts","../../src/cli/output/terminal.ts","../../src/cli/output/html.ts","../../src/utils/open-browser.ts","../../src/git/contributors.ts","../../src/core/team-coverage.ts","../../src/cli/output/coverage-terminal.ts","../../src/cli/output/coverage-html.ts","../../src/core/multi-user.ts","../../src/cli/output/multi-user-terminal.ts","../../src/cli/output/multi-user-html.ts","../../src/git/change-frequency.ts","../../src/core/hotspot.ts","../../src/cli/output/hotspot-terminal.ts","../../src/cli/output/hotspot-html.ts","../../bin/gitfamiliar.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { parseOptions } from \"./options.js\";\nimport { computeFamiliarity } from \"../core/familiarity.js\";\nimport { renderTerminal } from \"./output/terminal.js\";\nimport { generateAndOpenHTML } from \"./output/html.js\";\nimport { computeTeamCoverage } from \"../core/team-coverage.js\";\nimport { renderCoverageTerminal } from \"./output/coverage-terminal.js\";\nimport { generateAndOpenCoverageHTML } from \"./output/coverage-html.js\";\nimport { computeMultiUser } from \"../core/multi-user.js\";\nimport { renderMultiUserTerminal } from \"./output/multi-user-terminal.js\";\nimport { generateAndOpenMultiUserHTML } from \"./output/multi-user-html.js\";\nimport { computeHotspots } from \"../core/hotspot.js\";\nimport { renderHotspotTerminal } from \"./output/hotspot-terminal.js\";\nimport { generateAndOpenHotspotHTML } from \"./output/hotspot-html.js\";\n\nfunction collect(value: string, previous: string[]): string[] {\n return previous.concat([value]);\n}\n\nexport function createProgram(): Command {\n const program = new Command();\n\n program\n .name(\"gitfamiliar\")\n .description(\"Visualize your code familiarity from Git history\")\n .version(\"0.1.1\")\n .option(\n \"-m, --mode <mode>\",\n \"Scoring mode: binary, authorship, weighted\",\n \"binary\",\n )\n .option(\n \"-u, --user <user>\",\n \"Git user name or email (repeatable for comparison)\",\n collect,\n [],\n )\n .option(\n \"-e, --expiration <policy>\",\n \"Expiration policy: never, time:180d, change:50%, combined:365d:50%\",\n \"never\",\n )\n .option(\"--html\", \"Generate HTML treemap report\", false)\n .option(\n \"-w, --weights <weights>\",\n 'Weights for weighted mode: blame,commit (e.g., \"0.5,0.5\")',\n )\n .option(\"--team\", \"Compare all contributors\", false)\n .option(\n \"--team-coverage\",\n \"Show team coverage map (bus factor analysis)\",\n false,\n )\n .option(\"--hotspot [mode]\", \"Hotspot analysis: personal (default) or team\")\n .option(\n \"--window <days>\",\n \"Time window for hotspot analysis in days (default: 90)\",\n )\n .action(async (rawOptions) => {\n try {\n const repoPath = process.cwd();\n const options = parseOptions(rawOptions, repoPath);\n\n // Route: hotspot analysis\n if (options.hotspot) {\n const result = await computeHotspots(options);\n if (options.html) {\n await generateAndOpenHotspotHTML(result, repoPath);\n } else {\n renderHotspotTerminal(result);\n }\n return;\n }\n\n // Route: team coverage\n if (options.teamCoverage) {\n const result = await computeTeamCoverage(options);\n if (options.html) {\n await generateAndOpenCoverageHTML(result, repoPath);\n } else {\n renderCoverageTerminal(result);\n }\n return;\n }\n\n // Route: multi-user comparison\n const isMultiUser =\n options.team ||\n (Array.isArray(options.user) && options.user.length > 1);\n if (isMultiUser) {\n const result = await computeMultiUser(options);\n if (options.html) {\n await generateAndOpenMultiUserHTML(result, repoPath);\n } else {\n renderMultiUserTerminal(result);\n }\n return;\n }\n\n // Route: single user (existing flow)\n const result = await computeFamiliarity(options);\n if (options.html) {\n await generateAndOpenHTML(result, repoPath);\n } else {\n renderTerminal(result);\n }\n } catch (error: any) {\n console.error(`Error: ${error.message}`);\n process.exit(1);\n }\n });\n\n return program;\n}\n","export type ScoringMode = \"binary\" | \"authorship\" | \"weighted\";\nexport type ExpirationPolicyType = \"never\" | \"time\" | \"change\" | \"combined\";\n\nexport interface ExpirationConfig {\n policy: ExpirationPolicyType;\n duration?: number; // days\n threshold?: number; // 0-1 (e.g., 0.5 for 50%)\n}\n\nexport interface WeightConfig {\n blame: number; // default 0.5\n commit: number; // default 0.5\n}\n\nexport type HotspotMode = \"personal\" | \"team\";\nexport type HotspotRiskLevel = \"critical\" | \"high\" | \"medium\" | \"low\";\n\nexport interface CliOptions {\n mode: ScoringMode;\n user?: string | string[];\n expiration: ExpirationConfig;\n html: boolean;\n weights: WeightConfig;\n repoPath: string;\n team?: boolean;\n teamCoverage?: boolean;\n hotspot?: HotspotMode;\n window?: number; // days for hotspot analysis\n}\n\nexport interface UserIdentity {\n name: string;\n email: string;\n}\n\nexport interface FileScore {\n type: \"file\";\n path: string;\n lines: number;\n score: number;\n isWritten?: boolean;\n blameScore?: number;\n commitScore?: number;\n isExpired?: boolean;\n lastTouchDate?: Date;\n}\n\nexport interface FolderScore {\n type: \"folder\";\n path: string;\n lines: number;\n score: number;\n fileCount: number;\n readCount?: number;\n children: TreeNode[];\n}\n\nexport type TreeNode = FileScore | FolderScore;\n\nexport interface CommitInfo {\n hash: string;\n date: Date;\n addedLines: number;\n deletedLines: number;\n fileSizeAtCommit: number;\n}\n\nexport const DEFAULT_WEIGHTS: WeightConfig = {\n blame: 0.5,\n commit: 0.5,\n};\n\nexport const DEFAULT_EXPIRATION: ExpirationConfig = {\n policy: \"never\",\n};\n\n// ── Multi-User Comparison Types ──\n\nexport type RiskLevel = \"safe\" | \"moderate\" | \"risk\";\n\nexport interface UserScore {\n user: UserIdentity;\n score: number;\n isWritten?: boolean;\n}\n\nexport interface MultiUserFileScore {\n type: \"file\";\n path: string;\n lines: number;\n score: number;\n userScores: UserScore[];\n}\n\nexport interface MultiUserFolderScore {\n type: \"folder\";\n path: string;\n lines: number;\n score: number;\n fileCount: number;\n userScores: UserScore[];\n children: MultiUserTreeNode[];\n}\n\nexport type MultiUserTreeNode = MultiUserFileScore | MultiUserFolderScore;\n\nexport interface UserSummary {\n user: UserIdentity;\n writtenCount: number;\n overallScore: number;\n}\n\nexport interface MultiUserResult {\n tree: MultiUserFolderScore;\n repoName: string;\n users: UserIdentity[];\n mode: string;\n totalFiles: number;\n userSummaries: UserSummary[];\n}\n\n// ── Team Coverage Types ──\n\nexport interface CoverageFileScore {\n type: \"file\";\n path: string;\n lines: number;\n contributorCount: number;\n contributors: string[];\n riskLevel: RiskLevel;\n}\n\nexport interface CoverageFolderScore {\n type: \"folder\";\n path: string;\n lines: number;\n fileCount: number;\n avgContributors: number;\n busFactor: number;\n riskLevel: RiskLevel;\n children: CoverageTreeNode[];\n}\n\nexport type CoverageTreeNode = CoverageFileScore | CoverageFolderScore;\n\nexport interface TeamCoverageResult {\n tree: CoverageFolderScore;\n repoName: string;\n totalContributors: number;\n totalFiles: number;\n riskFiles: CoverageFileScore[];\n overallBusFactor: number;\n}\n\n// ── Hotspot Analysis Types ──\n\nexport interface HotspotFileScore {\n path: string;\n lines: number;\n familiarity: number;\n changeFrequency: number;\n lastChanged: Date | null;\n risk: number;\n riskLevel: HotspotRiskLevel;\n}\n\nexport interface HotspotResult {\n files: HotspotFileScore[];\n repoName: string;\n userName?: string;\n hotspotMode: HotspotMode;\n timeWindow: number;\n summary: {\n critical: number;\n high: number;\n medium: number;\n low: number;\n };\n}\n","import type {\n CliOptions,\n ScoringMode,\n WeightConfig,\n HotspotMode,\n} from \"../core/types.js\";\nimport { DEFAULT_WEIGHTS, DEFAULT_EXPIRATION } from \"../core/types.js\";\nimport { parseExpirationConfig } from \"../scoring/expiration.js\";\n\nexport interface RawCliOptions {\n mode?: string;\n user?: string[];\n expiration?: string;\n html?: boolean;\n weights?: string;\n team?: boolean;\n teamCoverage?: boolean;\n hotspot?: string;\n window?: string;\n}\n\nexport function parseOptions(raw: RawCliOptions, repoPath: string): CliOptions {\n const mode = validateMode(raw.mode || \"binary\");\n\n let weights = DEFAULT_WEIGHTS;\n if (raw.weights) {\n weights = parseWeights(raw.weights);\n }\n\n const expiration = raw.expiration\n ? parseExpirationConfig(raw.expiration)\n : DEFAULT_EXPIRATION;\n\n // Handle --user: Commander collects multiple -u flags into an array\n let user: string | string[] | undefined;\n if (raw.user && raw.user.length === 1) {\n user = raw.user[0];\n } else if (raw.user && raw.user.length > 1) {\n user = raw.user;\n }\n\n // Parse --hotspot flag: true (no arg) or \"team\" or \"personal\"\n let hotspot: HotspotMode | undefined;\n if (raw.hotspot !== undefined && raw.hotspot !== false) {\n if (raw.hotspot === \"team\") {\n hotspot = \"team\";\n } else {\n hotspot = \"personal\";\n }\n }\n\n // Parse --window flag\n const windowDays = raw.window ? parseInt(raw.window, 10) : undefined;\n\n return {\n mode,\n user,\n expiration,\n html: raw.html || false,\n weights,\n repoPath,\n team: raw.team || false,\n teamCoverage: raw.teamCoverage || false,\n hotspot,\n window: windowDays,\n };\n}\n\nfunction validateMode(mode: string): ScoringMode {\n const valid: ScoringMode[] = [\"binary\", \"authorship\", \"weighted\"];\n if (!valid.includes(mode as ScoringMode)) {\n throw new Error(\n `Invalid mode: \"${mode}\". Valid modes: ${valid.join(\", \")}`,\n );\n }\n return mode as ScoringMode;\n}\n\nfunction parseWeights(s: string): WeightConfig {\n const parts = s.split(\",\").map(Number);\n if (parts.length !== 2 || parts.some(isNaN)) {\n throw new Error(`Invalid weights: \"${s}\". Expected format: \"0.5,0.5\"`);\n }\n const sum = parts[0] + parts[1];\n if (Math.abs(sum - 1) > 0.01) {\n throw new Error(`Weights must sum to 1.0, got ${sum}`);\n }\n return { blame: parts[0], commit: parts[1] };\n}\n","import chalk from \"chalk\";\nimport type { FamiliarityResult } from \"../../core/familiarity.js\";\nimport type { FolderScore, FileScore, TreeNode } from \"../../core/types.js\";\n\nconst BAR_WIDTH = 10;\nconst FILLED_CHAR = \"\\u2588\"; // █\nconst EMPTY_CHAR = \"\\u2591\"; // ░\n\nfunction makeBar(score: number): string {\n const filled = Math.round(score * BAR_WIDTH);\n const empty = BAR_WIDTH - filled;\n const bar = FILLED_CHAR.repeat(filled) + EMPTY_CHAR.repeat(empty);\n\n if (score >= 0.8) return chalk.green(bar);\n if (score >= 0.5) return chalk.yellow(bar);\n if (score > 0) return chalk.red(bar);\n return chalk.gray(bar);\n}\n\nfunction formatPercent(score: number): string {\n return `${Math.round(score * 100)}%`;\n}\n\nfunction getModeLabel(mode: string): string {\n switch (mode) {\n case \"binary\":\n return \"Binary mode\";\n case \"authorship\":\n return \"Authorship mode\";\n case \"weighted\":\n return \"Weighted mode\";\n default:\n return mode;\n }\n}\n\nfunction renderFolder(\n node: FolderScore,\n indent: number,\n mode: string,\n maxDepth: number,\n): string[] {\n const lines: string[] = [];\n const prefix = \" \".repeat(indent);\n\n // Sort children: folders first, then files, by name\n const sorted = [...node.children].sort((a, b) => {\n if (a.type !== b.type) return a.type === \"folder\" ? -1 : 1;\n return a.path.localeCompare(b.path);\n });\n\n for (const child of sorted) {\n if (child.type === \"folder\") {\n const folder = child as FolderScore;\n const name = folder.path.split(\"/\").pop() + \"/\";\n const bar = makeBar(folder.score);\n const pct = formatPercent(folder.score);\n\n if (mode === \"binary\") {\n const readCount = folder.readCount || 0;\n lines.push(\n `${prefix}${chalk.bold(name.padEnd(16))} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`,\n );\n } else {\n lines.push(\n `${prefix}${chalk.bold(name.padEnd(16))} ${bar} ${pct.padStart(4)}`,\n );\n }\n\n // Recurse if within depth limit\n if (indent < maxDepth) {\n lines.push(...renderFolder(folder, indent + 1, mode, maxDepth));\n }\n }\n }\n\n return lines;\n}\n\nexport function renderTerminal(result: FamiliarityResult): void {\n const { tree, repoName, mode } = result;\n\n console.log(\"\");\n console.log(\n chalk.bold(`GitFamiliar \\u2014 ${repoName} (${getModeLabel(mode)})`),\n );\n console.log(\"\");\n\n if (mode === \"binary\") {\n const readCount = tree.readCount || 0;\n const pct = formatPercent(tree.score);\n console.log(`Overall: ${readCount}/${tree.fileCount} files (${pct})`);\n } else {\n const pct = formatPercent(tree.score);\n console.log(`Overall: ${pct}`);\n }\n\n console.log(\"\");\n\n const folderLines = renderFolder(tree, 1, mode, 2);\n for (const line of folderLines) {\n console.log(line);\n }\n\n console.log(\"\");\n\n if (mode === \"binary\") {\n const { writtenCount } = result;\n console.log(`Written: ${writtenCount} files`);\n console.log(\"\");\n }\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { FamiliarityResult } from \"../../core/familiarity.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateTreemapHTML(result: FamiliarityResult): string {\n const dataJson = JSON.stringify(result.tree);\n const mode = result.mode;\n const repoName = result.repoName;\n\n 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>GitFamiliar \\u2014 ${repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n\n #treemap { width: 100%; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 300px;\n }\n #legend {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n }\n #legend .gradient-bar {\n width: 120px;\n height: 12px;\n background: linear-gradient(to right, #e94560, #f5a623, #27ae60);\n border-radius: 3px;\n margin: 4px 0;\n }\n #legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${repoName}</h1>\n <div class=\"info\">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>\n</div>\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n\n<div id=\"treemap\"></div>\n<div id=\"tooltip\"></div>\n<div id=\"legend\">\n <div>Familiarity</div>\n <div class=\"gradient-bar\"></div>\n <div class=\"labels\"><span>0%</span><span>50%</span><span>100%</span></div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst rawData = ${dataJson};\nconst mode = \"${mode}\";\nlet currentPath = '';\n\nfunction scoreColor(score) {\n if (score <= 0) return '#e94560';\n if (score >= 1) return '#27ae60';\n if (score < 0.5) {\n const t = score / 0.5;\n return d3.interpolateRgb('#e94560', '#f5a623')(t);\n }\n const t = (score - 0.5) / 0.5;\n return d3.interpolateRgb('#f5a623', '#27ae60')(t);\n}\n\nfunction getNodeScore(node) {\n return node.score;\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction totalLines(node) {\n if (node.type === 'file') return Math.max(1, node.lines);\n if (!node.children) return 1;\n let sum = 0;\n for (const c of node.children) sum += totalLines(c);\n return Math.max(1, sum);\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction render() {\n const container = document.getElementById('treemap');\n container.innerHTML = '';\n\n const headerH = document.getElementById('header').offsetHeight;\n const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;\n const width = window.innerWidth;\n const height = window.innerHeight - headerH - breadcrumbH;\n\n const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;\n if (!targetNode) return;\n\n const children = targetNode.children || [];\n if (children.length === 0) return;\n\n // Build full nested hierarchy from the current target\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap()\n .size([width, height])\n .padding(2)\n .paddingTop(20)\n .round(true)(root);\n\n const svg = d3.select('#treemap')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const tooltip = document.getElementById('tooltip');\n\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g')\n .data(nodes)\n .join('g')\n .attr('transform', d => \\`translate(\\${d.x0},\\${d.y0})\\`);\n\n // Rect\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n return scoreColor(getNodeScore(d.data.data));\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e')\n .attr('stroke-width', d => d.children ? 1 : 0.5)\n .attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') {\n event.stopPropagation();\n zoomTo(d.data.data.path);\n }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n showTooltip(d.data.data, event);\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n tooltip.style.display = 'none';\n });\n\n // Labels\n groups.append('text')\n .attr('x', 4)\n .attr('y', 14)\n .attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => {\n const w = d.x1 - d.x0;\n const h = d.y1 - d.y0;\n const name = d.data.name || '';\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n });\n}\n\nfunction showTooltip(data, event) {\n const tooltip = document.getElementById('tooltip');\n const name = data.path || '';\n const score = getNodeScore(data);\n let html = '<strong>' + name + '</strong>';\n html += '<br>Score: ' + Math.round(score * 100) + '%';\n html += '<br>Lines: ' + data.lines.toLocaleString();\n if (data.type === 'folder') {\n html += '<br>Files: ' + data.fileCount;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n }\n if (data.blameScore !== undefined) {\n html += '<br>Blame: ' + Math.round(data.blameScore * 100) + '%';\n }\n if (data.commitScore !== undefined) {\n html += '<br>Commit: ' + Math.round(data.commitScore * 100) + '%';\n }\n\n if (data.isExpired) {\n html += '<br><span style=\"color:#e94560\">Expired</span>';\n }\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction zoomTo(path) {\n currentPath = path;\n updateBreadcrumb();\n render();\n}\n\nfunction updateBreadcrumb() {\n const el = document.getElementById('breadcrumb');\n const parts = currentPath ? currentPath.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += \\`<span class=\"sep\">/</span><span onclick=\"zoomTo('\\${p}')\">\\${part}</span>\\`;\n }\n el.innerHTML = html;\n}\n\nwindow.addEventListener('resize', render);\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenHTML(\n result: FamiliarityResult,\n repoPath: string,\n): Promise<void> {\n const html = generateTreemapHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-report.html\");\n\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Report generated: ${outputPath}`);\n\n await openBrowser(outputPath);\n}\n","export async function openBrowser(filePath: string): Promise<void> {\n try {\n const open = await import('open');\n await open.default(filePath);\n } catch {\n console.log(`Could not open browser automatically. Open this file manually:`);\n console.log(` ${filePath}`);\n }\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity } from \"../core/types.js\";\n\nconst COMMIT_SEP = \"GITFAMILIAR_SEP\";\n\n/**\n * Get all unique contributors from git history.\n * Returns deduplicated list of UserIdentity, sorted by commit count descending.\n */\nexport async function getAllContributors(\n gitClient: GitClient,\n minCommits: number = 1,\n): Promise<UserIdentity[]> {\n const output = await gitClient.getLog([\n \"--all\",\n `--format=%aN|%aE`,\n ]);\n\n const counts = new Map<string, { name: string; email: string; count: number }>();\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.includes(\"|\")) continue;\n const [name, email] = line.split(\"|\", 2);\n if (!name || !email) continue;\n\n const key = email.toLowerCase();\n const existing = counts.get(key);\n if (existing) {\n existing.count++;\n } else {\n counts.set(key, { name: name.trim(), email: email.trim(), count: 1 });\n }\n }\n\n return Array.from(counts.values())\n .filter((c) => c.count >= minCommits)\n .sort((a, b) => b.count - a.count)\n .map((c) => ({ name: c.name, email: c.email }));\n}\n\n/**\n * Bulk get file → contributors mapping from a single git log call.\n * Much faster than per-file git log queries.\n */\nexport async function bulkGetFileContributors(\n gitClient: GitClient,\n trackedFiles: Set<string>,\n): Promise<Map<string, Set<string>>> {\n const output = await gitClient.getLog([\n \"--all\",\n \"--name-only\",\n `--format=${COMMIT_SEP}%aN|%aE`,\n ]);\n\n const result = new Map<string, Set<string>>();\n\n let currentAuthor = \"\";\n for (const line of output.split(\"\\n\")) {\n if (line.startsWith(COMMIT_SEP)) {\n const parts = line.slice(COMMIT_SEP.length).split(\"|\", 2);\n currentAuthor = parts[0]?.trim() || \"\";\n continue;\n }\n\n const filePath = line.trim();\n if (!filePath || !currentAuthor) continue;\n if (!trackedFiles.has(filePath)) continue;\n\n let contributors = result.get(filePath);\n if (!contributors) {\n contributors = new Set<string>();\n result.set(filePath, contributors);\n }\n contributors.add(currentAuthor);\n }\n\n return result;\n}\n","import type {\n CliOptions,\n CoverageFileScore,\n CoverageFolderScore,\n CoverageTreeNode,\n RiskLevel,\n TeamCoverageResult,\n} from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { createFilter } from \"../filter/ignore.js\";\nimport { buildFileTree, walkFiles } from \"./file-tree.js\";\nimport { bulkGetFileContributors, getAllContributors } from \"../git/contributors.js\";\n\nexport async function computeTeamCoverage(\n options: CliOptions,\n): Promise<TeamCoverageResult> {\n const gitClient = new GitClient(options.repoPath);\n\n if (!(await gitClient.isRepo())) {\n throw new Error(`\"${options.repoPath}\" is not a git repository.`);\n }\n\n const repoRoot = await gitClient.getRepoRoot();\n const repoName = await gitClient.getRepoName();\n const filter = createFilter(repoRoot);\n const tree = await buildFileTree(gitClient, filter);\n\n // Get all tracked files\n const trackedFiles = new Set<string>();\n walkFiles(tree, (f) => trackedFiles.add(f.path));\n\n // Bulk get contributors for all files\n const fileContributors = await bulkGetFileContributors(gitClient, trackedFiles);\n const allContributors = await getAllContributors(gitClient);\n\n // Build coverage tree\n const coverageTree = buildCoverageTree(tree, fileContributors);\n\n // Identify risk files\n const riskFiles: CoverageFileScore[] = [];\n walkCoverageFiles(coverageTree, (f) => {\n if (f.contributorCount <= 1) {\n riskFiles.push(f);\n }\n });\n riskFiles.sort((a, b) => a.contributorCount - b.contributorCount);\n\n return {\n tree: coverageTree,\n repoName,\n totalContributors: allContributors.length,\n totalFiles: tree.fileCount,\n riskFiles,\n overallBusFactor: calculateBusFactor(fileContributors),\n };\n}\n\nfunction classifyRisk(contributorCount: number): RiskLevel {\n if (contributorCount <= 1) return \"risk\";\n if (contributorCount <= 3) return \"moderate\";\n return \"safe\";\n}\n\nfunction buildCoverageTree(\n node: import(\"./types.js\").FolderScore,\n fileContributors: Map<string, Set<string>>,\n): CoverageFolderScore {\n const children: CoverageTreeNode[] = [];\n\n for (const child of node.children) {\n if (child.type === \"file\") {\n const contributors = fileContributors.get(child.path);\n const names = contributors ? Array.from(contributors) : [];\n children.push({\n type: \"file\",\n path: child.path,\n lines: child.lines,\n contributorCount: names.length,\n contributors: names,\n riskLevel: classifyRisk(names.length),\n });\n } else {\n children.push(buildCoverageTree(child, fileContributors));\n }\n }\n\n // Compute folder aggregates\n const fileScores: CoverageFileScore[] = [];\n walkCoverageFiles({ type: \"folder\", path: \"\", lines: 0, fileCount: 0, avgContributors: 0, busFactor: 0, riskLevel: \"safe\", children }, (f) => {\n fileScores.push(f);\n });\n\n const totalContributors = fileScores.reduce((sum, f) => sum + f.contributorCount, 0);\n const avgContributors = fileScores.length > 0 ? totalContributors / fileScores.length : 0;\n\n // Calculate bus factor for this folder's files\n const folderFileContributors = new Map<string, Set<string>>();\n for (const f of fileScores) {\n folderFileContributors.set(f.path, new Set(f.contributors));\n }\n const busFactor = calculateBusFactor(folderFileContributors);\n\n return {\n type: \"folder\",\n path: node.path,\n lines: node.lines,\n fileCount: node.fileCount,\n avgContributors: Math.round(avgContributors * 10) / 10,\n busFactor,\n riskLevel: classifyRisk(busFactor),\n children,\n };\n}\n\nfunction walkCoverageFiles(\n node: CoverageTreeNode,\n visitor: (file: CoverageFileScore) => void,\n): void {\n if (node.type === \"file\") {\n visitor(node);\n } else {\n for (const child of node.children) {\n walkCoverageFiles(child, visitor);\n }\n }\n}\n\n/**\n * Calculate bus factor using greedy set cover.\n * Bus factor = minimum number of people who cover >50% of files.\n */\nexport function calculateBusFactor(\n fileContributors: Map<string, Set<string>>,\n): number {\n const totalFiles = fileContributors.size;\n if (totalFiles === 0) return 0;\n\n const target = Math.ceil(totalFiles * 0.5);\n\n // Count files per contributor\n const contributorFiles = new Map<string, Set<string>>();\n for (const [file, contributors] of fileContributors) {\n for (const contributor of contributors) {\n let files = contributorFiles.get(contributor);\n if (!files) {\n files = new Set<string>();\n contributorFiles.set(contributor, files);\n }\n files.add(file);\n }\n }\n\n // Greedy: pick contributor covering most uncovered files\n const coveredFiles = new Set<string>();\n let count = 0;\n\n while (coveredFiles.size < target && contributorFiles.size > 0) {\n let bestContributor = \"\";\n let bestNewFiles = 0;\n\n for (const [contributor, files] of contributorFiles) {\n let newFiles = 0;\n for (const file of files) {\n if (!coveredFiles.has(file)) newFiles++;\n }\n if (newFiles > bestNewFiles) {\n bestNewFiles = newFiles;\n bestContributor = contributor;\n }\n }\n\n if (bestNewFiles === 0) break;\n\n const files = contributorFiles.get(bestContributor)!;\n for (const file of files) {\n coveredFiles.add(file);\n }\n contributorFiles.delete(bestContributor);\n count++;\n }\n\n return count;\n}\n","import chalk from \"chalk\";\nimport type {\n TeamCoverageResult,\n CoverageFolderScore,\n CoverageFileScore,\n} from \"../../core/types.js\";\n\nfunction riskBadge(level: string): string {\n switch (level) {\n case \"risk\":\n return chalk.bgRed.white(\" RISK \");\n case \"moderate\":\n return chalk.bgYellow.black(\" MOD \");\n case \"safe\":\n return chalk.bgGreen.black(\" SAFE \");\n default:\n return level;\n }\n}\n\nfunction riskColor(level: string): typeof chalk {\n switch (level) {\n case \"risk\":\n return chalk.red;\n case \"moderate\":\n return chalk.yellow;\n default:\n return chalk.green;\n }\n}\n\nfunction renderFolder(\n node: CoverageFolderScore,\n indent: number,\n maxDepth: number,\n): string[] {\n const lines: string[] = [];\n\n const sorted = [...node.children].sort((a, b) => {\n if (a.type !== b.type) return a.type === \"folder\" ? -1 : 1;\n return a.path.localeCompare(b.path);\n });\n\n for (const child of sorted) {\n if (child.type === \"folder\") {\n const prefix = \" \".repeat(indent);\n const name = (child.path.split(\"/\").pop() || child.path) + \"/\";\n const color = riskColor(child.riskLevel);\n lines.push(\n `${prefix}${chalk.bold(name.padEnd(24))} ${String(child.avgContributors).padStart(4)} avg ${String(child.busFactor).padStart(2)} ${riskBadge(child.riskLevel)}`,\n );\n if (indent < maxDepth) {\n lines.push(...renderFolder(child, indent + 1, maxDepth));\n }\n }\n }\n\n return lines;\n}\n\nexport function renderCoverageTerminal(result: TeamCoverageResult): void {\n console.log(\"\");\n console.log(\n chalk.bold(\n `GitFamiliar \\u2014 Team Coverage (${result.totalFiles} files, ${result.totalContributors} contributors)`,\n ),\n );\n console.log(\"\");\n\n // Overall bus factor\n const bfColor =\n result.overallBusFactor <= 1\n ? chalk.red\n : result.overallBusFactor <= 2\n ? chalk.yellow\n : chalk.green;\n console.log(`Overall Bus Factor: ${bfColor.bold(String(result.overallBusFactor))}`);\n console.log(\"\");\n\n // Risk files\n if (result.riskFiles.length > 0) {\n console.log(chalk.red.bold(`Risk Files (0-1 contributors):`));\n const displayFiles = result.riskFiles.slice(0, 20);\n for (const file of displayFiles) {\n const count = file.contributorCount;\n const names = file.contributors.join(\", \");\n const label =\n count === 0\n ? chalk.red(\"0 people\")\n : chalk.yellow(`1 person (${names})`);\n console.log(` ${file.path.padEnd(40)} ${label}`);\n }\n if (result.riskFiles.length > 20) {\n console.log(\n chalk.gray(` ... and ${result.riskFiles.length - 20} more`),\n );\n }\n console.log(\"\");\n } else {\n console.log(chalk.green(\"No high-risk files found.\"));\n console.log(\"\");\n }\n\n // Folder coverage table\n console.log(chalk.bold(\"Folder Coverage:\"));\n console.log(\n chalk.gray(\n ` ${\"Folder\".padEnd(24)} ${\"Avg Contrib\".padStart(11)} ${\"Bus Factor\".padStart(10)} Risk`,\n ),\n );\n\n const folderLines = renderFolder(result.tree, 1, 2);\n for (const line of folderLines) {\n console.log(line);\n }\n\n console.log(\"\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { TeamCoverageResult } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateCoverageHTML(result: TeamCoverageResult): string {\n const dataJson = JSON.stringify(result.tree);\n const riskFilesJson = JSON.stringify(result.riskFiles);\n\n 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>GitFamiliar \\u2014 Team Coverage \\u2014 ${result.repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n #main { display: flex; height: calc(100vh - 90px); }\n #treemap { flex: 1; }\n #sidebar {\n width: 300px;\n background: #16213e;\n border-left: 1px solid #0f3460;\n overflow-y: auto;\n padding: 16px;\n }\n #sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }\n #sidebar .risk-file {\n padding: 6px 0;\n border-bottom: 1px solid #0f3460;\n font-size: 12px;\n }\n #sidebar .risk-file .path { color: #e0e0e0; word-break: break-all; }\n #sidebar .risk-file .meta { color: #888; margin-top: 2px; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 320px;\n }\n #legend {\n position: absolute;\n bottom: 16px;\n left: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n }\n #legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }\n #legend .swatch { width: 14px; height: 14px; border-radius: 3px; }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 Team Coverage \\u2014 ${result.repoName}</h1>\n <div class=\"info\">${result.totalFiles} files | ${result.totalContributors} contributors | Bus Factor: ${result.overallBusFactor}</div>\n</div>\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n<div id=\"main\">\n <div id=\"treemap\"></div>\n <div id=\"sidebar\">\n <h3>Risk Files (0-1 contributors)</h3>\n <div id=\"risk-list\"></div>\n </div>\n</div>\n<div id=\"tooltip\"></div>\n<div id=\"legend\">\n <div>Contributors</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#e94560\"></div> 0\\u20131 (Risk)</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#f5a623\"></div> 2\\u20133 (Moderate)</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#27ae60\"></div> 4+ (Safe)</div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst rawData = ${dataJson};\nconst riskFiles = ${riskFilesJson};\nlet currentPath = '';\n\nfunction coverageColor(count) {\n if (count <= 0) return '#e94560';\n if (count === 1) return '#d63c57';\n if (count <= 3) return '#f5a623';\n return '#27ae60';\n}\n\nfunction folderColor(riskLevel) {\n switch (riskLevel) {\n case 'risk': return '#e94560';\n case 'moderate': return '#f5a623';\n default: return '#27ae60';\n }\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction render() {\n const container = document.getElementById('treemap');\n container.innerHTML = '';\n\n const headerH = document.getElementById('header').offsetHeight;\n const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;\n const width = container.offsetWidth;\n const height = window.innerHeight - headerH - breadcrumbH;\n\n const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap()\n .size([width, height])\n .padding(2)\n .paddingTop(20)\n .round(true)(root);\n\n const svg = d3.select('#treemap')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const tooltip = document.getElementById('tooltip');\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g')\n .data(nodes)\n .join('g')\n .attr('transform', d => \\`translate(\\${d.x0},\\${d.y0})\\`);\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n if (d.data.data.type === 'file') return coverageColor(d.data.data.contributorCount);\n return folderColor(d.data.data.riskLevel);\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e')\n .attr('stroke-width', d => d.children ? 1 : 0.5)\n .attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') {\n event.stopPropagation();\n zoomTo(d.data.data.path);\n }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n showTooltip(d.data.data, event);\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n tooltip.style.display = 'none';\n });\n\n groups.append('text')\n .attr('x', 4)\n .attr('y', 14)\n .attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => {\n const w = d.x1 - d.x0;\n const h = d.y1 - d.y0;\n const name = d.data.name || '';\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n });\n}\n\nfunction showTooltip(data, event) {\n const tooltip = document.getElementById('tooltip');\n let html = '<strong>' + data.path + '</strong>';\n if (data.type === 'file') {\n html += '<br>Contributors: ' + data.contributorCount;\n if (data.contributors.length > 0) {\n html += '<br>' + data.contributors.slice(0, 8).join(', ');\n if (data.contributors.length > 8) html += ', ...';\n }\n html += '<br>Lines: ' + data.lines.toLocaleString();\n } else {\n html += '<br>Files: ' + data.fileCount;\n html += '<br>Avg Contributors: ' + data.avgContributors;\n html += '<br>Bus Factor: ' + data.busFactor;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n }\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction zoomTo(path) {\n currentPath = path;\n const el = document.getElementById('breadcrumb');\n const parts = path ? path.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += '<span class=\"sep\">/</span><span onclick=\"zoomTo(\\\\'' + p + '\\\\')\">' + part + '</span>';\n }\n el.innerHTML = html;\n render();\n}\n\n// Render risk sidebar\nfunction renderRiskSidebar() {\n const container = document.getElementById('risk-list');\n if (riskFiles.length === 0) {\n container.innerHTML = '<div style=\"color:#888\">No high-risk files found.</div>';\n return;\n }\n let html = '';\n for (const f of riskFiles.slice(0, 50)) {\n const countLabel = f.contributorCount === 0 ? '0 people' : '1 person (' + f.contributors[0] + ')';\n html += '<div class=\"risk-file\"><div class=\"path\">' + f.path + '</div><div class=\"meta\">' + countLabel + '</div></div>';\n }\n if (riskFiles.length > 50) {\n html += '<div style=\"color:#888;padding:8px 0\">... and ' + (riskFiles.length - 50) + ' more</div>';\n }\n container.innerHTML = html;\n}\n\nwindow.addEventListener('resize', render);\nrenderRiskSidebar();\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenCoverageHTML(\n result: TeamCoverageResult,\n repoPath: string,\n): Promise<void> {\n const html = generateCoverageHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-coverage.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Coverage report generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import type {\n CliOptions,\n MultiUserResult,\n MultiUserFolderScore,\n MultiUserFileScore,\n MultiUserTreeNode,\n UserScore,\n UserSummary,\n UserIdentity,\n FolderScore,\n FileScore,\n TreeNode,\n} from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { computeFamiliarity, type FamiliarityResult } from \"./familiarity.js\";\nimport { getAllContributors } from \"../git/contributors.js\";\nimport { resolveUser } from \"../git/identity.js\";\nimport { processBatch } from \"../utils/batch.js\";\nimport { walkFiles } from \"./file-tree.js\";\n\nexport async function computeMultiUser(\n options: CliOptions,\n): Promise<MultiUserResult> {\n const gitClient = new GitClient(options.repoPath);\n\n if (!(await gitClient.isRepo())) {\n throw new Error(`\"${options.repoPath}\" is not a git repository.`);\n }\n\n const repoName = await gitClient.getRepoName();\n\n // Determine which users to compare\n let userNames: string[];\n if (options.team) {\n const contributors = await getAllContributors(gitClient, 3);\n userNames = contributors.map((c) => c.name);\n if (userNames.length === 0) {\n throw new Error(\"No contributors found with 3+ commits.\");\n }\n console.log(`Found ${userNames.length} contributors with 3+ commits`);\n } else if (Array.isArray(options.user)) {\n userNames = options.user;\n } else if (options.user) {\n userNames = [options.user];\n } else {\n // Default: current user only\n const user = await resolveUser(gitClient);\n userNames = [user.name || user.email];\n }\n\n // Run scoring for each user (batched, 3 at a time)\n const results: Array<{ userName: string; result: FamiliarityResult }> = [];\n\n await processBatch(\n userNames,\n async (userName) => {\n const userOptions: CliOptions = {\n ...options,\n user: userName,\n team: false,\n teamCoverage: false,\n };\n const result = await computeFamiliarity(userOptions);\n results.push({ userName, result });\n },\n 3,\n );\n\n // Resolve user identities\n const users: UserIdentity[] = results.map((r) => ({\n name: r.result.userName,\n email: \"\",\n }));\n\n // Merge results into multi-user tree\n const tree = mergeResults(results);\n\n // Compute user summaries\n const userSummaries: UserSummary[] = results.map((r) => ({\n user: { name: r.result.userName, email: \"\" },\n writtenCount: r.result.writtenCount,\n overallScore: r.result.tree.score,\n }));\n\n return {\n tree,\n repoName,\n users,\n mode: options.mode,\n totalFiles: results[0]?.result.totalFiles || 0,\n userSummaries,\n };\n}\n\nfunction mergeResults(\n results: Array<{ userName: string; result: FamiliarityResult }>,\n): MultiUserFolderScore {\n if (results.length === 0) {\n return {\n type: \"folder\",\n path: \"\",\n lines: 0,\n score: 0,\n fileCount: 0,\n userScores: [],\n children: [],\n };\n }\n\n // Use first result as the structural template\n const baseTree = results[0].result.tree;\n\n // Build a map from file path → per-user scores\n const fileScoresMap = new Map<string, UserScore[]>();\n for (const { result } of results) {\n const userName = result.userName;\n walkFiles(result.tree, (file: FileScore) => {\n let scores = fileScoresMap.get(file.path);\n if (!scores) {\n scores = [];\n fileScoresMap.set(file.path, scores);\n }\n scores.push({\n user: { name: userName, email: \"\" },\n score: file.score,\n isWritten: file.isWritten,\n });\n });\n }\n\n return convertFolder(baseTree, fileScoresMap, results);\n}\n\nfunction convertFolder(\n node: FolderScore,\n fileScoresMap: Map<string, UserScore[]>,\n results: Array<{ userName: string; result: FamiliarityResult }>,\n): MultiUserFolderScore {\n const children: MultiUserTreeNode[] = [];\n\n for (const child of node.children) {\n if (child.type === \"file\") {\n const userScores = fileScoresMap.get(child.path) || [];\n const avgScore =\n userScores.length > 0\n ? userScores.reduce((sum, s) => sum + s.score, 0) / userScores.length\n : 0;\n children.push({\n type: \"file\",\n path: child.path,\n lines: child.lines,\n score: avgScore,\n userScores,\n });\n } else {\n children.push(convertFolder(child, fileScoresMap, results));\n }\n }\n\n // Compute folder-level user scores\n const userScores: UserScore[] = results.map(({ result }) => {\n // Find this folder in the user's result tree\n const folderNode = findFolderInTree(result.tree, node.path);\n return {\n user: { name: result.userName, email: \"\" },\n score: folderNode?.score || 0,\n };\n });\n\n const avgScore =\n userScores.length > 0\n ? userScores.reduce((sum, s) => sum + s.score, 0) / userScores.length\n : 0;\n\n return {\n type: \"folder\",\n path: node.path,\n lines: node.lines,\n score: avgScore,\n fileCount: node.fileCount,\n userScores,\n children,\n };\n}\n\nfunction findFolderInTree(\n node: TreeNode,\n targetPath: string,\n): FolderScore | null {\n if (node.type === \"folder\") {\n if (node.path === targetPath) return node;\n for (const child of node.children) {\n const found = findFolderInTree(child, targetPath);\n if (found) return found;\n }\n }\n return null;\n}\n","import chalk from \"chalk\";\nimport type {\n MultiUserResult,\n MultiUserFolderScore,\n UserScore,\n} from \"../../core/types.js\";\n\nconst BAR_WIDTH = 20;\nconst FILLED_CHAR = \"\\u2588\";\nconst EMPTY_CHAR = \"\\u2591\";\n\nfunction makeBar(score: number, width: number = BAR_WIDTH): string {\n const filled = Math.round(score * width);\n const empty = width - filled;\n const bar = FILLED_CHAR.repeat(filled) + EMPTY_CHAR.repeat(empty);\n if (score >= 0.8) return chalk.green(bar);\n if (score >= 0.5) return chalk.yellow(bar);\n if (score > 0) return chalk.red(bar);\n return chalk.gray(bar);\n}\n\nfunction formatPercent(score: number): string {\n return `${Math.round(score * 100)}%`;\n}\n\nfunction getModeLabel(mode: string): string {\n switch (mode) {\n case \"binary\":\n return \"Binary mode\";\n case \"authorship\":\n return \"Authorship mode\";\n case \"weighted\":\n return \"Weighted mode\";\n default:\n return mode;\n }\n}\n\nfunction truncateName(name: string, maxLen: number): string {\n if (name.length <= maxLen) return name;\n return name.slice(0, maxLen - 1) + \"\\u2026\";\n}\n\nfunction renderFolder(\n node: MultiUserFolderScore,\n indent: number,\n maxDepth: number,\n nameWidth: number,\n): string[] {\n const lines: string[] = [];\n\n const sorted = [...node.children].sort((a, b) => {\n if (a.type !== b.type) return a.type === \"folder\" ? -1 : 1;\n return a.path.localeCompare(b.path);\n });\n\n for (const child of sorted) {\n if (child.type === \"folder\") {\n const prefix = \" \".repeat(indent);\n const name = (child.path.split(\"/\").pop() || child.path) + \"/\";\n const displayName = truncateName(name, nameWidth).padEnd(nameWidth);\n\n const scores = child.userScores\n .map((s) => formatPercent(s.score).padStart(5))\n .join(\" \");\n\n lines.push(`${prefix}${chalk.bold(displayName)} ${scores}`);\n\n if (indent < maxDepth) {\n lines.push(...renderFolder(child, indent + 1, maxDepth, nameWidth));\n }\n }\n }\n\n return lines;\n}\n\nexport function renderMultiUserTerminal(result: MultiUserResult): void {\n const { tree, repoName, mode, userSummaries, totalFiles } = result;\n\n console.log(\"\");\n console.log(\n chalk.bold(\n `GitFamiliar \\u2014 ${repoName} (${getModeLabel(mode)}, ${userSummaries.length} users)`,\n ),\n );\n console.log(\"\");\n\n // Overall per-user stats\n console.log(chalk.bold(\"Overall:\"));\n for (const summary of userSummaries) {\n const name = truncateName(summary.user.name, 14).padEnd(14);\n const bar = makeBar(summary.overallScore);\n const pct = formatPercent(summary.overallScore);\n\n if (mode === \"binary\") {\n console.log(\n ` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`,\n );\n } else {\n console.log(` ${name} ${bar} ${pct.padStart(4)}`);\n }\n }\n console.log(\"\");\n\n // Folder breakdown header\n const nameWidth = 20;\n const headerNames = userSummaries\n .map((s) => truncateName(s.user.name, 7).padStart(7))\n .join(\" \");\n console.log(chalk.bold(\"Folders:\") + \" \".repeat(nameWidth - 4) + headerNames);\n\n const folderLines = renderFolder(tree, 1, 2, nameWidth);\n for (const line of folderLines) {\n console.log(line);\n }\n\n console.log(\"\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { MultiUserResult } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateMultiUserHTML(result: MultiUserResult): string {\n const dataJson = JSON.stringify(result.tree);\n const summariesJson = JSON.stringify(result.userSummaries);\n const usersJson = JSON.stringify(result.users.map((u) => u.name));\n\n 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>GitFamiliar \\u2014 ${result.repoName} \\u2014 Multi-User</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .controls { display: flex; align-items: center; gap: 12px; }\n #header select {\n padding: 4px 12px;\n border: 1px solid #0f3460;\n background: #1a1a2e;\n color: #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n #treemap { width: 100%; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 350px;\n }\n #legend {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n }\n #legend .gradient-bar {\n width: 120px; height: 12px;\n background: linear-gradient(to right, #e94560, #f5a623, #27ae60);\n border-radius: 3px; margin: 4px 0;\n }\n #legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${result.repoName}</h1>\n <div class=\"controls\">\n <span style=\"color:#888;font-size:13px;\">View as:</span>\n <select id=\"userSelect\" onchange=\"changeUser()\"></select>\n <div class=\"info\">${result.mode} mode | ${result.totalFiles} files</div>\n </div>\n</div>\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n<div id=\"treemap\"></div>\n<div id=\"tooltip\"></div>\n<div id=\"legend\">\n <div>Familiarity</div>\n <div class=\"gradient-bar\"></div>\n <div class=\"labels\"><span>0%</span><span>50%</span><span>100%</span></div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst rawData = ${dataJson};\nconst userNames = ${usersJson};\nconst summaries = ${summariesJson};\nlet currentUser = 0;\nlet currentPath = '';\n\n// Populate user selector\nconst select = document.getElementById('userSelect');\nuserNames.forEach((name, i) => {\n const opt = document.createElement('option');\n opt.value = i;\n const summary = summaries[i];\n opt.textContent = name + ' (' + Math.round(summary.overallScore * 100) + '%)';\n select.appendChild(opt);\n});\n\nfunction changeUser() {\n currentUser = parseInt(select.value);\n render();\n}\n\nfunction scoreColor(score) {\n if (score <= 0) return '#e94560';\n if (score >= 1) return '#27ae60';\n if (score < 0.5) {\n const t = score / 0.5;\n return d3.interpolateRgb('#e94560', '#f5a623')(t);\n }\n const t = (score - 0.5) / 0.5;\n return d3.interpolateRgb('#f5a623', '#27ae60')(t);\n}\n\nfunction getUserScore(node) {\n if (!node.userScores || node.userScores.length === 0) return node.score;\n const s = node.userScores[currentUser];\n return s ? s.score : 0;\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction render() {\n const container = document.getElementById('treemap');\n container.innerHTML = '';\n\n const headerH = document.getElementById('header').offsetHeight;\n const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;\n const width = window.innerWidth;\n const height = window.innerHeight - headerH - breadcrumbH;\n\n const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap()\n .size([width, height])\n .padding(2)\n .paddingTop(20)\n .round(true)(root);\n\n const svg = d3.select('#treemap')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const tooltip = document.getElementById('tooltip');\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g')\n .data(nodes)\n .join('g')\n .attr('transform', d => \\`translate(\\${d.x0},\\${d.y0})\\`);\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n return scoreColor(getUserScore(d.data.data));\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e')\n .attr('stroke-width', d => d.children ? 1 : 0.5)\n .attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') {\n event.stopPropagation();\n zoomTo(d.data.data.path);\n }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n showTooltip(d.data.data, event);\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n tooltip.style.display = 'none';\n });\n\n groups.append('text')\n .attr('x', 4)\n .attr('y', 14)\n .attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => {\n const w = d.x1 - d.x0;\n const h = d.y1 - d.y0;\n const name = d.data.name || '';\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n });\n}\n\nfunction showTooltip(data, event) {\n const tooltip = document.getElementById('tooltip');\n let html = '<strong>' + (data.path || 'root') + '</strong>';\n\n if (data.userScores && data.userScores.length > 0) {\n html += '<table style=\"margin-top:6px;width:100%\">';\n data.userScores.forEach((s, i) => {\n const isCurrent = (i === currentUser);\n const style = isCurrent ? 'font-weight:bold;color:#5eadf7' : '';\n html += '<tr style=\"' + style + '\"><td>' + userNames[i] + '</td><td style=\"text-align:right\">' + Math.round(s.score * 100) + '%</td></tr>';\n });\n html += '</table>';\n }\n\n if (data.type === 'folder') {\n html += '<br>Files: ' + data.fileCount;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n } else {\n html += '<br>Lines: ' + data.lines.toLocaleString();\n }\n\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction zoomTo(path) {\n currentPath = path;\n const el = document.getElementById('breadcrumb');\n const parts = path ? path.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += '<span class=\"sep\">/</span><span onclick=\"zoomTo(\\\\'' + p + '\\\\')\">' + part + '</span>';\n }\n el.innerHTML = html;\n render();\n}\n\nwindow.addEventListener('resize', render);\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenMultiUserHTML(\n result: MultiUserResult,\n repoPath: string,\n): Promise<void> {\n const html = generateMultiUserHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-multiuser.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Multi-user report generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import type { GitClient } from \"./client.js\";\n\nconst COMMIT_SEP = \"GITFAMILIAR_FREQ_SEP\";\n\nexport interface FileChangeFrequency {\n commitCount: number;\n lastChanged: Date | null;\n}\n\n/**\n * Bulk get change frequency for all files in a single git log call.\n * Returns Map of filePath → { commitCount, lastChanged } within the given window.\n */\nexport async function bulkGetChangeFrequency(\n gitClient: GitClient,\n days: number,\n trackedFiles: Set<string>,\n): Promise<Map<string, FileChangeFrequency>> {\n const sinceDate = `${days} days ago`;\n\n const output = await gitClient.getLog([\n \"--all\",\n `--since=${sinceDate}`,\n \"--name-only\",\n `--format=${COMMIT_SEP}%aI`,\n ]);\n\n const result = new Map<string, FileChangeFrequency>();\n\n let currentDate: Date | null = null;\n\n for (const line of output.split(\"\\n\")) {\n if (line.startsWith(COMMIT_SEP)) {\n const dateStr = line.slice(COMMIT_SEP.length).trim();\n currentDate = dateStr ? new Date(dateStr) : null;\n continue;\n }\n\n const filePath = line.trim();\n if (!filePath || !trackedFiles.has(filePath)) continue;\n\n let entry = result.get(filePath);\n if (!entry) {\n entry = { commitCount: 0, lastChanged: null };\n result.set(filePath, entry);\n }\n entry.commitCount++;\n\n if (currentDate && (!entry.lastChanged || currentDate > entry.lastChanged)) {\n entry.lastChanged = currentDate;\n }\n }\n\n return result;\n}\n","import type {\n CliOptions,\n HotspotFileScore,\n HotspotResult,\n HotspotRiskLevel,\n FileScore,\n} from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { createFilter } from \"../filter/ignore.js\";\nimport { buildFileTree, walkFiles } from \"./file-tree.js\";\nimport { computeFamiliarity } from \"./familiarity.js\";\nimport { bulkGetChangeFrequency } from \"../git/change-frequency.js\";\nimport { bulkGetFileContributors, getAllContributors } from \"../git/contributors.js\";\nimport { processBatch } from \"../utils/batch.js\";\nimport { resolveUser } from \"../git/identity.js\";\n\nconst DEFAULT_WINDOW = 90;\n\nexport async function computeHotspots(\n options: CliOptions,\n): Promise<HotspotResult> {\n const gitClient = new GitClient(options.repoPath);\n\n if (!(await gitClient.isRepo())) {\n throw new Error(`\"${options.repoPath}\" is not a git repository.`);\n }\n\n const repoName = await gitClient.getRepoName();\n const repoRoot = await gitClient.getRepoRoot();\n const filter = createFilter(repoRoot);\n const tree = await buildFileTree(gitClient, filter);\n const timeWindow = options.window || DEFAULT_WINDOW;\n const isTeamMode = options.hotspot === \"team\";\n\n // Get all tracked files\n const trackedFiles = new Set<string>();\n walkFiles(tree, (f) => trackedFiles.add(f.path));\n\n // Get change frequency for all files (single git log call)\n const changeFreqMap = await bulkGetChangeFrequency(gitClient, timeWindow, trackedFiles);\n\n // Get familiarity scores\n let familiarityMap: Map<string, number>;\n let userName: string | undefined;\n\n if (isTeamMode) {\n // Team mode: average familiarity across all contributors\n familiarityMap = await computeTeamAvgFamiliarity(gitClient, trackedFiles, options);\n } else {\n // Personal mode: single user's familiarity\n const userFlag = Array.isArray(options.user) ? options.user[0] : options.user;\n const result = await computeFamiliarity({ ...options, team: false, teamCoverage: false });\n userName = result.userName;\n familiarityMap = new Map<string, number>();\n walkFiles(result.tree, (f) => {\n familiarityMap.set(f.path, f.score);\n });\n }\n\n // Find max change frequency for normalization\n let maxFreq = 0;\n for (const entry of changeFreqMap.values()) {\n if (entry.commitCount > maxFreq) maxFreq = entry.commitCount;\n }\n\n // Calculate risk for each file\n const hotspotFiles: HotspotFileScore[] = [];\n\n for (const filePath of trackedFiles) {\n const freq = changeFreqMap.get(filePath);\n const changeFrequency = freq?.commitCount || 0;\n const lastChanged = freq?.lastChanged || null;\n const familiarity = familiarityMap.get(filePath) || 0;\n\n // Normalize frequency to 0-1\n const normalizedFreq = maxFreq > 0 ? changeFrequency / maxFreq : 0;\n\n // Risk = normalizedFrequency × (1 - familiarity)\n const risk = normalizedFreq * (1 - familiarity);\n\n // Find lines from tree\n let lines = 0;\n walkFiles(tree, (f) => {\n if (f.path === filePath) lines = f.lines;\n });\n\n hotspotFiles.push({\n path: filePath,\n lines,\n familiarity,\n changeFrequency,\n lastChanged,\n risk,\n riskLevel: classifyHotspotRisk(risk),\n });\n }\n\n // Sort by risk descending\n hotspotFiles.sort((a, b) => b.risk - a.risk);\n\n // Compute summary\n const summary = { critical: 0, high: 0, medium: 0, low: 0 };\n for (const f of hotspotFiles) {\n summary[f.riskLevel]++;\n }\n\n return {\n files: hotspotFiles,\n repoName,\n userName,\n hotspotMode: isTeamMode ? \"team\" : \"personal\",\n timeWindow,\n summary,\n };\n}\n\nexport function classifyHotspotRisk(risk: number): HotspotRiskLevel {\n if (risk >= 0.6) return \"critical\";\n if (risk >= 0.4) return \"high\";\n if (risk >= 0.2) return \"medium\";\n return \"low\";\n}\n\n/**\n * For team mode: compute average familiarity across all contributors.\n * Uses bulkGetFileContributors (single git log call) to count how many people\n * know each file, then normalizes as: avgFam = contributorCount / totalContributors.\n * This is a lightweight proxy for \"how well-known is this file across the team\".\n */\nasync function computeTeamAvgFamiliarity(\n gitClient: GitClient,\n trackedFiles: Set<string>,\n options: CliOptions,\n): Promise<Map<string, number>> {\n const contributors = await getAllContributors(gitClient, 1);\n const totalContributors = Math.max(1, contributors.length);\n const fileContributors = await bulkGetFileContributors(gitClient, trackedFiles);\n\n const result = new Map<string, number>();\n for (const filePath of trackedFiles) {\n const contribs = fileContributors.get(filePath);\n const count = contribs ? contribs.size : 0;\n // Normalize: what fraction of the team knows this file\n // Cap at 1.0 (e.g., if everyone knows it)\n result.set(filePath, Math.min(1, count / Math.max(1, totalContributors * 0.3)));\n }\n\n return result;\n}\n","import chalk from \"chalk\";\nimport type { HotspotResult, HotspotRiskLevel } from \"../../core/types.js\";\n\nfunction riskBadge(level: HotspotRiskLevel): string {\n switch (level) {\n case \"critical\":\n return chalk.bgRed.white.bold(\" CRIT \");\n case \"high\":\n return chalk.bgRedBright.white(\" HIGH \");\n case \"medium\":\n return chalk.bgYellow.black(\" MED \");\n case \"low\":\n return chalk.bgGreen.black(\" LOW \");\n }\n}\n\nfunction riskColor(level: HotspotRiskLevel): typeof chalk {\n switch (level) {\n case \"critical\": return chalk.red;\n case \"high\": return chalk.redBright;\n case \"medium\": return chalk.yellow;\n case \"low\": return chalk.green;\n }\n}\n\nexport function renderHotspotTerminal(result: HotspotResult): void {\n const { files, repoName, hotspotMode, timeWindow, summary, userName } = result;\n\n console.log(\"\");\n const modeLabel = hotspotMode === \"team\" ? \"Team Hotspots\" : \"Personal Hotspots\";\n const userLabel = userName ? ` (${userName})` : \"\";\n console.log(\n chalk.bold(`GitFamiliar \\u2014 ${modeLabel}${userLabel} \\u2014 ${repoName}`),\n );\n console.log(chalk.gray(` Time window: last ${timeWindow} days`));\n console.log(\"\");\n\n // Filter to files with actual activity\n const activeFiles = files.filter((f) => f.changeFrequency > 0);\n\n if (activeFiles.length === 0) {\n console.log(chalk.gray(\" No files changed in the time window.\"));\n console.log(\"\");\n return;\n }\n\n // Top hotspots table\n const displayCount = Math.min(30, activeFiles.length);\n const topFiles = activeFiles.slice(0, displayCount);\n\n console.log(\n chalk.gray(\n ` ${\"Rank\".padEnd(5)} ${\"File\".padEnd(42)} ${\"Familiarity\".padStart(11)} ${\"Changes\".padStart(8)} ${\"Risk\".padStart(6)} Level`,\n ),\n );\n console.log(chalk.gray(\" \" + \"\\u2500\".repeat(90)));\n\n for (let i = 0; i < topFiles.length; i++) {\n const f = topFiles[i];\n const rank = String(i + 1).padEnd(5);\n const path = truncate(f.path, 42).padEnd(42);\n const fam = `${Math.round(f.familiarity * 100)}%`.padStart(11);\n const changes = String(f.changeFrequency).padStart(8);\n const risk = f.risk.toFixed(2).padStart(6);\n const color = riskColor(f.riskLevel);\n const badge = riskBadge(f.riskLevel);\n\n console.log(\n ` ${color(rank)}${path} ${fam} ${changes} ${color(risk)} ${badge}`,\n );\n }\n\n if (activeFiles.length > displayCount) {\n console.log(\n chalk.gray(` ... and ${activeFiles.length - displayCount} more files`),\n );\n }\n\n console.log(\"\");\n\n // Summary\n console.log(chalk.bold(\"Summary:\"));\n if (summary.critical > 0) {\n console.log(\n ` ${chalk.red.bold(`\\u{1F534} Critical Risk: ${summary.critical} files`)}`,\n );\n }\n if (summary.high > 0) {\n console.log(\n ` ${chalk.redBright(`\\u{1F7E0} High Risk: ${summary.high} files`)}`,\n );\n }\n if (summary.medium > 0) {\n console.log(\n ` ${chalk.yellow(`\\u{1F7E1} Medium Risk: ${summary.medium} files`)}`,\n );\n }\n console.log(\n ` ${chalk.green(`\\u{1F7E2} Low Risk: ${summary.low} files`)}`,\n );\n\n console.log(\"\");\n if (summary.critical > 0 || summary.high > 0) {\n console.log(\n chalk.gray(\n \" Recommendation: Focus code review and knowledge transfer on critical/high risk files.\",\n ),\n );\n console.log(\"\");\n }\n}\n\nfunction truncate(s: string, maxLen: number): string {\n if (s.length <= maxLen) return s;\n return s.slice(0, maxLen - 1) + \"\\u2026\";\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { HotspotResult } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateHotspotHTML(result: HotspotResult): string {\n // Only include files with activity for the scatter plot\n const activeFiles = result.files.filter((f) => f.changeFrequency > 0);\n const dataJson = JSON.stringify(\n activeFiles.map((f) => ({\n path: f.path,\n lines: f.lines,\n familiarity: f.familiarity,\n changeFrequency: f.changeFrequency,\n risk: f.risk,\n riskLevel: f.riskLevel,\n })),\n );\n\n const modeLabel =\n result.hotspotMode === \"team\" ? \"Team Hotspots\" : \"Personal Hotspots\";\n const userLabel = result.userName ? ` (${result.userName})` : \"\";\n\n 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>GitFamiliar \\u2014 ${modeLabel} \\u2014 ${result.repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #main { display: flex; height: calc(100vh - 60px); }\n #chart { flex: 1; position: relative; }\n #sidebar {\n width: 320px;\n background: #16213e;\n border-left: 1px solid #0f3460;\n overflow-y: auto;\n padding: 16px;\n }\n #sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }\n .hotspot-item {\n padding: 8px 0;\n border-bottom: 1px solid #0f3460;\n font-size: 12px;\n }\n .hotspot-item .path { color: #e0e0e0; word-break: break-all; }\n .hotspot-item .meta { color: #888; margin-top: 2px; }\n .hotspot-item .risk-badge {\n display: inline-block;\n padding: 1px 6px;\n border-radius: 3px;\n font-size: 10px;\n font-weight: bold;\n margin-left: 4px;\n }\n .risk-critical { background: #e94560; color: white; }\n .risk-high { background: #f07040; color: white; }\n .risk-medium { background: #f5a623; color: black; }\n .risk-low { background: #27ae60; color: white; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 350px;\n }\n #zone-labels { position: absolute; pointer-events: none; }\n .zone-label {\n position: absolute;\n font-size: 12px;\n color: rgba(255,255,255,0.15);\n font-weight: bold;\n }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${modeLabel}${userLabel} \\u2014 ${result.repoName}</h1>\n <div class=\"info\">${result.timeWindow}-day window | ${activeFiles.length} active files | Summary: ${result.summary.critical} critical, ${result.summary.high} high</div>\n</div>\n<div id=\"main\">\n <div id=\"chart\">\n <div id=\"zone-labels\"></div>\n </div>\n <div id=\"sidebar\">\n <h3>Top Hotspots</h3>\n <div id=\"hotspot-list\"></div>\n </div>\n</div>\n<div id=\"tooltip\"></div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst data = ${dataJson};\nconst margin = { top: 30, right: 30, bottom: 60, left: 70 };\n\nfunction riskColor(level) {\n switch(level) {\n case 'critical': return '#e94560';\n case 'high': return '#f07040';\n case 'medium': return '#f5a623';\n default: return '#27ae60';\n }\n}\n\nfunction render() {\n const container = document.getElementById('chart');\n const svg = container.querySelector('svg');\n if (svg) svg.remove();\n\n const width = container.offsetWidth;\n const height = container.offsetHeight;\n const innerW = width - margin.left - margin.right;\n const innerH = height - margin.top - margin.bottom;\n\n const maxFreq = d3.max(data, d => d.changeFrequency) || 1;\n\n const x = d3.scaleLinear().domain([0, 1]).range([0, innerW]);\n const y = d3.scaleLinear().domain([0, maxFreq * 1.1]).range([innerH, 0]);\n const r = d3.scaleSqrt()\n .domain([0, d3.max(data, d => d.lines) || 1])\n .range([3, 20]);\n\n const svgEl = d3.select('#chart')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const g = svgEl.append('g')\n .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');\n\n // Danger zone background (top-left quadrant)\n g.append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', x(0.3))\n .attr('height', y(maxFreq * 0.3))\n .attr('fill', 'rgba(233, 69, 96, 0.06)');\n\n // X axis\n g.append('g')\n .attr('transform', 'translate(0,' + innerH + ')')\n .call(d3.axisBottom(x).ticks(5).tickFormat(d => Math.round(d * 100) + '%'))\n .selectAll('text,line,path').attr('stroke', '#555').attr('fill', '#888');\n\n svgEl.append('text')\n .attr('x', margin.left + innerW / 2)\n .attr('y', height - 10)\n .attr('text-anchor', 'middle')\n .attr('fill', '#888')\n .attr('font-size', '13px')\n .text('Familiarity \\\\u2192');\n\n // Y axis\n g.append('g')\n .call(d3.axisLeft(y).ticks(6))\n .selectAll('text,line,path').attr('stroke', '#555').attr('fill', '#888');\n\n svgEl.append('text')\n .attr('transform', 'rotate(-90)')\n .attr('x', -(margin.top + innerH / 2))\n .attr('y', 16)\n .attr('text-anchor', 'middle')\n .attr('fill', '#888')\n .attr('font-size', '13px')\n .text('Change Frequency (commits) \\\\u2192');\n\n // Zone labels\n const labels = document.getElementById('zone-labels');\n labels.innerHTML = '';\n const dangerLabel = document.createElement('div');\n dangerLabel.className = 'zone-label';\n dangerLabel.style.left = (margin.left + 8) + 'px';\n dangerLabel.style.top = (margin.top + 8) + 'px';\n dangerLabel.textContent = 'DANGER ZONE';\n dangerLabel.style.color = 'rgba(233,69,96,0.25)';\n dangerLabel.style.fontSize = '16px';\n labels.appendChild(dangerLabel);\n\n const safeLabel = document.createElement('div');\n safeLabel.className = 'zone-label';\n safeLabel.style.right = (320 + 40) + 'px';\n safeLabel.style.bottom = (margin.bottom + 16) + 'px';\n safeLabel.textContent = 'SAFE ZONE';\n safeLabel.style.color = 'rgba(39,174,96,0.2)';\n safeLabel.style.fontSize = '16px';\n labels.appendChild(safeLabel);\n\n const tooltip = document.getElementById('tooltip');\n\n // Data points\n g.selectAll('circle')\n .data(data)\n .join('circle')\n .attr('cx', d => x(d.familiarity))\n .attr('cy', d => y(d.changeFrequency))\n .attr('r', d => r(d.lines))\n .attr('fill', d => riskColor(d.riskLevel))\n .attr('opacity', 0.7)\n .attr('stroke', 'none')\n .style('cursor', 'pointer')\n .on('mouseover', function(event, d) {\n d3.select(this).attr('opacity', 1).attr('stroke', '#fff').attr('stroke-width', 2);\n tooltip.innerHTML =\n '<strong>' + d.path + '</strong>' +\n '<br>Familiarity: ' + Math.round(d.familiarity * 100) + '%' +\n '<br>Changes: ' + d.changeFrequency + ' commits' +\n '<br>Risk: ' + d.risk.toFixed(2) + ' (' + d.riskLevel + ')' +\n '<br>Lines: ' + d.lines.toLocaleString();\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function() {\n d3.select(this).attr('opacity', 0.7).attr('stroke', 'none');\n tooltip.style.display = 'none';\n });\n}\n\n// Sidebar\nfunction renderSidebar() {\n const container = document.getElementById('hotspot-list');\n const top = data.slice(0, 30);\n if (top.length === 0) {\n container.innerHTML = '<div style=\"color:#888\">No active files in time window.</div>';\n return;\n }\n let html = '';\n for (let i = 0; i < top.length; i++) {\n const f = top[i];\n const badgeClass = 'risk-' + f.riskLevel;\n html += '<div class=\"hotspot-item\">' +\n '<div class=\"path\">' + (i + 1) + '. ' + f.path +\n ' <span class=\"risk-badge ' + badgeClass + '\">' + f.riskLevel.toUpperCase() + '</span></div>' +\n '<div class=\"meta\">Fam: ' + Math.round(f.familiarity * 100) + '% | Changes: ' + f.changeFrequency + ' | Risk: ' + f.risk.toFixed(2) + '</div>' +\n '</div>';\n }\n container.innerHTML = html;\n}\n\nwindow.addEventListener('resize', render);\nrenderSidebar();\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenHotspotHTML(\n result: HotspotResult,\n repoPath: string,\n): Promise<void> {\n const html = generateHotspotHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-hotspot.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Hotspot report generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import { createProgram } from '../src/cli/index.js';\n\nconst program = createProgram();\nprogram.parse();\n"],"mappings":"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","names":["files","chalk","renderFolder","writeFileSync","join","join","writeFileSync","userScores","avgScore","chalk","BAR_WIDTH","FILLED_CHAR","EMPTY_CHAR","makeBar","formatPercent","getModeLabel","renderFolder","writeFileSync","join","join","writeFileSync","COMMIT_SEP","chalk","riskBadge","riskColor","writeFileSync","join","join","writeFileSync","program","result"]}
1
+ {"version":3,"sources":["../../src/cli/index.ts","../../src/core/types.ts","../../src/cli/options.ts","../../src/cli/output/terminal.ts","../../src/cli/output/html.ts","../../src/utils/open-browser.ts","../../src/git/contributors.ts","../../src/core/team-coverage.ts","../../src/cli/output/coverage-terminal.ts","../../src/cli/output/coverage-html.ts","../../src/core/multi-user.ts","../../src/cli/output/multi-user-terminal.ts","../../src/cli/output/multi-user-html.ts","../../src/git/change-frequency.ts","../../src/core/hotspot.ts","../../src/cli/output/hotspot-terminal.ts","../../src/cli/output/hotspot-html.ts","../../src/core/unified.ts","../../src/cli/output/unified-html.ts","../../bin/gitfamiliar.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { parseOptions } from \"./options.js\";\nimport { computeFamiliarity } from \"../core/familiarity.js\";\nimport { renderTerminal } from \"./output/terminal.js\";\nimport { generateAndOpenHTML } from \"./output/html.js\";\nimport { computeTeamCoverage } from \"../core/team-coverage.js\";\nimport { renderCoverageTerminal } from \"./output/coverage-terminal.js\";\nimport { generateAndOpenCoverageHTML } from \"./output/coverage-html.js\";\nimport { computeMultiUser } from \"../core/multi-user.js\";\nimport { renderMultiUserTerminal } from \"./output/multi-user-terminal.js\";\nimport { generateAndOpenMultiUserHTML } from \"./output/multi-user-html.js\";\nimport { computeHotspots } from \"../core/hotspot.js\";\nimport { renderHotspotTerminal } from \"./output/hotspot-terminal.js\";\nimport { generateAndOpenHotspotHTML } from \"./output/hotspot-html.js\";\nimport { computeUnified } from \"../core/unified.js\";\nimport { generateAndOpenUnifiedHTML } from \"./output/unified-html.js\";\n\nfunction collect(value: string, previous: string[]): string[] {\n return previous.concat([value]);\n}\n\nexport function createProgram(): Command {\n const program = new Command();\n\n program\n .name(\"gitfamiliar\")\n .description(\"Visualize your code familiarity from Git history\")\n .version(\"0.1.1\")\n .option(\n \"-m, --mode <mode>\",\n \"Scoring mode: binary, authorship, weighted\",\n \"binary\",\n )\n .option(\n \"-u, --user <user>\",\n \"Git user name or email (repeatable for comparison)\",\n collect,\n [],\n )\n .option(\n \"-e, --expiration <policy>\",\n \"Expiration policy: never, time:180d, change:50%, combined:365d:50%\",\n \"never\",\n )\n .option(\"--html\", \"Generate HTML treemap report\", false)\n .option(\n \"-w, --weights <weights>\",\n 'Weights for weighted mode: blame,commit (e.g., \"0.5,0.5\")',\n )\n .option(\"--team\", \"Compare all contributors\", false)\n .option(\n \"--team-coverage\",\n \"Show team coverage map (bus factor analysis)\",\n false,\n )\n .option(\"--hotspot [mode]\", \"Hotspot analysis: personal (default) or team\")\n .option(\n \"--window <days>\",\n \"Time window for hotspot analysis in days (default: 90)\",\n )\n .action(async (rawOptions) => {\n try {\n const repoPath = process.cwd();\n const options = parseOptions(rawOptions, repoPath);\n\n // Route: unified HTML dashboard (--html without specific feature flags)\n const isMultiUserCheck =\n options.team ||\n (Array.isArray(options.user) && options.user.length > 1);\n if (\n options.html &&\n !options.hotspot &&\n !options.teamCoverage &&\n !isMultiUserCheck\n ) {\n const data = await computeUnified(options);\n await generateAndOpenUnifiedHTML(data, repoPath);\n return;\n }\n\n // Route: hotspot analysis\n if (options.hotspot) {\n const result = await computeHotspots(options);\n if (options.html) {\n await generateAndOpenHotspotHTML(result, repoPath);\n } else {\n renderHotspotTerminal(result);\n }\n return;\n }\n\n // Route: team coverage\n if (options.teamCoverage) {\n const result = await computeTeamCoverage(options);\n if (options.html) {\n await generateAndOpenCoverageHTML(result, repoPath);\n } else {\n renderCoverageTerminal(result);\n }\n return;\n }\n\n // Route: multi-user comparison\n const isMultiUser =\n options.team ||\n (Array.isArray(options.user) && options.user.length > 1);\n if (isMultiUser) {\n const result = await computeMultiUser(options);\n if (options.html) {\n await generateAndOpenMultiUserHTML(result, repoPath);\n } else {\n renderMultiUserTerminal(result);\n }\n return;\n }\n\n // Route: single user (existing flow)\n const result = await computeFamiliarity(options);\n if (options.html) {\n await generateAndOpenHTML(result, repoPath);\n } else {\n renderTerminal(result);\n }\n } catch (error: any) {\n console.error(`Error: ${error.message}`);\n process.exit(1);\n }\n });\n\n return program;\n}\n","export type ScoringMode = \"binary\" | \"authorship\" | \"weighted\";\nexport type ExpirationPolicyType = \"never\" | \"time\" | \"change\" | \"combined\";\n\nexport interface ExpirationConfig {\n policy: ExpirationPolicyType;\n duration?: number; // days\n threshold?: number; // 0-1 (e.g., 0.5 for 50%)\n}\n\nexport interface WeightConfig {\n blame: number; // default 0.5\n commit: number; // default 0.5\n}\n\nexport type HotspotMode = \"personal\" | \"team\";\nexport type HotspotRiskLevel = \"critical\" | \"high\" | \"medium\" | \"low\";\n\nexport interface CliOptions {\n mode: ScoringMode;\n user?: string | string[];\n expiration: ExpirationConfig;\n html: boolean;\n weights: WeightConfig;\n repoPath: string;\n team?: boolean;\n teamCoverage?: boolean;\n hotspot?: HotspotMode;\n window?: number; // days for hotspot analysis\n}\n\nexport interface UserIdentity {\n name: string;\n email: string;\n}\n\nexport interface FileScore {\n type: \"file\";\n path: string;\n lines: number;\n score: number;\n isWritten?: boolean;\n blameScore?: number;\n commitScore?: number;\n isExpired?: boolean;\n lastTouchDate?: Date;\n}\n\nexport interface FolderScore {\n type: \"folder\";\n path: string;\n lines: number;\n score: number;\n fileCount: number;\n readCount?: number;\n children: TreeNode[];\n}\n\nexport type TreeNode = FileScore | FolderScore;\n\nexport interface CommitInfo {\n hash: string;\n date: Date;\n addedLines: number;\n deletedLines: number;\n fileSizeAtCommit: number;\n}\n\nexport const DEFAULT_WEIGHTS: WeightConfig = {\n blame: 0.5,\n commit: 0.5,\n};\n\nexport const DEFAULT_EXPIRATION: ExpirationConfig = {\n policy: \"never\",\n};\n\n// ── Multi-User Comparison Types ──\n\nexport type RiskLevel = \"safe\" | \"moderate\" | \"risk\";\n\nexport interface UserScore {\n user: UserIdentity;\n score: number;\n isWritten?: boolean;\n}\n\nexport interface MultiUserFileScore {\n type: \"file\";\n path: string;\n lines: number;\n score: number;\n userScores: UserScore[];\n}\n\nexport interface MultiUserFolderScore {\n type: \"folder\";\n path: string;\n lines: number;\n score: number;\n fileCount: number;\n userScores: UserScore[];\n children: MultiUserTreeNode[];\n}\n\nexport type MultiUserTreeNode = MultiUserFileScore | MultiUserFolderScore;\n\nexport interface UserSummary {\n user: UserIdentity;\n writtenCount: number;\n overallScore: number;\n}\n\nexport interface MultiUserResult {\n tree: MultiUserFolderScore;\n repoName: string;\n users: UserIdentity[];\n mode: string;\n totalFiles: number;\n userSummaries: UserSummary[];\n}\n\n// ── Team Coverage Types ──\n\nexport interface CoverageFileScore {\n type: \"file\";\n path: string;\n lines: number;\n contributorCount: number;\n contributors: string[];\n riskLevel: RiskLevel;\n}\n\nexport interface CoverageFolderScore {\n type: \"folder\";\n path: string;\n lines: number;\n fileCount: number;\n avgContributors: number;\n busFactor: number;\n riskLevel: RiskLevel;\n children: CoverageTreeNode[];\n}\n\nexport type CoverageTreeNode = CoverageFileScore | CoverageFolderScore;\n\nexport interface TeamCoverageResult {\n tree: CoverageFolderScore;\n repoName: string;\n totalContributors: number;\n totalFiles: number;\n riskFiles: CoverageFileScore[];\n overallBusFactor: number;\n}\n\n// ── Unified Dashboard Types ──\n\nexport interface UnifiedData {\n repoName: string;\n userName: string;\n scoring: {\n binary: FamiliarityResult;\n authorship: FamiliarityResult;\n weighted: FamiliarityResult;\n };\n coverage: TeamCoverageResult;\n hotspot: HotspotResult;\n multiUser: MultiUserResult;\n}\n\n// FamiliarityResult is defined in familiarity.ts but we need a forward reference\n// for UnifiedData. The actual type is re-exported from familiarity.ts.\nexport interface FamiliarityResult {\n tree: FolderScore;\n repoName: string;\n userName: string;\n mode: string;\n writtenCount: number;\n totalFiles: number;\n}\n\n// ── Hotspot Analysis Types ──\n\nexport interface HotspotFileScore {\n path: string;\n lines: number;\n familiarity: number;\n changeFrequency: number;\n lastChanged: Date | null;\n risk: number;\n riskLevel: HotspotRiskLevel;\n}\n\nexport interface HotspotResult {\n files: HotspotFileScore[];\n repoName: string;\n userName?: string;\n hotspotMode: HotspotMode;\n timeWindow: number;\n summary: {\n critical: number;\n high: number;\n medium: number;\n low: number;\n };\n}\n","import type {\n CliOptions,\n ScoringMode,\n WeightConfig,\n HotspotMode,\n} from \"../core/types.js\";\nimport { DEFAULT_WEIGHTS, DEFAULT_EXPIRATION } from \"../core/types.js\";\nimport { parseExpirationConfig } from \"../scoring/expiration.js\";\n\nexport interface RawCliOptions {\n mode?: string;\n user?: string[];\n expiration?: string;\n html?: boolean;\n weights?: string;\n team?: boolean;\n teamCoverage?: boolean;\n hotspot?: string;\n window?: string;\n}\n\nexport function parseOptions(raw: RawCliOptions, repoPath: string): CliOptions {\n const mode = validateMode(raw.mode || \"binary\");\n\n let weights = DEFAULT_WEIGHTS;\n if (raw.weights) {\n weights = parseWeights(raw.weights);\n }\n\n const expiration = raw.expiration\n ? parseExpirationConfig(raw.expiration)\n : DEFAULT_EXPIRATION;\n\n // Handle --user: Commander collects multiple -u flags into an array\n let user: string | string[] | undefined;\n if (raw.user && raw.user.length === 1) {\n user = raw.user[0];\n } else if (raw.user && raw.user.length > 1) {\n user = raw.user;\n }\n\n // Parse --hotspot flag: true (no arg) or \"team\" or \"personal\"\n let hotspot: HotspotMode | undefined;\n if (raw.hotspot !== undefined && raw.hotspot !== false) {\n if (raw.hotspot === \"team\") {\n hotspot = \"team\";\n } else {\n hotspot = \"personal\";\n }\n }\n\n // Parse --window flag\n const windowDays = raw.window ? parseInt(raw.window, 10) : undefined;\n\n return {\n mode,\n user,\n expiration,\n html: raw.html || false,\n weights,\n repoPath,\n team: raw.team || false,\n teamCoverage: raw.teamCoverage || false,\n hotspot,\n window: windowDays,\n };\n}\n\nfunction validateMode(mode: string): ScoringMode {\n const valid: ScoringMode[] = [\"binary\", \"authorship\", \"weighted\"];\n if (!valid.includes(mode as ScoringMode)) {\n throw new Error(\n `Invalid mode: \"${mode}\". Valid modes: ${valid.join(\", \")}`,\n );\n }\n return mode as ScoringMode;\n}\n\nfunction parseWeights(s: string): WeightConfig {\n const parts = s.split(\",\").map(Number);\n if (parts.length !== 2 || parts.some(isNaN)) {\n throw new Error(`Invalid weights: \"${s}\". Expected format: \"0.5,0.5\"`);\n }\n const sum = parts[0] + parts[1];\n if (Math.abs(sum - 1) > 0.01) {\n throw new Error(`Weights must sum to 1.0, got ${sum}`);\n }\n return { blame: parts[0], commit: parts[1] };\n}\n","import chalk from \"chalk\";\nimport type { FamiliarityResult } from \"../../core/familiarity.js\";\nimport type { FolderScore, FileScore, TreeNode } from \"../../core/types.js\";\n\nconst BAR_WIDTH = 10;\nconst FILLED_CHAR = \"\\u2588\"; // █\nconst EMPTY_CHAR = \"\\u2591\"; // ░\n\nfunction makeBar(score: number): string {\n const filled = Math.round(score * BAR_WIDTH);\n const empty = BAR_WIDTH - filled;\n const bar = FILLED_CHAR.repeat(filled) + EMPTY_CHAR.repeat(empty);\n\n if (score >= 0.8) return chalk.green(bar);\n if (score >= 0.5) return chalk.yellow(bar);\n if (score > 0) return chalk.red(bar);\n return chalk.gray(bar);\n}\n\nfunction formatPercent(score: number): string {\n return `${Math.round(score * 100)}%`;\n}\n\nfunction getModeLabel(mode: string): string {\n switch (mode) {\n case \"binary\":\n return \"Binary mode\";\n case \"authorship\":\n return \"Authorship mode\";\n case \"weighted\":\n return \"Weighted mode\";\n default:\n return mode;\n }\n}\n\nconst NAME_COLUMN_WIDTH = 24; // total width for indent + folder name\n\nfunction renderFolder(\n node: FolderScore,\n indent: number,\n mode: string,\n maxDepth: number,\n): string[] {\n const lines: string[] = [];\n const prefix = \" \".repeat(indent);\n const prefixWidth = indent * 2;\n\n // Sort children: folders first, then files, by name\n const sorted = [...node.children].sort((a, b) => {\n if (a.type !== b.type) return a.type === \"folder\" ? -1 : 1;\n return a.path.localeCompare(b.path);\n });\n\n for (const child of sorted) {\n if (child.type === \"folder\") {\n const folder = child as FolderScore;\n const name = folder.path.split(\"/\").pop() + \"/\";\n const bar = makeBar(folder.score);\n const pct = formatPercent(folder.score);\n const padWidth = Math.max(\n 1,\n NAME_COLUMN_WIDTH - prefixWidth - name.length,\n );\n const padding = \" \".repeat(padWidth);\n\n if (mode === \"binary\") {\n const readCount = folder.readCount || 0;\n lines.push(\n `${prefix}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`,\n );\n } else {\n lines.push(\n `${prefix}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)}`,\n );\n }\n\n // Recurse if within depth limit\n if (indent < maxDepth) {\n lines.push(...renderFolder(folder, indent + 1, mode, maxDepth));\n }\n }\n }\n\n return lines;\n}\n\nexport function renderTerminal(result: FamiliarityResult): void {\n const { tree, repoName, mode } = result;\n\n console.log(\"\");\n console.log(\n chalk.bold(`GitFamiliar \\u2014 ${repoName} (${getModeLabel(mode)})`),\n );\n console.log(\"\");\n\n if (mode === \"binary\") {\n const readCount = tree.readCount || 0;\n const pct = formatPercent(tree.score);\n console.log(`Overall: ${readCount}/${tree.fileCount} files (${pct})`);\n } else {\n const pct = formatPercent(tree.score);\n console.log(`Overall: ${pct}`);\n }\n\n console.log(\"\");\n\n const folderLines = renderFolder(tree, 1, mode, 2);\n for (const line of folderLines) {\n console.log(line);\n }\n\n console.log(\"\");\n\n if (mode === \"binary\") {\n const { writtenCount } = result;\n console.log(`Written: ${writtenCount} files`);\n console.log(\"\");\n }\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { FamiliarityResult } from \"../../core/familiarity.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateTreemapHTML(result: FamiliarityResult): string {\n const dataJson = JSON.stringify(result.tree);\n const mode = result.mode;\n const repoName = result.repoName;\n\n 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>GitFamiliar \\u2014 ${repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n\n #treemap { width: 100%; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 300px;\n }\n #legend {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n }\n #legend .gradient-bar {\n width: 120px;\n height: 12px;\n background: linear-gradient(to right, #e94560, #f5a623, #27ae60);\n border-radius: 3px;\n margin: 4px 0;\n }\n #legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${repoName}</h1>\n <div class=\"info\">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>\n</div>\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n\n<div id=\"treemap\"></div>\n<div id=\"tooltip\"></div>\n<div id=\"legend\">\n <div>Familiarity</div>\n <div class=\"gradient-bar\"></div>\n <div class=\"labels\"><span>0%</span><span>50%</span><span>100%</span></div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst rawData = ${dataJson};\nconst mode = \"${mode}\";\nlet currentPath = '';\n\nfunction scoreColor(score) {\n if (score <= 0) return '#e94560';\n if (score >= 1) return '#27ae60';\n if (score < 0.5) {\n const t = score / 0.5;\n return d3.interpolateRgb('#e94560', '#f5a623')(t);\n }\n const t = (score - 0.5) / 0.5;\n return d3.interpolateRgb('#f5a623', '#27ae60')(t);\n}\n\nfunction getNodeScore(node) {\n return node.score;\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction totalLines(node) {\n if (node.type === 'file') return Math.max(1, node.lines);\n if (!node.children) return 1;\n let sum = 0;\n for (const c of node.children) sum += totalLines(c);\n return Math.max(1, sum);\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction render() {\n const container = document.getElementById('treemap');\n container.innerHTML = '';\n\n const headerH = document.getElementById('header').offsetHeight;\n const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;\n const width = window.innerWidth;\n const height = window.innerHeight - headerH - breadcrumbH;\n\n const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;\n if (!targetNode) return;\n\n const children = targetNode.children || [];\n if (children.length === 0) return;\n\n // Build full nested hierarchy from the current target\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap()\n .size([width, height])\n .padding(2)\n .paddingTop(20)\n .round(true)(root);\n\n const svg = d3.select('#treemap')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const tooltip = document.getElementById('tooltip');\n\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g')\n .data(nodes)\n .join('g')\n .attr('transform', d => \\`translate(\\${d.x0},\\${d.y0})\\`);\n\n // Rect\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n return scoreColor(getNodeScore(d.data.data));\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e')\n .attr('stroke-width', d => d.children ? 1 : 0.5)\n .attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') {\n event.stopPropagation();\n zoomTo(d.data.data.path);\n }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n showTooltip(d.data.data, event);\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n tooltip.style.display = 'none';\n });\n\n // Labels\n groups.append('text')\n .attr('x', 4)\n .attr('y', 14)\n .attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => {\n const w = d.x1 - d.x0;\n const h = d.y1 - d.y0;\n const name = d.data.name || '';\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n });\n}\n\nfunction showTooltip(data, event) {\n const tooltip = document.getElementById('tooltip');\n const name = data.path || '';\n const score = getNodeScore(data);\n let html = '<strong>' + name + '</strong>';\n html += '<br>Score: ' + Math.round(score * 100) + '%';\n html += '<br>Lines: ' + data.lines.toLocaleString();\n if (data.type === 'folder') {\n html += '<br>Files: ' + data.fileCount;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n }\n if (data.blameScore !== undefined) {\n html += '<br>Blame: ' + Math.round(data.blameScore * 100) + '%';\n }\n if (data.commitScore !== undefined) {\n html += '<br>Commit: ' + Math.round(data.commitScore * 100) + '%';\n }\n\n if (data.isExpired) {\n html += '<br><span style=\"color:#e94560\">Expired</span>';\n }\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction zoomTo(path) {\n currentPath = path;\n updateBreadcrumb();\n render();\n}\n\nfunction updateBreadcrumb() {\n const el = document.getElementById('breadcrumb');\n const parts = currentPath ? currentPath.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += \\`<span class=\"sep\">/</span><span onclick=\"zoomTo('\\${p}')\">\\${part}</span>\\`;\n }\n el.innerHTML = html;\n}\n\nwindow.addEventListener('resize', render);\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenHTML(\n result: FamiliarityResult,\n repoPath: string,\n): Promise<void> {\n const html = generateTreemapHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-report.html\");\n\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Report generated: ${outputPath}`);\n\n await openBrowser(outputPath);\n}\n","export async function openBrowser(filePath: string): Promise<void> {\n try {\n const open = await import('open');\n await open.default(filePath);\n } catch {\n console.log(`Could not open browser automatically. Open this file manually:`);\n console.log(` ${filePath}`);\n }\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity } from \"../core/types.js\";\n\nconst COMMIT_SEP = \"GITFAMILIAR_SEP\";\n\n/**\n * Get all unique contributors from git history.\n * Returns deduplicated list of UserIdentity, sorted by commit count descending.\n */\nexport async function getAllContributors(\n gitClient: GitClient,\n minCommits: number = 1,\n): Promise<UserIdentity[]> {\n const output = await gitClient.getLog([\n \"--all\",\n `--format=%aN|%aE`,\n ]);\n\n const counts = new Map<string, { name: string; email: string; count: number }>();\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.includes(\"|\")) continue;\n const [name, email] = line.split(\"|\", 2);\n if (!name || !email) continue;\n\n const key = email.toLowerCase();\n const existing = counts.get(key);\n if (existing) {\n existing.count++;\n } else {\n counts.set(key, { name: name.trim(), email: email.trim(), count: 1 });\n }\n }\n\n return Array.from(counts.values())\n .filter((c) => c.count >= minCommits)\n .sort((a, b) => b.count - a.count)\n .map((c) => ({ name: c.name, email: c.email }));\n}\n\n/**\n * Bulk get file → contributors mapping from a single git log call.\n * Much faster than per-file git log queries.\n */\nexport async function bulkGetFileContributors(\n gitClient: GitClient,\n trackedFiles: Set<string>,\n): Promise<Map<string, Set<string>>> {\n const output = await gitClient.getLog([\n \"--all\",\n \"--name-only\",\n `--format=${COMMIT_SEP}%aN|%aE`,\n ]);\n\n const result = new Map<string, Set<string>>();\n\n let currentAuthor = \"\";\n for (const line of output.split(\"\\n\")) {\n if (line.startsWith(COMMIT_SEP)) {\n const parts = line.slice(COMMIT_SEP.length).split(\"|\", 2);\n currentAuthor = parts[0]?.trim() || \"\";\n continue;\n }\n\n const filePath = line.trim();\n if (!filePath || !currentAuthor) continue;\n if (!trackedFiles.has(filePath)) continue;\n\n let contributors = result.get(filePath);\n if (!contributors) {\n contributors = new Set<string>();\n result.set(filePath, contributors);\n }\n contributors.add(currentAuthor);\n }\n\n return result;\n}\n","import type {\n CliOptions,\n CoverageFileScore,\n CoverageFolderScore,\n CoverageTreeNode,\n RiskLevel,\n TeamCoverageResult,\n} from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { createFilter } from \"../filter/ignore.js\";\nimport { buildFileTree, walkFiles } from \"./file-tree.js\";\nimport { bulkGetFileContributors, getAllContributors } from \"../git/contributors.js\";\n\nexport async function computeTeamCoverage(\n options: CliOptions,\n): Promise<TeamCoverageResult> {\n const gitClient = new GitClient(options.repoPath);\n\n if (!(await gitClient.isRepo())) {\n throw new Error(`\"${options.repoPath}\" is not a git repository.`);\n }\n\n const repoRoot = await gitClient.getRepoRoot();\n const repoName = await gitClient.getRepoName();\n const filter = createFilter(repoRoot);\n const tree = await buildFileTree(gitClient, filter);\n\n // Get all tracked files\n const trackedFiles = new Set<string>();\n walkFiles(tree, (f) => trackedFiles.add(f.path));\n\n // Bulk get contributors for all files\n const fileContributors = await bulkGetFileContributors(gitClient, trackedFiles);\n const allContributors = await getAllContributors(gitClient);\n\n // Build coverage tree\n const coverageTree = buildCoverageTree(tree, fileContributors);\n\n // Identify risk files\n const riskFiles: CoverageFileScore[] = [];\n walkCoverageFiles(coverageTree, (f) => {\n if (f.contributorCount <= 1) {\n riskFiles.push(f);\n }\n });\n riskFiles.sort((a, b) => a.contributorCount - b.contributorCount);\n\n return {\n tree: coverageTree,\n repoName,\n totalContributors: allContributors.length,\n totalFiles: tree.fileCount,\n riskFiles,\n overallBusFactor: calculateBusFactor(fileContributors),\n };\n}\n\nfunction classifyRisk(contributorCount: number): RiskLevel {\n if (contributorCount <= 1) return \"risk\";\n if (contributorCount <= 3) return \"moderate\";\n return \"safe\";\n}\n\nfunction buildCoverageTree(\n node: import(\"./types.js\").FolderScore,\n fileContributors: Map<string, Set<string>>,\n): CoverageFolderScore {\n const children: CoverageTreeNode[] = [];\n\n for (const child of node.children) {\n if (child.type === \"file\") {\n const contributors = fileContributors.get(child.path);\n const names = contributors ? Array.from(contributors) : [];\n children.push({\n type: \"file\",\n path: child.path,\n lines: child.lines,\n contributorCount: names.length,\n contributors: names,\n riskLevel: classifyRisk(names.length),\n });\n } else {\n children.push(buildCoverageTree(child, fileContributors));\n }\n }\n\n // Compute folder aggregates\n const fileScores: CoverageFileScore[] = [];\n walkCoverageFiles({ type: \"folder\", path: \"\", lines: 0, fileCount: 0, avgContributors: 0, busFactor: 0, riskLevel: \"safe\", children }, (f) => {\n fileScores.push(f);\n });\n\n const totalContributors = fileScores.reduce((sum, f) => sum + f.contributorCount, 0);\n const avgContributors = fileScores.length > 0 ? totalContributors / fileScores.length : 0;\n\n // Calculate bus factor for this folder's files\n const folderFileContributors = new Map<string, Set<string>>();\n for (const f of fileScores) {\n folderFileContributors.set(f.path, new Set(f.contributors));\n }\n const busFactor = calculateBusFactor(folderFileContributors);\n\n return {\n type: \"folder\",\n path: node.path,\n lines: node.lines,\n fileCount: node.fileCount,\n avgContributors: Math.round(avgContributors * 10) / 10,\n busFactor,\n riskLevel: classifyRisk(busFactor),\n children,\n };\n}\n\nfunction walkCoverageFiles(\n node: CoverageTreeNode,\n visitor: (file: CoverageFileScore) => void,\n): void {\n if (node.type === \"file\") {\n visitor(node);\n } else {\n for (const child of node.children) {\n walkCoverageFiles(child, visitor);\n }\n }\n}\n\n/**\n * Calculate bus factor using greedy set cover.\n * Bus factor = minimum number of people who cover >50% of files.\n */\nexport function calculateBusFactor(\n fileContributors: Map<string, Set<string>>,\n): number {\n const totalFiles = fileContributors.size;\n if (totalFiles === 0) return 0;\n\n const target = Math.ceil(totalFiles * 0.5);\n\n // Count files per contributor\n const contributorFiles = new Map<string, Set<string>>();\n for (const [file, contributors] of fileContributors) {\n for (const contributor of contributors) {\n let files = contributorFiles.get(contributor);\n if (!files) {\n files = new Set<string>();\n contributorFiles.set(contributor, files);\n }\n files.add(file);\n }\n }\n\n // Greedy: pick contributor covering most uncovered files\n const coveredFiles = new Set<string>();\n let count = 0;\n\n while (coveredFiles.size < target && contributorFiles.size > 0) {\n let bestContributor = \"\";\n let bestNewFiles = 0;\n\n for (const [contributor, files] of contributorFiles) {\n let newFiles = 0;\n for (const file of files) {\n if (!coveredFiles.has(file)) newFiles++;\n }\n if (newFiles > bestNewFiles) {\n bestNewFiles = newFiles;\n bestContributor = contributor;\n }\n }\n\n if (bestNewFiles === 0) break;\n\n const files = contributorFiles.get(bestContributor)!;\n for (const file of files) {\n coveredFiles.add(file);\n }\n contributorFiles.delete(bestContributor);\n count++;\n }\n\n return count;\n}\n","import chalk from \"chalk\";\nimport type {\n TeamCoverageResult,\n CoverageFolderScore,\n CoverageFileScore,\n} from \"../../core/types.js\";\n\nfunction riskBadge(level: string): string {\n switch (level) {\n case \"risk\":\n return chalk.bgRed.white(\" RISK \");\n case \"moderate\":\n return chalk.bgYellow.black(\" MOD \");\n case \"safe\":\n return chalk.bgGreen.black(\" SAFE \");\n default:\n return level;\n }\n}\n\nfunction riskColor(level: string): typeof chalk {\n switch (level) {\n case \"risk\":\n return chalk.red;\n case \"moderate\":\n return chalk.yellow;\n default:\n return chalk.green;\n }\n}\n\nfunction renderFolder(\n node: CoverageFolderScore,\n indent: number,\n maxDepth: number,\n): string[] {\n const lines: string[] = [];\n\n const sorted = [...node.children].sort((a, b) => {\n if (a.type !== b.type) return a.type === \"folder\" ? -1 : 1;\n return a.path.localeCompare(b.path);\n });\n\n for (const child of sorted) {\n if (child.type === \"folder\") {\n const prefix = \" \".repeat(indent);\n const name = (child.path.split(\"/\").pop() || child.path) + \"/\";\n const color = riskColor(child.riskLevel);\n lines.push(\n `${prefix}${chalk.bold(name.padEnd(24))} ${String(child.avgContributors).padStart(4)} avg ${String(child.busFactor).padStart(2)} ${riskBadge(child.riskLevel)}`,\n );\n if (indent < maxDepth) {\n lines.push(...renderFolder(child, indent + 1, maxDepth));\n }\n }\n }\n\n return lines;\n}\n\nexport function renderCoverageTerminal(result: TeamCoverageResult): void {\n console.log(\"\");\n console.log(\n chalk.bold(\n `GitFamiliar \\u2014 Team Coverage (${result.totalFiles} files, ${result.totalContributors} contributors)`,\n ),\n );\n console.log(\"\");\n\n // Overall bus factor\n const bfColor =\n result.overallBusFactor <= 1\n ? chalk.red\n : result.overallBusFactor <= 2\n ? chalk.yellow\n : chalk.green;\n console.log(`Overall Bus Factor: ${bfColor.bold(String(result.overallBusFactor))}`);\n console.log(\"\");\n\n // Risk files\n if (result.riskFiles.length > 0) {\n console.log(chalk.red.bold(`Risk Files (0-1 contributors):`));\n const displayFiles = result.riskFiles.slice(0, 20);\n for (const file of displayFiles) {\n const count = file.contributorCount;\n const names = file.contributors.join(\", \");\n const label =\n count === 0\n ? chalk.red(\"0 people\")\n : chalk.yellow(`1 person (${names})`);\n console.log(` ${file.path.padEnd(40)} ${label}`);\n }\n if (result.riskFiles.length > 20) {\n console.log(\n chalk.gray(` ... and ${result.riskFiles.length - 20} more`),\n );\n }\n console.log(\"\");\n } else {\n console.log(chalk.green(\"No high-risk files found.\"));\n console.log(\"\");\n }\n\n // Folder coverage table\n console.log(chalk.bold(\"Folder Coverage:\"));\n console.log(\n chalk.gray(\n ` ${\"Folder\".padEnd(24)} ${\"Avg Contrib\".padStart(11)} ${\"Bus Factor\".padStart(10)} Risk`,\n ),\n );\n\n const folderLines = renderFolder(result.tree, 1, 2);\n for (const line of folderLines) {\n console.log(line);\n }\n\n console.log(\"\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { TeamCoverageResult } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateCoverageHTML(result: TeamCoverageResult): string {\n const dataJson = JSON.stringify(result.tree);\n const riskFilesJson = JSON.stringify(result.riskFiles);\n\n 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>GitFamiliar \\u2014 Team Coverage \\u2014 ${result.repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n #main { display: flex; height: calc(100vh - 90px); }\n #treemap { flex: 1; }\n #sidebar {\n width: 300px;\n background: #16213e;\n border-left: 1px solid #0f3460;\n overflow-y: auto;\n padding: 16px;\n }\n #sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }\n #sidebar .risk-file {\n padding: 6px 0;\n border-bottom: 1px solid #0f3460;\n font-size: 12px;\n }\n #sidebar .risk-file .path { color: #e0e0e0; word-break: break-all; }\n #sidebar .risk-file .meta { color: #888; margin-top: 2px; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 320px;\n }\n #legend {\n position: absolute;\n bottom: 16px;\n left: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n }\n #legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }\n #legend .swatch { width: 14px; height: 14px; border-radius: 3px; }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 Team Coverage \\u2014 ${result.repoName}</h1>\n <div class=\"info\">${result.totalFiles} files | ${result.totalContributors} contributors | Bus Factor: ${result.overallBusFactor}</div>\n</div>\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n<div id=\"main\">\n <div id=\"treemap\"></div>\n <div id=\"sidebar\">\n <h3>Risk Files (0-1 contributors)</h3>\n <div id=\"risk-list\"></div>\n </div>\n</div>\n<div id=\"tooltip\"></div>\n<div id=\"legend\">\n <div>Contributors</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#e94560\"></div> 0\\u20131 (Risk)</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#f5a623\"></div> 2\\u20133 (Moderate)</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#27ae60\"></div> 4+ (Safe)</div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst rawData = ${dataJson};\nconst riskFiles = ${riskFilesJson};\nlet currentPath = '';\n\nfunction coverageColor(count) {\n if (count <= 0) return '#e94560';\n if (count === 1) return '#d63c57';\n if (count <= 3) return '#f5a623';\n return '#27ae60';\n}\n\nfunction folderColor(riskLevel) {\n switch (riskLevel) {\n case 'risk': return '#e94560';\n case 'moderate': return '#f5a623';\n default: return '#27ae60';\n }\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction render() {\n const container = document.getElementById('treemap');\n container.innerHTML = '';\n\n const headerH = document.getElementById('header').offsetHeight;\n const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;\n const width = container.offsetWidth;\n const height = window.innerHeight - headerH - breadcrumbH;\n\n const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap()\n .size([width, height])\n .padding(2)\n .paddingTop(20)\n .round(true)(root);\n\n const svg = d3.select('#treemap')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const tooltip = document.getElementById('tooltip');\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g')\n .data(nodes)\n .join('g')\n .attr('transform', d => \\`translate(\\${d.x0},\\${d.y0})\\`);\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n if (d.data.data.type === 'file') return coverageColor(d.data.data.contributorCount);\n return folderColor(d.data.data.riskLevel);\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e')\n .attr('stroke-width', d => d.children ? 1 : 0.5)\n .attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') {\n event.stopPropagation();\n zoomTo(d.data.data.path);\n }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n showTooltip(d.data.data, event);\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n tooltip.style.display = 'none';\n });\n\n groups.append('text')\n .attr('x', 4)\n .attr('y', 14)\n .attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => {\n const w = d.x1 - d.x0;\n const h = d.y1 - d.y0;\n const name = d.data.name || '';\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n });\n}\n\nfunction showTooltip(data, event) {\n const tooltip = document.getElementById('tooltip');\n let html = '<strong>' + data.path + '</strong>';\n if (data.type === 'file') {\n html += '<br>Contributors: ' + data.contributorCount;\n if (data.contributors.length > 0) {\n html += '<br>' + data.contributors.slice(0, 8).join(', ');\n if (data.contributors.length > 8) html += ', ...';\n }\n html += '<br>Lines: ' + data.lines.toLocaleString();\n } else {\n html += '<br>Files: ' + data.fileCount;\n html += '<br>Avg Contributors: ' + data.avgContributors;\n html += '<br>Bus Factor: ' + data.busFactor;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n }\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction zoomTo(path) {\n currentPath = path;\n const el = document.getElementById('breadcrumb');\n const parts = path ? path.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += '<span class=\"sep\">/</span><span onclick=\"zoomTo(\\\\'' + p + '\\\\')\">' + part + '</span>';\n }\n el.innerHTML = html;\n render();\n}\n\n// Render risk sidebar\nfunction renderRiskSidebar() {\n const container = document.getElementById('risk-list');\n if (riskFiles.length === 0) {\n container.innerHTML = '<div style=\"color:#888\">No high-risk files found.</div>';\n return;\n }\n let html = '';\n for (const f of riskFiles.slice(0, 50)) {\n const countLabel = f.contributorCount === 0 ? '0 people' : '1 person (' + f.contributors[0] + ')';\n html += '<div class=\"risk-file\"><div class=\"path\">' + f.path + '</div><div class=\"meta\">' + countLabel + '</div></div>';\n }\n if (riskFiles.length > 50) {\n html += '<div style=\"color:#888;padding:8px 0\">... and ' + (riskFiles.length - 50) + ' more</div>';\n }\n container.innerHTML = html;\n}\n\nwindow.addEventListener('resize', render);\nrenderRiskSidebar();\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenCoverageHTML(\n result: TeamCoverageResult,\n repoPath: string,\n): Promise<void> {\n const html = generateCoverageHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-coverage.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Coverage report generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import type {\n CliOptions,\n MultiUserResult,\n MultiUserFolderScore,\n MultiUserFileScore,\n MultiUserTreeNode,\n UserScore,\n UserSummary,\n UserIdentity,\n FolderScore,\n FileScore,\n TreeNode,\n} from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { computeFamiliarity, type FamiliarityResult } from \"./familiarity.js\";\nimport { getAllContributors } from \"../git/contributors.js\";\nimport { resolveUser } from \"../git/identity.js\";\nimport { processBatch } from \"../utils/batch.js\";\nimport { walkFiles } from \"./file-tree.js\";\n\nexport async function computeMultiUser(\n options: CliOptions,\n): Promise<MultiUserResult> {\n const gitClient = new GitClient(options.repoPath);\n\n if (!(await gitClient.isRepo())) {\n throw new Error(`\"${options.repoPath}\" is not a git repository.`);\n }\n\n const repoName = await gitClient.getRepoName();\n\n // Determine which users to compare\n let userNames: string[];\n if (options.team) {\n const contributors = await getAllContributors(gitClient, 3);\n userNames = contributors.map((c) => c.name);\n if (userNames.length === 0) {\n throw new Error(\"No contributors found with 3+ commits.\");\n }\n console.log(`Found ${userNames.length} contributors with 3+ commits`);\n } else if (Array.isArray(options.user)) {\n userNames = options.user;\n } else if (options.user) {\n userNames = [options.user];\n } else {\n // Default: current user only\n const user = await resolveUser(gitClient);\n userNames = [user.name || user.email];\n }\n\n // Run scoring for each user (batched, 3 at a time)\n const results: Array<{ userName: string; result: FamiliarityResult }> = [];\n\n await processBatch(\n userNames,\n async (userName) => {\n const userOptions: CliOptions = {\n ...options,\n user: userName,\n team: false,\n teamCoverage: false,\n };\n const result = await computeFamiliarity(userOptions);\n results.push({ userName, result });\n },\n 3,\n );\n\n // Resolve user identities\n const users: UserIdentity[] = results.map((r) => ({\n name: r.result.userName,\n email: \"\",\n }));\n\n // Merge results into multi-user tree\n const tree = mergeResults(results);\n\n // Compute user summaries\n const userSummaries: UserSummary[] = results.map((r) => ({\n user: { name: r.result.userName, email: \"\" },\n writtenCount: r.result.writtenCount,\n overallScore: r.result.tree.score,\n }));\n\n return {\n tree,\n repoName,\n users,\n mode: options.mode,\n totalFiles: results[0]?.result.totalFiles || 0,\n userSummaries,\n };\n}\n\nfunction mergeResults(\n results: Array<{ userName: string; result: FamiliarityResult }>,\n): MultiUserFolderScore {\n if (results.length === 0) {\n return {\n type: \"folder\",\n path: \"\",\n lines: 0,\n score: 0,\n fileCount: 0,\n userScores: [],\n children: [],\n };\n }\n\n // Use first result as the structural template\n const baseTree = results[0].result.tree;\n\n // Build a map from file path → per-user scores\n const fileScoresMap = new Map<string, UserScore[]>();\n for (const { result } of results) {\n const userName = result.userName;\n walkFiles(result.tree, (file: FileScore) => {\n let scores = fileScoresMap.get(file.path);\n if (!scores) {\n scores = [];\n fileScoresMap.set(file.path, scores);\n }\n scores.push({\n user: { name: userName, email: \"\" },\n score: file.score,\n isWritten: file.isWritten,\n });\n });\n }\n\n return convertFolder(baseTree, fileScoresMap, results);\n}\n\nfunction convertFolder(\n node: FolderScore,\n fileScoresMap: Map<string, UserScore[]>,\n results: Array<{ userName: string; result: FamiliarityResult }>,\n): MultiUserFolderScore {\n const children: MultiUserTreeNode[] = [];\n\n for (const child of node.children) {\n if (child.type === \"file\") {\n const userScores = fileScoresMap.get(child.path) || [];\n const avgScore =\n userScores.length > 0\n ? userScores.reduce((sum, s) => sum + s.score, 0) / userScores.length\n : 0;\n children.push({\n type: \"file\",\n path: child.path,\n lines: child.lines,\n score: avgScore,\n userScores,\n });\n } else {\n children.push(convertFolder(child, fileScoresMap, results));\n }\n }\n\n // Compute folder-level user scores\n const userScores: UserScore[] = results.map(({ result }) => {\n // Find this folder in the user's result tree\n const folderNode = findFolderInTree(result.tree, node.path);\n return {\n user: { name: result.userName, email: \"\" },\n score: folderNode?.score || 0,\n };\n });\n\n const avgScore =\n userScores.length > 0\n ? userScores.reduce((sum, s) => sum + s.score, 0) / userScores.length\n : 0;\n\n return {\n type: \"folder\",\n path: node.path,\n lines: node.lines,\n score: avgScore,\n fileCount: node.fileCount,\n userScores,\n children,\n };\n}\n\nfunction findFolderInTree(\n node: TreeNode,\n targetPath: string,\n): FolderScore | null {\n if (node.type === \"folder\") {\n if (node.path === targetPath) return node;\n for (const child of node.children) {\n const found = findFolderInTree(child, targetPath);\n if (found) return found;\n }\n }\n return null;\n}\n","import chalk from \"chalk\";\nimport type {\n MultiUserResult,\n MultiUserFolderScore,\n UserScore,\n} from \"../../core/types.js\";\n\nconst BAR_WIDTH = 20;\nconst FILLED_CHAR = \"\\u2588\";\nconst EMPTY_CHAR = \"\\u2591\";\n\nfunction makeBar(score: number, width: number = BAR_WIDTH): string {\n const filled = Math.round(score * width);\n const empty = width - filled;\n const bar = FILLED_CHAR.repeat(filled) + EMPTY_CHAR.repeat(empty);\n if (score >= 0.8) return chalk.green(bar);\n if (score >= 0.5) return chalk.yellow(bar);\n if (score > 0) return chalk.red(bar);\n return chalk.gray(bar);\n}\n\nfunction formatPercent(score: number): string {\n return `${Math.round(score * 100)}%`;\n}\n\nfunction getModeLabel(mode: string): string {\n switch (mode) {\n case \"binary\":\n return \"Binary mode\";\n case \"authorship\":\n return \"Authorship mode\";\n case \"weighted\":\n return \"Weighted mode\";\n default:\n return mode;\n }\n}\n\nfunction truncateName(name: string, maxLen: number): string {\n if (name.length <= maxLen) return name;\n return name.slice(0, maxLen - 1) + \"\\u2026\";\n}\n\nfunction renderFolder(\n node: MultiUserFolderScore,\n indent: number,\n maxDepth: number,\n nameWidth: number,\n): string[] {\n const lines: string[] = [];\n\n const sorted = [...node.children].sort((a, b) => {\n if (a.type !== b.type) return a.type === \"folder\" ? -1 : 1;\n return a.path.localeCompare(b.path);\n });\n\n for (const child of sorted) {\n if (child.type === \"folder\") {\n const prefix = \" \".repeat(indent);\n const name = (child.path.split(\"/\").pop() || child.path) + \"/\";\n const displayName = truncateName(name, nameWidth).padEnd(nameWidth);\n\n const scores = child.userScores\n .map((s) => formatPercent(s.score).padStart(5))\n .join(\" \");\n\n lines.push(`${prefix}${chalk.bold(displayName)} ${scores}`);\n\n if (indent < maxDepth) {\n lines.push(...renderFolder(child, indent + 1, maxDepth, nameWidth));\n }\n }\n }\n\n return lines;\n}\n\nexport function renderMultiUserTerminal(result: MultiUserResult): void {\n const { tree, repoName, mode, userSummaries, totalFiles } = result;\n\n console.log(\"\");\n console.log(\n chalk.bold(\n `GitFamiliar \\u2014 ${repoName} (${getModeLabel(mode)}, ${userSummaries.length} users)`,\n ),\n );\n console.log(\"\");\n\n // Overall per-user stats\n console.log(chalk.bold(\"Overall:\"));\n for (const summary of userSummaries) {\n const name = truncateName(summary.user.name, 14).padEnd(14);\n const bar = makeBar(summary.overallScore);\n const pct = formatPercent(summary.overallScore);\n\n if (mode === \"binary\") {\n console.log(\n ` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`,\n );\n } else {\n console.log(` ${name} ${bar} ${pct.padStart(4)}`);\n }\n }\n console.log(\"\");\n\n // Folder breakdown header\n const nameWidth = 20;\n const headerNames = userSummaries\n .map((s) => truncateName(s.user.name, 7).padStart(7))\n .join(\" \");\n console.log(chalk.bold(\"Folders:\") + \" \".repeat(nameWidth - 4) + headerNames);\n\n const folderLines = renderFolder(tree, 1, 2, nameWidth);\n for (const line of folderLines) {\n console.log(line);\n }\n\n console.log(\"\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { MultiUserResult } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateMultiUserHTML(result: MultiUserResult): string {\n const dataJson = JSON.stringify(result.tree);\n const summariesJson = JSON.stringify(result.userSummaries);\n const usersJson = JSON.stringify(result.users.map((u) => u.name));\n\n 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>GitFamiliar \\u2014 ${result.repoName} \\u2014 Multi-User</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .controls { display: flex; align-items: center; gap: 12px; }\n #header select {\n padding: 4px 12px;\n border: 1px solid #0f3460;\n background: #1a1a2e;\n color: #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n #treemap { width: 100%; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 350px;\n }\n #legend {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n }\n #legend .gradient-bar {\n width: 120px; height: 12px;\n background: linear-gradient(to right, #e94560, #f5a623, #27ae60);\n border-radius: 3px; margin: 4px 0;\n }\n #legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${result.repoName}</h1>\n <div class=\"controls\">\n <span style=\"color:#888;font-size:13px;\">View as:</span>\n <select id=\"userSelect\" onchange=\"changeUser()\"></select>\n <div class=\"info\">${result.mode} mode | ${result.totalFiles} files</div>\n </div>\n</div>\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n<div id=\"treemap\"></div>\n<div id=\"tooltip\"></div>\n<div id=\"legend\">\n <div>Familiarity</div>\n <div class=\"gradient-bar\"></div>\n <div class=\"labels\"><span>0%</span><span>50%</span><span>100%</span></div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst rawData = ${dataJson};\nconst userNames = ${usersJson};\nconst summaries = ${summariesJson};\nlet currentUser = 0;\nlet currentPath = '';\n\n// Populate user selector\nconst select = document.getElementById('userSelect');\nuserNames.forEach((name, i) => {\n const opt = document.createElement('option');\n opt.value = i;\n const summary = summaries[i];\n opt.textContent = name + ' (' + Math.round(summary.overallScore * 100) + '%)';\n select.appendChild(opt);\n});\n\nfunction changeUser() {\n currentUser = parseInt(select.value);\n render();\n}\n\nfunction scoreColor(score) {\n if (score <= 0) return '#e94560';\n if (score >= 1) return '#27ae60';\n if (score < 0.5) {\n const t = score / 0.5;\n return d3.interpolateRgb('#e94560', '#f5a623')(t);\n }\n const t = (score - 0.5) / 0.5;\n return d3.interpolateRgb('#f5a623', '#27ae60')(t);\n}\n\nfunction getUserScore(node) {\n if (!node.userScores || node.userScores.length === 0) return node.score;\n const s = node.userScores[currentUser];\n return s ? s.score : 0;\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction render() {\n const container = document.getElementById('treemap');\n container.innerHTML = '';\n\n const headerH = document.getElementById('header').offsetHeight;\n const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;\n const width = window.innerWidth;\n const height = window.innerHeight - headerH - breadcrumbH;\n\n const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap()\n .size([width, height])\n .padding(2)\n .paddingTop(20)\n .round(true)(root);\n\n const svg = d3.select('#treemap')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const tooltip = document.getElementById('tooltip');\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g')\n .data(nodes)\n .join('g')\n .attr('transform', d => \\`translate(\\${d.x0},\\${d.y0})\\`);\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n return scoreColor(getUserScore(d.data.data));\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e')\n .attr('stroke-width', d => d.children ? 1 : 0.5)\n .attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') {\n event.stopPropagation();\n zoomTo(d.data.data.path);\n }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n showTooltip(d.data.data, event);\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n tooltip.style.display = 'none';\n });\n\n groups.append('text')\n .attr('x', 4)\n .attr('y', 14)\n .attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => {\n const w = d.x1 - d.x0;\n const h = d.y1 - d.y0;\n const name = d.data.name || '';\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n });\n}\n\nfunction showTooltip(data, event) {\n const tooltip = document.getElementById('tooltip');\n let html = '<strong>' + (data.path || 'root') + '</strong>';\n\n if (data.userScores && data.userScores.length > 0) {\n html += '<table style=\"margin-top:6px;width:100%\">';\n data.userScores.forEach((s, i) => {\n const isCurrent = (i === currentUser);\n const style = isCurrent ? 'font-weight:bold;color:#5eadf7' : '';\n html += '<tr style=\"' + style + '\"><td>' + userNames[i] + '</td><td style=\"text-align:right\">' + Math.round(s.score * 100) + '%</td></tr>';\n });\n html += '</table>';\n }\n\n if (data.type === 'folder') {\n html += '<br>Files: ' + data.fileCount;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n } else {\n html += '<br>Lines: ' + data.lines.toLocaleString();\n }\n\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction zoomTo(path) {\n currentPath = path;\n const el = document.getElementById('breadcrumb');\n const parts = path ? path.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += '<span class=\"sep\">/</span><span onclick=\"zoomTo(\\\\'' + p + '\\\\')\">' + part + '</span>';\n }\n el.innerHTML = html;\n render();\n}\n\nwindow.addEventListener('resize', render);\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenMultiUserHTML(\n result: MultiUserResult,\n repoPath: string,\n): Promise<void> {\n const html = generateMultiUserHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-multiuser.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Multi-user report generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import type { GitClient } from \"./client.js\";\n\nconst COMMIT_SEP = \"GITFAMILIAR_FREQ_SEP\";\n\nexport interface FileChangeFrequency {\n commitCount: number;\n lastChanged: Date | null;\n}\n\n/**\n * Bulk get change frequency for all files in a single git log call.\n * Returns Map of filePath → { commitCount, lastChanged } within the given window.\n */\nexport async function bulkGetChangeFrequency(\n gitClient: GitClient,\n days: number,\n trackedFiles: Set<string>,\n): Promise<Map<string, FileChangeFrequency>> {\n const sinceDate = `${days} days ago`;\n\n const output = await gitClient.getLog([\n \"--all\",\n `--since=${sinceDate}`,\n \"--name-only\",\n `--format=${COMMIT_SEP}%aI`,\n ]);\n\n const result = new Map<string, FileChangeFrequency>();\n\n let currentDate: Date | null = null;\n\n for (const line of output.split(\"\\n\")) {\n if (line.startsWith(COMMIT_SEP)) {\n const dateStr = line.slice(COMMIT_SEP.length).trim();\n currentDate = dateStr ? new Date(dateStr) : null;\n continue;\n }\n\n const filePath = line.trim();\n if (!filePath || !trackedFiles.has(filePath)) continue;\n\n let entry = result.get(filePath);\n if (!entry) {\n entry = { commitCount: 0, lastChanged: null };\n result.set(filePath, entry);\n }\n entry.commitCount++;\n\n if (currentDate && (!entry.lastChanged || currentDate > entry.lastChanged)) {\n entry.lastChanged = currentDate;\n }\n }\n\n return result;\n}\n","import type {\n CliOptions,\n HotspotFileScore,\n HotspotResult,\n HotspotRiskLevel,\n FileScore,\n} from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { createFilter } from \"../filter/ignore.js\";\nimport { buildFileTree, walkFiles } from \"./file-tree.js\";\nimport { computeFamiliarity } from \"./familiarity.js\";\nimport { bulkGetChangeFrequency } from \"../git/change-frequency.js\";\nimport { bulkGetFileContributors, getAllContributors } from \"../git/contributors.js\";\nimport { processBatch } from \"../utils/batch.js\";\nimport { resolveUser } from \"../git/identity.js\";\n\nconst DEFAULT_WINDOW = 90;\n\nexport async function computeHotspots(\n options: CliOptions,\n): Promise<HotspotResult> {\n const gitClient = new GitClient(options.repoPath);\n\n if (!(await gitClient.isRepo())) {\n throw new Error(`\"${options.repoPath}\" is not a git repository.`);\n }\n\n const repoName = await gitClient.getRepoName();\n const repoRoot = await gitClient.getRepoRoot();\n const filter = createFilter(repoRoot);\n const tree = await buildFileTree(gitClient, filter);\n const timeWindow = options.window || DEFAULT_WINDOW;\n const isTeamMode = options.hotspot === \"team\";\n\n // Get all tracked files\n const trackedFiles = new Set<string>();\n walkFiles(tree, (f) => trackedFiles.add(f.path));\n\n // Get change frequency for all files (single git log call)\n const changeFreqMap = await bulkGetChangeFrequency(gitClient, timeWindow, trackedFiles);\n\n // Get familiarity scores\n let familiarityMap: Map<string, number>;\n let userName: string | undefined;\n\n if (isTeamMode) {\n // Team mode: average familiarity across all contributors\n familiarityMap = await computeTeamAvgFamiliarity(gitClient, trackedFiles, options);\n } else {\n // Personal mode: single user's familiarity\n const userFlag = Array.isArray(options.user) ? options.user[0] : options.user;\n const result = await computeFamiliarity({ ...options, team: false, teamCoverage: false });\n userName = result.userName;\n familiarityMap = new Map<string, number>();\n walkFiles(result.tree, (f) => {\n familiarityMap.set(f.path, f.score);\n });\n }\n\n // Find max change frequency for normalization\n let maxFreq = 0;\n for (const entry of changeFreqMap.values()) {\n if (entry.commitCount > maxFreq) maxFreq = entry.commitCount;\n }\n\n // Calculate risk for each file\n const hotspotFiles: HotspotFileScore[] = [];\n\n for (const filePath of trackedFiles) {\n const freq = changeFreqMap.get(filePath);\n const changeFrequency = freq?.commitCount || 0;\n const lastChanged = freq?.lastChanged || null;\n const familiarity = familiarityMap.get(filePath) || 0;\n\n // Normalize frequency to 0-1\n const normalizedFreq = maxFreq > 0 ? changeFrequency / maxFreq : 0;\n\n // Risk = normalizedFrequency × (1 - familiarity)\n const risk = normalizedFreq * (1 - familiarity);\n\n // Find lines from tree\n let lines = 0;\n walkFiles(tree, (f) => {\n if (f.path === filePath) lines = f.lines;\n });\n\n hotspotFiles.push({\n path: filePath,\n lines,\n familiarity,\n changeFrequency,\n lastChanged,\n risk,\n riskLevel: classifyHotspotRisk(risk),\n });\n }\n\n // Sort by risk descending\n hotspotFiles.sort((a, b) => b.risk - a.risk);\n\n // Compute summary\n const summary = { critical: 0, high: 0, medium: 0, low: 0 };\n for (const f of hotspotFiles) {\n summary[f.riskLevel]++;\n }\n\n return {\n files: hotspotFiles,\n repoName,\n userName,\n hotspotMode: isTeamMode ? \"team\" : \"personal\",\n timeWindow,\n summary,\n };\n}\n\nexport function classifyHotspotRisk(risk: number): HotspotRiskLevel {\n if (risk >= 0.6) return \"critical\";\n if (risk >= 0.4) return \"high\";\n if (risk >= 0.2) return \"medium\";\n return \"low\";\n}\n\n/**\n * For team mode: compute average familiarity across all contributors.\n * Uses bulkGetFileContributors (single git log call) to count how many people\n * know each file, then normalizes as: avgFam = contributorCount / totalContributors.\n * This is a lightweight proxy for \"how well-known is this file across the team\".\n */\nasync function computeTeamAvgFamiliarity(\n gitClient: GitClient,\n trackedFiles: Set<string>,\n options: CliOptions,\n): Promise<Map<string, number>> {\n const contributors = await getAllContributors(gitClient, 1);\n const totalContributors = Math.max(1, contributors.length);\n const fileContributors = await bulkGetFileContributors(gitClient, trackedFiles);\n\n const result = new Map<string, number>();\n for (const filePath of trackedFiles) {\n const contribs = fileContributors.get(filePath);\n const count = contribs ? contribs.size : 0;\n // Normalize: what fraction of the team knows this file\n // Cap at 1.0 (e.g., if everyone knows it)\n result.set(filePath, Math.min(1, count / Math.max(1, totalContributors * 0.3)));\n }\n\n return result;\n}\n","import chalk from \"chalk\";\nimport type { HotspotResult, HotspotRiskLevel } from \"../../core/types.js\";\n\nfunction riskBadge(level: HotspotRiskLevel): string {\n switch (level) {\n case \"critical\":\n return chalk.bgRed.white.bold(\" CRIT \");\n case \"high\":\n return chalk.bgRedBright.white(\" HIGH \");\n case \"medium\":\n return chalk.bgYellow.black(\" MED \");\n case \"low\":\n return chalk.bgGreen.black(\" LOW \");\n }\n}\n\nfunction riskColor(level: HotspotRiskLevel): typeof chalk {\n switch (level) {\n case \"critical\": return chalk.red;\n case \"high\": return chalk.redBright;\n case \"medium\": return chalk.yellow;\n case \"low\": return chalk.green;\n }\n}\n\nexport function renderHotspotTerminal(result: HotspotResult): void {\n const { files, repoName, hotspotMode, timeWindow, summary, userName } = result;\n\n console.log(\"\");\n const modeLabel = hotspotMode === \"team\" ? \"Team Hotspots\" : \"Personal Hotspots\";\n const userLabel = userName ? ` (${userName})` : \"\";\n console.log(\n chalk.bold(`GitFamiliar \\u2014 ${modeLabel}${userLabel} \\u2014 ${repoName}`),\n );\n console.log(chalk.gray(` Time window: last ${timeWindow} days`));\n console.log(\"\");\n\n // Filter to files with actual activity\n const activeFiles = files.filter((f) => f.changeFrequency > 0);\n\n if (activeFiles.length === 0) {\n console.log(chalk.gray(\" No files changed in the time window.\"));\n console.log(\"\");\n return;\n }\n\n // Top hotspots table\n const displayCount = Math.min(30, activeFiles.length);\n const topFiles = activeFiles.slice(0, displayCount);\n\n console.log(\n chalk.gray(\n ` ${\"Rank\".padEnd(5)} ${\"File\".padEnd(42)} ${\"Familiarity\".padStart(11)} ${\"Changes\".padStart(8)} ${\"Risk\".padStart(6)} Level`,\n ),\n );\n console.log(chalk.gray(\" \" + \"\\u2500\".repeat(90)));\n\n for (let i = 0; i < topFiles.length; i++) {\n const f = topFiles[i];\n const rank = String(i + 1).padEnd(5);\n const path = truncate(f.path, 42).padEnd(42);\n const fam = `${Math.round(f.familiarity * 100)}%`.padStart(11);\n const changes = String(f.changeFrequency).padStart(8);\n const risk = f.risk.toFixed(2).padStart(6);\n const color = riskColor(f.riskLevel);\n const badge = riskBadge(f.riskLevel);\n\n console.log(\n ` ${color(rank)}${path} ${fam} ${changes} ${color(risk)} ${badge}`,\n );\n }\n\n if (activeFiles.length > displayCount) {\n console.log(\n chalk.gray(` ... and ${activeFiles.length - displayCount} more files`),\n );\n }\n\n console.log(\"\");\n\n // Summary\n console.log(chalk.bold(\"Summary:\"));\n if (summary.critical > 0) {\n console.log(\n ` ${chalk.red.bold(`\\u{1F534} Critical Risk: ${summary.critical} files`)}`,\n );\n }\n if (summary.high > 0) {\n console.log(\n ` ${chalk.redBright(`\\u{1F7E0} High Risk: ${summary.high} files`)}`,\n );\n }\n if (summary.medium > 0) {\n console.log(\n ` ${chalk.yellow(`\\u{1F7E1} Medium Risk: ${summary.medium} files`)}`,\n );\n }\n console.log(\n ` ${chalk.green(`\\u{1F7E2} Low Risk: ${summary.low} files`)}`,\n );\n\n console.log(\"\");\n if (summary.critical > 0 || summary.high > 0) {\n console.log(\n chalk.gray(\n \" Recommendation: Focus code review and knowledge transfer on critical/high risk files.\",\n ),\n );\n console.log(\"\");\n }\n}\n\nfunction truncate(s: string, maxLen: number): string {\n if (s.length <= maxLen) return s;\n return s.slice(0, maxLen - 1) + \"\\u2026\";\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { HotspotResult } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateHotspotHTML(result: HotspotResult): string {\n // Only include files with activity for the scatter plot\n const activeFiles = result.files.filter((f) => f.changeFrequency > 0);\n const dataJson = JSON.stringify(\n activeFiles.map((f) => ({\n path: f.path,\n lines: f.lines,\n familiarity: f.familiarity,\n changeFrequency: f.changeFrequency,\n risk: f.risk,\n riskLevel: f.riskLevel,\n })),\n );\n\n const modeLabel =\n result.hotspotMode === \"team\" ? \"Team Hotspots\" : \"Personal Hotspots\";\n const userLabel = result.userName ? ` (${result.userName})` : \"\";\n\n 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>GitFamiliar \\u2014 ${modeLabel} \\u2014 ${result.repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n }\n #header {\n padding: 16px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 14px; color: #a0a0a0; }\n #main { display: flex; height: calc(100vh - 60px); }\n #chart { flex: 1; position: relative; }\n #sidebar {\n width: 320px;\n background: #16213e;\n border-left: 1px solid #0f3460;\n overflow-y: auto;\n padding: 16px;\n }\n #sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }\n .hotspot-item {\n padding: 8px 0;\n border-bottom: 1px solid #0f3460;\n font-size: 12px;\n }\n .hotspot-item .path { color: #e0e0e0; word-break: break-all; }\n .hotspot-item .meta { color: #888; margin-top: 2px; }\n .hotspot-item .risk-badge {\n display: inline-block;\n padding: 1px 6px;\n border-radius: 3px;\n font-size: 10px;\n font-weight: bold;\n margin-left: 4px;\n }\n .risk-critical { background: #e94560; color: white; }\n .risk-high { background: #f07040; color: white; }\n .risk-medium { background: #f5a623; color: black; }\n .risk-low { background: #27ae60; color: white; }\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 350px;\n }\n #zone-labels { position: absolute; pointer-events: none; }\n .zone-label {\n position: absolute;\n font-size: 12px;\n color: rgba(255,255,255,0.15);\n font-weight: bold;\n }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${modeLabel}${userLabel} \\u2014 ${result.repoName}</h1>\n <div class=\"info\">${result.timeWindow}-day window | ${activeFiles.length} active files | Summary: ${result.summary.critical} critical, ${result.summary.high} high</div>\n</div>\n<div id=\"main\">\n <div id=\"chart\">\n <div id=\"zone-labels\"></div>\n </div>\n <div id=\"sidebar\">\n <h3>Top Hotspots</h3>\n <div id=\"hotspot-list\"></div>\n </div>\n</div>\n<div id=\"tooltip\"></div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\nconst data = ${dataJson};\nconst margin = { top: 30, right: 30, bottom: 60, left: 70 };\n\nfunction riskColor(level) {\n switch(level) {\n case 'critical': return '#e94560';\n case 'high': return '#f07040';\n case 'medium': return '#f5a623';\n default: return '#27ae60';\n }\n}\n\nfunction render() {\n const container = document.getElementById('chart');\n const svg = container.querySelector('svg');\n if (svg) svg.remove();\n\n const width = container.offsetWidth;\n const height = container.offsetHeight;\n const innerW = width - margin.left - margin.right;\n const innerH = height - margin.top - margin.bottom;\n\n const maxFreq = d3.max(data, d => d.changeFrequency) || 1;\n\n const x = d3.scaleLinear().domain([0, 1]).range([0, innerW]);\n const y = d3.scaleLinear().domain([0, maxFreq * 1.1]).range([innerH, 0]);\n const r = d3.scaleSqrt()\n .domain([0, d3.max(data, d => d.lines) || 1])\n .range([3, 20]);\n\n const svgEl = d3.select('#chart')\n .append('svg')\n .attr('width', width)\n .attr('height', height);\n\n const g = svgEl.append('g')\n .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');\n\n // Danger zone background (top-left quadrant)\n g.append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', x(0.3))\n .attr('height', y(maxFreq * 0.3))\n .attr('fill', 'rgba(233, 69, 96, 0.06)');\n\n // X axis\n g.append('g')\n .attr('transform', 'translate(0,' + innerH + ')')\n .call(d3.axisBottom(x).ticks(5).tickFormat(d => Math.round(d * 100) + '%'))\n .selectAll('text,line,path').attr('stroke', '#555').attr('fill', '#888');\n\n svgEl.append('text')\n .attr('x', margin.left + innerW / 2)\n .attr('y', height - 10)\n .attr('text-anchor', 'middle')\n .attr('fill', '#888')\n .attr('font-size', '13px')\n .text('Familiarity \\\\u2192');\n\n // Y axis\n g.append('g')\n .call(d3.axisLeft(y).ticks(6))\n .selectAll('text,line,path').attr('stroke', '#555').attr('fill', '#888');\n\n svgEl.append('text')\n .attr('transform', 'rotate(-90)')\n .attr('x', -(margin.top + innerH / 2))\n .attr('y', 16)\n .attr('text-anchor', 'middle')\n .attr('fill', '#888')\n .attr('font-size', '13px')\n .text('Change Frequency (commits) \\\\u2192');\n\n // Zone labels\n const labels = document.getElementById('zone-labels');\n labels.innerHTML = '';\n const dangerLabel = document.createElement('div');\n dangerLabel.className = 'zone-label';\n dangerLabel.style.left = (margin.left + 8) + 'px';\n dangerLabel.style.top = (margin.top + 8) + 'px';\n dangerLabel.textContent = 'DANGER ZONE';\n dangerLabel.style.color = 'rgba(233,69,96,0.25)';\n dangerLabel.style.fontSize = '16px';\n labels.appendChild(dangerLabel);\n\n const safeLabel = document.createElement('div');\n safeLabel.className = 'zone-label';\n safeLabel.style.right = (320 + 40) + 'px';\n safeLabel.style.bottom = (margin.bottom + 16) + 'px';\n safeLabel.textContent = 'SAFE ZONE';\n safeLabel.style.color = 'rgba(39,174,96,0.2)';\n safeLabel.style.fontSize = '16px';\n labels.appendChild(safeLabel);\n\n const tooltip = document.getElementById('tooltip');\n\n // Data points\n g.selectAll('circle')\n .data(data)\n .join('circle')\n .attr('cx', d => x(d.familiarity))\n .attr('cy', d => y(d.changeFrequency))\n .attr('r', d => r(d.lines))\n .attr('fill', d => riskColor(d.riskLevel))\n .attr('opacity', 0.7)\n .attr('stroke', 'none')\n .style('cursor', 'pointer')\n .on('mouseover', function(event, d) {\n d3.select(this).attr('opacity', 1).attr('stroke', '#fff').attr('stroke-width', 2);\n tooltip.innerHTML =\n '<strong>' + d.path + '</strong>' +\n '<br>Familiarity: ' + Math.round(d.familiarity * 100) + '%' +\n '<br>Changes: ' + d.changeFrequency + ' commits' +\n '<br>Risk: ' + d.risk.toFixed(2) + ' (' + d.riskLevel + ')' +\n '<br>Lines: ' + d.lines.toLocaleString();\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mousemove', (event) => {\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n })\n .on('mouseout', function() {\n d3.select(this).attr('opacity', 0.7).attr('stroke', 'none');\n tooltip.style.display = 'none';\n });\n}\n\n// Sidebar\nfunction renderSidebar() {\n const container = document.getElementById('hotspot-list');\n const top = data.slice(0, 30);\n if (top.length === 0) {\n container.innerHTML = '<div style=\"color:#888\">No active files in time window.</div>';\n return;\n }\n let html = '';\n for (let i = 0; i < top.length; i++) {\n const f = top[i];\n const badgeClass = 'risk-' + f.riskLevel;\n html += '<div class=\"hotspot-item\">' +\n '<div class=\"path\">' + (i + 1) + '. ' + f.path +\n ' <span class=\"risk-badge ' + badgeClass + '\">' + f.riskLevel.toUpperCase() + '</span></div>' +\n '<div class=\"meta\">Fam: ' + Math.round(f.familiarity * 100) + '% | Changes: ' + f.changeFrequency + ' | Risk: ' + f.risk.toFixed(2) + '</div>' +\n '</div>';\n }\n container.innerHTML = html;\n}\n\nwindow.addEventListener('resize', render);\nrenderSidebar();\nrender();\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenHotspotHTML(\n result: HotspotResult,\n repoPath: string,\n): Promise<void> {\n const html = generateHotspotHTML(result);\n const outputPath = join(repoPath, \"gitfamiliar-hotspot.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Hotspot report generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import type { CliOptions, UnifiedData } from \"./types.js\";\nimport { computeFamiliarity } from \"./familiarity.js\";\nimport { computeTeamCoverage } from \"./team-coverage.js\";\nimport { computeHotspots } from \"./hotspot.js\";\nimport { computeMultiUser } from \"./multi-user.js\";\n\nexport async function computeUnified(\n options: CliOptions,\n): Promise<UnifiedData> {\n console.log(\"Computing unified dashboard data...\");\n\n // Run scoring for all 3 modes\n console.log(\" [1/4] Scoring (binary, authorship, weighted)...\");\n const [binary, authorship, weighted] = await Promise.all([\n computeFamiliarity({ ...options, mode: \"binary\" }),\n computeFamiliarity({ ...options, mode: \"authorship\" }),\n computeFamiliarity({ ...options, mode: \"weighted\" }),\n ]);\n\n // Team coverage\n console.log(\" [2/4] Team coverage...\");\n const coverage = await computeTeamCoverage(options);\n\n // Hotspots (personal mode for default user)\n console.log(\" [3/4] Hotspot analysis...\");\n const hotspot = await computeHotspots({\n ...options,\n hotspot: \"personal\",\n });\n\n // Multi-user comparison (all contributors)\n console.log(\" [4/4] Multi-user comparison...\");\n const multiUser = await computeMultiUser({\n ...options,\n team: true,\n });\n\n console.log(\"Done.\");\n\n return {\n repoName: binary.repoName,\n userName: binary.userName,\n scoring: { binary, authorship, weighted },\n coverage,\n hotspot,\n multiUser,\n };\n}\n","import { writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { UnifiedData } from \"../../core/types.js\";\nimport { openBrowser } from \"../../utils/open-browser.js\";\n\nfunction generateUnifiedHTML(data: UnifiedData): string {\n const scoringBinaryJson = JSON.stringify(data.scoring.binary.tree);\n const scoringAuthorshipJson = JSON.stringify(data.scoring.authorship.tree);\n const scoringWeightedJson = JSON.stringify(data.scoring.weighted.tree);\n const coverageTreeJson = JSON.stringify(data.coverage.tree);\n const coverageRiskJson = JSON.stringify(data.coverage.riskFiles);\n const hotspotJson = JSON.stringify(\n data.hotspot.files\n .filter((f) => f.changeFrequency > 0)\n .map((f) => ({\n path: f.path,\n lines: f.lines,\n familiarity: f.familiarity,\n changeFrequency: f.changeFrequency,\n risk: f.risk,\n riskLevel: f.riskLevel,\n })),\n );\n const multiUserTreeJson = JSON.stringify(data.multiUser.tree);\n const multiUserSummariesJson = JSON.stringify(data.multiUser.userSummaries);\n const multiUserNamesJson = JSON.stringify(\n data.multiUser.users.map((u) => u.name),\n );\n\n 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>GitFamiliar \\u2014 ${data.repoName}</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #1a1a2e;\n color: #e0e0e0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n height: 100vh;\n }\n #header {\n padding: 12px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n #header h1 { font-size: 18px; color: #e94560; }\n #header .info { font-size: 13px; color: #a0a0a0; }\n\n /* Tabs */\n #tabs {\n display: flex;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n padding: 0 24px;\n }\n #tabs .tab {\n padding: 10px 20px;\n cursor: pointer;\n color: #888;\n border-bottom: 2px solid transparent;\n font-size: 14px;\n transition: color 0.15s;\n }\n #tabs .tab:hover { color: #ccc; }\n #tabs .tab.active { color: #e94560; border-bottom-color: #e94560; }\n\n /* Sub-tabs (scoring modes) */\n #scoring-controls {\n display: none;\n padding: 8px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n align-items: center;\n gap: 16px;\n }\n #scoring-controls.visible { display: flex; }\n .subtab {\n padding: 5px 14px;\n cursor: pointer;\n color: #888;\n border: 1px solid #0f3460;\n border-radius: 4px;\n font-size: 12px;\n background: transparent;\n transition: all 0.15s;\n }\n .subtab:hover { color: #ccc; border-color: #555; }\n .subtab.active { color: #e94560; border-color: #e94560; background: rgba(233,69,96,0.1); }\n #weight-controls {\n display: none;\n align-items: center;\n gap: 8px;\n margin-left: 24px;\n font-size: 12px;\n color: #a0a0a0;\n }\n #weight-controls.visible { display: flex; }\n #weight-controls input[type=\"range\"] {\n width: 120px;\n accent-color: #e94560;\n }\n #weight-controls .weight-label { min-width: 36px; text-align: right; color: #e0e0e0; }\n\n /* Breadcrumb */\n #breadcrumb {\n padding: 8px 24px;\n background: #16213e;\n font-size: 13px;\n border-bottom: 1px solid #0f3460;\n display: none;\n }\n #breadcrumb.visible { display: block; }\n #breadcrumb span { cursor: pointer; color: #5eadf7; }\n #breadcrumb span:hover { text-decoration: underline; }\n #breadcrumb .sep { color: #666; margin: 0 4px; }\n\n /* Tab descriptions */\n .tab-desc {\n padding: 8px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n font-size: 12px;\n color: #888;\n display: none;\n }\n .tab-desc.visible { display: block; }\n\n /* Tab content */\n #content-area { flex: 1; position: relative; overflow: hidden; }\n .tab-content { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; }\n .tab-content.active { display: block; }\n .tab-content.with-sidebar.active { display: flex; }\n\n /* Layout with sidebar */\n .with-sidebar .viz-area { flex: 1; position: relative; height: 100%; }\n .with-sidebar .sidebar {\n width: 300px;\n height: 100%;\n background: #16213e;\n border-left: 1px solid #0f3460;\n overflow-y: auto;\n padding: 16px;\n }\n .sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }\n .sidebar .risk-file, .sidebar .hotspot-item {\n padding: 6px 0;\n border-bottom: 1px solid #0f3460;\n font-size: 12px;\n }\n .sidebar .path { color: #e0e0e0; word-break: break-all; }\n .sidebar .meta { color: #888; margin-top: 2px; }\n .risk-badge {\n display: inline-block;\n padding: 1px 6px;\n border-radius: 3px;\n font-size: 10px;\n font-weight: bold;\n margin-left: 4px;\n }\n .risk-critical { background: #e94560; color: white; }\n .risk-high { background: #f07040; color: white; }\n .risk-medium { background: #f5a623; color: black; }\n .risk-low { background: #27ae60; color: white; }\n\n /* Multi-user controls */\n #multiuser-controls {\n display: none;\n padding: 8px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n align-items: center;\n gap: 12px;\n }\n #multiuser-controls.visible { display: flex; }\n #multiuser-controls select {\n padding: 4px 12px;\n border: 1px solid #0f3460;\n background: #1a1a2e;\n color: #e0e0e0;\n border-radius: 4px;\n font-size: 13px;\n }\n #multiuser-controls label { font-size: 13px; color: #888; }\n\n /* Tooltip */\n #tooltip {\n position: absolute;\n pointer-events: none;\n background: rgba(22, 33, 62, 0.95);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px 14px;\n font-size: 13px;\n line-height: 1.6;\n display: none;\n z-index: 100;\n max-width: 350px;\n }\n\n /* Legends */\n .legend {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(22, 33, 62, 0.9);\n border: 1px solid #0f3460;\n border-radius: 6px;\n padding: 10px;\n font-size: 12px;\n display: none;\n z-index: 50;\n }\n .legend.active { display: block; }\n .legend .gradient-bar {\n width: 120px;\n height: 12px;\n background: linear-gradient(to right, #e94560, #f5a623, #27ae60);\n border-radius: 3px;\n margin: 4px 0;\n }\n .legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }\n .legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }\n .legend .swatch { width: 14px; height: 14px; border-radius: 3px; }\n\n /* Zone labels for hotspot */\n #zone-labels { position: absolute; pointer-events: none; }\n .zone-label {\n position: absolute;\n font-size: 16px;\n font-weight: bold;\n }\n</style>\n</head>\n<body>\n<div id=\"header\">\n <h1>GitFamiliar \\u2014 ${data.repoName}</h1>\n <div class=\"info\">${data.userName} | ${data.scoring.binary.totalFiles} files</div>\n</div>\n\n<div id=\"tabs\">\n <div class=\"tab active\" onclick=\"switchTab('scoring')\">Scoring</div>\n <div class=\"tab\" onclick=\"switchTab('coverage')\">Coverage</div>\n <div class=\"tab\" onclick=\"switchTab('multiuser')\">Multi-User</div>\n <div class=\"tab\" onclick=\"switchTab('hotspots')\">Hotspots</div>\n</div>\n\n<div id=\"tab-desc-scoring\" class=\"tab-desc visible\">\n Your personal familiarity with each file, based on Git history. Larger blocks = more lines of code. Color shows how well you know each file.\n</div>\n<div id=\"tab-desc-coverage\" class=\"tab-desc\">\n Team knowledge distribution: how many people have contributed to each file. Low contributor count = high bus factor risk.\n</div>\n<div id=\"tab-desc-multiuser\" class=\"tab-desc\">\n Compare familiarity scores across team members. Select a user to see the codebase colored by their knowledge.\n</div>\n<div id=\"tab-desc-hotspots\" class=\"tab-desc\">\n Files that change frequently but are poorly understood. Top-left = danger zone (high change, low familiarity).\n</div>\n\n<div id=\"scoring-controls\" class=\"visible\">\n <button class=\"subtab active\" onclick=\"switchScoringMode('binary')\">Binary</button>\n <button class=\"subtab\" onclick=\"switchScoringMode('authorship')\">Authorship</button>\n <button class=\"subtab\" onclick=\"switchScoringMode('weighted')\">Weighted</button>\n <div id=\"weight-controls\">\n <span>Blame:</span>\n <span class=\"weight-label\" id=\"blame-label\">50%</span>\n <input type=\"range\" id=\"blame-slider\" min=\"0\" max=\"100\" value=\"50\" oninput=\"onWeightChange()\">\n <span>Commit:</span>\n <span class=\"weight-label\" id=\"commit-label\">50%</span>\n </div>\n</div>\n<div id=\"scoring-mode-desc\" class=\"tab-desc visible\" style=\"padding-top:0\">\n <span id=\"mode-desc-text\">Binary: Have you ever committed to this file? Yes (green) or No (red).</span>\n</div>\n\n<div id=\"multiuser-controls\">\n <label>View as:</label>\n <select id=\"userSelect\" onchange=\"onUserChange()\"></select>\n</div>\n\n<div id=\"breadcrumb\"><span onclick=\"zoomTo('')\">root</span></div>\n\n<div id=\"content-area\">\n <div id=\"tab-scoring\" class=\"tab-content active\"></div>\n <div id=\"tab-coverage\" class=\"tab-content with-sidebar\">\n <div class=\"viz-area\" id=\"coverage-viz\"></div>\n <div class=\"sidebar\" id=\"coverage-sidebar\">\n <h3>Risk Files (0-1 contributors)</h3>\n <div id=\"risk-list\"></div>\n </div>\n </div>\n <div id=\"tab-multiuser\" class=\"tab-content\"></div>\n <div id=\"tab-hotspots\" class=\"tab-content with-sidebar\">\n <div class=\"viz-area\" id=\"hotspot-viz\">\n <div id=\"zone-labels\"></div>\n </div>\n <div class=\"sidebar\" id=\"hotspot-sidebar\">\n <h3>Top Hotspots</h3>\n <div id=\"hotspot-list\"></div>\n </div>\n </div>\n</div>\n\n<div id=\"tooltip\"></div>\n\n<!-- Legends -->\n<div class=\"legend active\" id=\"legend-scoring\">\n <div>Familiarity</div>\n <div class=\"gradient-bar\"></div>\n <div class=\"labels\"><span>0%</span><span>50%</span><span>100%</span></div>\n</div>\n<div class=\"legend\" id=\"legend-coverage\">\n <div>Contributors</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#e94560\"></div> 0\\u20131 (Risk)</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#f5a623\"></div> 2\\u20133 (Moderate)</div>\n <div class=\"row\"><div class=\"swatch\" style=\"background:#27ae60\"></div> 4+ (Safe)</div>\n</div>\n<div class=\"legend\" id=\"legend-multiuser\">\n <div>Familiarity</div>\n <div class=\"gradient-bar\"></div>\n <div class=\"labels\"><span>0%</span><span>50%</span><span>100%</span></div>\n</div>\n\n<script src=\"https://d3js.org/d3.v7.min.js\"></script>\n<script>\n// ── Data ──\nconst scoringData = {\n binary: ${scoringBinaryJson},\n authorship: ${scoringAuthorshipJson},\n weighted: ${scoringWeightedJson},\n};\nconst coverageData = ${coverageTreeJson};\nconst coverageRiskFiles = ${coverageRiskJson};\nconst hotspotData = ${hotspotJson};\nconst multiUserData = ${multiUserTreeJson};\nconst multiUserNames = ${multiUserNamesJson};\nconst multiUserSummaries = ${multiUserSummariesJson};\n\n// ── State ──\nlet activeTab = 'scoring';\nlet scoringMode = 'binary';\nlet blameWeight = 0.5;\nlet scoringPath = '';\nlet coveragePath = '';\nlet multiuserPath = '';\nlet currentUser = 0;\nconst rendered = { scoring: false, coverage: false, hotspots: false, multiuser: false };\n\n// ── Common utilities ──\nfunction scoreColor(score) {\n if (score <= 0) return '#e94560';\n if (score >= 1) return '#27ae60';\n if (score < 0.5) return d3.interpolateRgb('#e94560', '#f5a623')(score / 0.5);\n return d3.interpolateRgb('#f5a623', '#27ae60')((score - 0.5) / 0.5);\n}\n\nfunction coverageColor(count) {\n if (count <= 0) return '#e94560';\n if (count === 1) return '#d63c57';\n if (count <= 3) return '#f5a623';\n return '#27ae60';\n}\n\nfunction folderRiskColor(riskLevel) {\n switch (riskLevel) {\n case 'risk': return '#e94560';\n case 'moderate': return '#f5a623';\n default: return '#27ae60';\n }\n}\n\nfunction riskLevelColor(level) {\n switch(level) {\n case 'critical': return '#e94560';\n case 'high': return '#f07040';\n case 'medium': return '#f5a623';\n default: return '#27ae60';\n }\n}\n\nfunction findNode(node, path) {\n if (node.path === path) return node;\n if (node.children) {\n for (const child of node.children) {\n const found = findNode(child, path);\n if (found) return found;\n }\n }\n return null;\n}\n\nfunction buildHierarchy(node) {\n if (node.type === 'file') {\n return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };\n }\n return {\n name: node.path.split('/').pop() || node.path,\n data: node,\n children: (node.children || []).map(c => buildHierarchy(c)),\n };\n}\n\nfunction updateBreadcrumb(path) {\n const el = document.getElementById('breadcrumb');\n const parts = path ? path.split('/') : [];\n let html = '<span onclick=\"zoomTo(\\\\'\\\\')\">root</span>';\n let accumulated = '';\n for (const part of parts) {\n accumulated = accumulated ? accumulated + '/' + part : part;\n const p = accumulated;\n html += '<span class=\"sep\">/</span><span onclick=\"zoomTo(\\\\'' + p + '\\\\')\">' + part + '</span>';\n }\n el.innerHTML = html;\n}\n\nfunction showTooltipAt(html, event) {\n const tooltip = document.getElementById('tooltip');\n tooltip.innerHTML = html;\n tooltip.style.display = 'block';\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction moveTooltip(event) {\n const tooltip = document.getElementById('tooltip');\n tooltip.style.left = (event.pageX + 14) + 'px';\n tooltip.style.top = (event.pageY - 14) + 'px';\n}\n\nfunction hideTooltip() {\n document.getElementById('tooltip').style.display = 'none';\n}\n\nfunction truncateLabel(name, w, h) {\n if (w < 36 || h < 18) return '';\n const maxChars = Math.floor((w - 8) / 6.5);\n if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\\\u2026';\n return name;\n}\n\n// ── Tab switching ──\nconst modeDescriptions = {\n binary: 'Binary: Have you ever committed to this file? Yes (green) or No (red).',\n authorship: 'Authorship: How much of the current code did you write? Based on git blame line ownership.',\n weighted: 'Weighted: Combines blame ownership and commit history with adjustable weights. Use the sliders to tune.',\n};\n\nfunction switchTab(tab) {\n activeTab = tab;\n document.querySelectorAll('#tabs .tab').forEach((el, i) => {\n const tabs = ['scoring', 'coverage', 'multiuser', 'hotspots'];\n el.classList.toggle('active', tabs[i] === tab);\n });\n document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));\n document.getElementById('tab-' + tab).classList.add('active');\n\n // Show/hide tab descriptions\n ['scoring', 'coverage', 'multiuser', 'hotspots'].forEach(t => {\n document.getElementById('tab-desc-' + t).classList.toggle('visible', t === tab);\n });\n\n // Show/hide controls\n document.getElementById('scoring-controls').classList.toggle('visible', tab === 'scoring');\n document.getElementById('scoring-mode-desc').classList.toggle('visible', tab === 'scoring');\n document.getElementById('multiuser-controls').classList.toggle('visible', tab === 'multiuser');\n\n // Show/hide breadcrumb\n const showBreadcrumb = tab === 'scoring' || tab === 'coverage' || tab === 'multiuser';\n document.getElementById('breadcrumb').classList.toggle('visible', showBreadcrumb);\n\n // Show/hide legends\n document.getElementById('legend-scoring').classList.toggle('active', tab === 'scoring');\n document.getElementById('legend-coverage').classList.toggle('active', tab === 'coverage');\n document.getElementById('legend-multiuser').classList.toggle('active', tab === 'multiuser');\n\n // Update breadcrumb for current tab\n if (tab === 'scoring') updateBreadcrumb(scoringPath);\n else if (tab === 'coverage') updateBreadcrumb(coveragePath);\n else if (tab === 'multiuser') updateBreadcrumb(multiuserPath);\n\n // Render after a short delay so layout is computed after display change\n setTimeout(() => {\n if (!rendered[tab]) {\n rendered[tab] = true;\n if (tab === 'coverage') { renderCoverageSidebar(); renderCoverage(); }\n else if (tab === 'hotspots') { renderHotspotSidebar(); renderHotspot(); }\n else if (tab === 'multiuser') { initMultiUserSelect(); renderMultiUser(); }\n } else {\n if (tab === 'scoring') renderScoring();\n else if (tab === 'coverage') renderCoverage();\n else if (tab === 'hotspots') renderHotspot();\n else if (tab === 'multiuser') renderMultiUser();\n }\n }, 0);\n}\n\n// ── Zoom (shared across treemap tabs) ──\nfunction zoomTo(path) {\n if (activeTab === 'scoring') { scoringPath = path; renderScoring(); }\n else if (activeTab === 'coverage') { coveragePath = path; renderCoverage(); }\n else if (activeTab === 'multiuser') { multiuserPath = path; renderMultiUser(); }\n updateBreadcrumb(path);\n}\n\n// ── Layout dimensions ──\nfunction getContentHeight() {\n return document.getElementById('content-area').offsetHeight;\n}\n\n// ══════════════════════════════════════\n// ── SCORING TAB ──\n// ══════════════════════════════════════\n\nfunction switchScoringMode(mode) {\n scoringMode = mode;\n scoringPath = '';\n updateBreadcrumb('');\n document.querySelectorAll('#scoring-controls .subtab').forEach(el => {\n el.classList.toggle('active', el.textContent.toLowerCase() === mode);\n });\n document.getElementById('weight-controls').classList.toggle('visible', mode === 'weighted');\n document.getElementById('mode-desc-text').textContent = modeDescriptions[mode];\n renderScoring();\n}\n\nfunction onWeightChange() {\n const slider = document.getElementById('blame-slider');\n const bv = parseInt(slider.value);\n blameWeight = bv / 100;\n document.getElementById('blame-label').textContent = bv + '%';\n document.getElementById('commit-label').textContent = (100 - bv) + '%';\n recalcWeightedScores(scoringData.weighted, blameWeight, 1 - blameWeight);\n renderScoring();\n}\n\nfunction recalcWeightedScores(node, bw, cw) {\n if (node.type === 'file') {\n const bs = node.blameScore || 0;\n const cs = node.commitScore || 0;\n node.score = bw * bs + cw * cs;\n } else if (node.children) {\n let totalLines = 0;\n let weightedSum = 0;\n for (const child of node.children) {\n recalcWeightedScores(child, bw, cw);\n const lines = child.lines || 1;\n totalLines += lines;\n weightedSum += child.score * lines;\n }\n node.score = totalLines > 0 ? weightedSum / totalLines : 0;\n }\n}\n\nfunction renderScoring() {\n const container = document.getElementById('tab-scoring');\n container.innerHTML = '';\n const height = getContentHeight();\n const width = window.innerWidth;\n\n const treeData = scoringData[scoringMode];\n const targetNode = scoringPath ? findNode(treeData, scoringPath) : treeData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap().size([width, height]).padding(2).paddingTop(20).round(true)(root);\n\n const svg = d3.select('#tab-scoring').append('svg').attr('width', width).attr('height', height);\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g').data(nodes).join('g')\n .attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')');\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => d.data.data ? scoreColor(d.data.data.score) : '#333')\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e').attr('stroke-width', d => d.children ? 1 : 0.5).attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') { event.stopPropagation(); zoomTo(d.data.data.path); }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n let html = '<strong>' + d.data.data.path + '</strong>';\n html += '<br>Score: ' + Math.round(d.data.data.score * 100) + '%';\n html += '<br>Lines: ' + d.data.data.lines.toLocaleString();\n if (d.data.data.type === 'folder') {\n html += '<br>Files: ' + d.data.data.fileCount;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n }\n if (d.data.data.blameScore !== undefined) html += '<br>Blame: ' + Math.round(d.data.data.blameScore * 100) + '%';\n if (d.data.data.commitScore !== undefined) html += '<br>Commit: ' + Math.round(d.data.data.commitScore * 100) + '%';\n if (d.data.data.isExpired) html += '<br><span style=\"color:#e94560\">Expired</span>';\n showTooltipAt(html, event);\n })\n .on('mousemove', moveTooltip)\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n hideTooltip();\n });\n\n groups.append('text')\n .attr('x', 4).attr('y', 14).attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => truncateLabel(d.data.name || '', d.x1 - d.x0, d.y1 - d.y0));\n}\n\n// ══════════════════════════════════════\n// ── COVERAGE TAB ──\n// ══════════════════════════════════════\n\nfunction renderCoverage() {\n const vizArea = document.getElementById('coverage-viz');\n vizArea.innerHTML = '';\n let height = vizArea.offsetHeight;\n let width = vizArea.offsetWidth;\n\n if (!width || !height) {\n const contentH = document.getElementById('content-area').offsetHeight;\n width = window.innerWidth - 300;\n height = contentH;\n }\n\n const targetNode = coveragePath ? findNode(coverageData, coveragePath) : coverageData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap().size([width, height]).padding(2).paddingTop(20).round(true)(root);\n\n const svg = d3.select('#coverage-viz').append('svg').attr('width', width).attr('height', height);\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g').data(nodes).join('g')\n .attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')');\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => {\n if (!d.data.data) return '#333';\n if (d.data.data.type === 'file') return coverageColor(d.data.data.contributorCount);\n return folderRiskColor(d.data.data.riskLevel);\n })\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e').attr('stroke-width', d => d.children ? 1 : 0.5).attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') { event.stopPropagation(); zoomTo(d.data.data.path); }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n let html = '<strong>' + d.data.data.path + '</strong>';\n if (d.data.data.type === 'file') {\n html += '<br>Contributors: ' + d.data.data.contributorCount;\n if (d.data.data.contributors && d.data.data.contributors.length > 0) {\n html += '<br>' + d.data.data.contributors.slice(0, 8).join(', ');\n if (d.data.data.contributors.length > 8) html += ', ...';\n }\n html += '<br>Lines: ' + d.data.data.lines.toLocaleString();\n } else {\n html += '<br>Files: ' + d.data.data.fileCount;\n html += '<br>Avg Contributors: ' + d.data.data.avgContributors;\n html += '<br>Bus Factor: ' + d.data.data.busFactor;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n }\n showTooltipAt(html, event);\n })\n .on('mousemove', moveTooltip)\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n hideTooltip();\n });\n\n groups.append('text')\n .attr('x', 4).attr('y', 14).attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => truncateLabel(d.data.name || '', d.x1 - d.x0, d.y1 - d.y0));\n}\n\nfunction renderCoverageSidebar() {\n const container = document.getElementById('risk-list');\n if (coverageRiskFiles.length === 0) {\n container.innerHTML = '<div style=\"color:#888\">No high-risk files found.</div>';\n return;\n }\n let html = '';\n for (const f of coverageRiskFiles.slice(0, 50)) {\n const countLabel = f.contributorCount === 0 ? '0 people' : '1 person (' + f.contributors[0] + ')';\n html += '<div class=\"risk-file\"><div class=\"path\">' + f.path + '</div><div class=\"meta\">' + countLabel + '</div></div>';\n }\n if (coverageRiskFiles.length > 50) {\n html += '<div style=\"color:#888;padding:8px 0\">... and ' + (coverageRiskFiles.length - 50) + ' more</div>';\n }\n container.innerHTML = html;\n}\n\n// ══════════════════════════════════════\n// ── HOTSPOT TAB ──\n// ══════════════════════════════════════\n\nfunction renderHotspot() {\n const vizArea = document.getElementById('hotspot-viz');\n const existingSvg = vizArea.querySelector('svg');\n if (existingSvg) existingSvg.remove();\n\n let height = vizArea.offsetHeight;\n let width = vizArea.offsetWidth;\n\n // Fallback: if the element hasn't been laid out yet, calculate manually\n if (!width || !height) {\n const contentH = document.getElementById('content-area').offsetHeight;\n const totalW = window.innerWidth;\n width = totalW - 300; // subtract sidebar width\n height = contentH;\n }\n\n const margin = { top: 30, right: 30, bottom: 60, left: 70 };\n const innerW = width - margin.left - margin.right;\n const innerH = height - margin.top - margin.bottom;\n\n const maxFreq = d3.max(hotspotData, d => d.changeFrequency) || 1;\n\n const x = d3.scaleLinear().domain([0, 1]).range([0, innerW]);\n const y = d3.scaleLinear().domain([0, maxFreq * 1.1]).range([innerH, 0]);\n const r = d3.scaleSqrt().domain([0, d3.max(hotspotData, d => d.lines) || 1]).range([3, 20]);\n\n const svg = d3.select('#hotspot-viz').append('svg').attr('width', width).attr('height', height);\n const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');\n\n // Danger zone\n g.append('rect').attr('x', 0).attr('y', 0)\n .attr('width', x(0.3)).attr('height', y(maxFreq * 0.3))\n .attr('fill', 'rgba(233, 69, 96, 0.06)');\n\n // Axes\n g.append('g').attr('transform', 'translate(0,' + innerH + ')')\n .call(d3.axisBottom(x).ticks(5).tickFormat(d => Math.round(d * 100) + '%'))\n .selectAll('text,line,path').attr('stroke', '#555').attr('fill', '#888');\n\n svg.append('text').attr('x', margin.left + innerW / 2).attr('y', height - 10)\n .attr('text-anchor', 'middle').attr('fill', '#888').attr('font-size', '13px')\n .text('Familiarity \\\\u2192');\n\n g.append('g').call(d3.axisLeft(y).ticks(6))\n .selectAll('text,line,path').attr('stroke', '#555').attr('fill', '#888');\n\n svg.append('text').attr('transform', 'rotate(-90)')\n .attr('x', -(margin.top + innerH / 2)).attr('y', 16)\n .attr('text-anchor', 'middle').attr('fill', '#888').attr('font-size', '13px')\n .text('Change Frequency (commits) \\\\u2192');\n\n // Zone labels\n const labels = document.getElementById('zone-labels');\n labels.innerHTML = '';\n const dangerLabel = document.createElement('div');\n dangerLabel.className = 'zone-label';\n dangerLabel.style.left = (margin.left + 8) + 'px';\n dangerLabel.style.top = (margin.top + 8) + 'px';\n dangerLabel.textContent = 'DANGER ZONE';\n dangerLabel.style.color = 'rgba(233,69,96,0.25)';\n labels.appendChild(dangerLabel);\n\n const safeLabel = document.createElement('div');\n safeLabel.className = 'zone-label';\n safeLabel.style.right = (320 + 40) + 'px';\n safeLabel.style.bottom = (margin.bottom + 16) + 'px';\n safeLabel.textContent = 'SAFE ZONE';\n safeLabel.style.color = 'rgba(39,174,96,0.2)';\n labels.appendChild(safeLabel);\n\n // Data points\n g.selectAll('circle').data(hotspotData).join('circle')\n .attr('cx', d => x(d.familiarity))\n .attr('cy', d => y(d.changeFrequency))\n .attr('r', d => r(d.lines))\n .attr('fill', d => riskLevelColor(d.riskLevel))\n .attr('opacity', 0.7)\n .attr('stroke', 'none')\n .style('cursor', 'pointer')\n .on('mouseover', function(event, d) {\n d3.select(this).attr('opacity', 1).attr('stroke', '#fff').attr('stroke-width', 2);\n showTooltipAt(\n '<strong>' + d.path + '</strong>' +\n '<br>Familiarity: ' + Math.round(d.familiarity * 100) + '%' +\n '<br>Changes: ' + d.changeFrequency + ' commits' +\n '<br>Risk: ' + d.risk.toFixed(2) + ' (' + d.riskLevel + ')' +\n '<br>Lines: ' + d.lines.toLocaleString(),\n event\n );\n })\n .on('mousemove', moveTooltip)\n .on('mouseout', function() {\n d3.select(this).attr('opacity', 0.7).attr('stroke', 'none');\n hideTooltip();\n });\n}\n\nfunction renderHotspotSidebar() {\n const container = document.getElementById('hotspot-list');\n const top = hotspotData.slice(0, 30);\n if (top.length === 0) {\n container.innerHTML = '<div style=\"color:#888\">No active files in time window.</div>';\n return;\n }\n let html = '';\n for (let i = 0; i < top.length; i++) {\n const f = top[i];\n html += '<div class=\"hotspot-item\"><div class=\"path\">' + (i + 1) + '. ' + f.path +\n ' <span class=\"risk-badge risk-' + f.riskLevel + '\">' + f.riskLevel.toUpperCase() + '</span></div>' +\n '<div class=\"meta\">Fam: ' + Math.round(f.familiarity * 100) + '% | Changes: ' + f.changeFrequency + ' | Risk: ' + f.risk.toFixed(2) + '</div></div>';\n }\n container.innerHTML = html;\n}\n\n// ══════════════════════════════════════\n// ── MULTI-USER TAB ──\n// ══════════════════════════════════════\n\nfunction initMultiUserSelect() {\n const select = document.getElementById('userSelect');\n select.innerHTML = '';\n multiUserNames.forEach((name, i) => {\n const opt = document.createElement('option');\n opt.value = i;\n const summary = multiUserSummaries[i];\n opt.textContent = name + ' (' + Math.round(summary.overallScore * 100) + '%)';\n select.appendChild(opt);\n });\n}\n\nfunction onUserChange() {\n currentUser = parseInt(document.getElementById('userSelect').value);\n renderMultiUser();\n}\n\nfunction getUserScore(node) {\n if (!node.userScores || node.userScores.length === 0) return node.score;\n const s = node.userScores[currentUser];\n return s ? s.score : 0;\n}\n\nfunction renderMultiUser() {\n const container = document.getElementById('tab-multiuser');\n container.innerHTML = '';\n const height = getContentHeight();\n const width = window.innerWidth;\n\n const targetNode = multiuserPath ? findNode(multiUserData, multiuserPath) : multiUserData;\n if (!targetNode || !targetNode.children || targetNode.children.length === 0) return;\n\n const hierarchyData = {\n name: targetNode.path || 'root',\n children: targetNode.children.map(c => buildHierarchy(c)),\n };\n\n const root = d3.hierarchy(hierarchyData)\n .sum(d => d.value || 0)\n .sort((a, b) => (b.value || 0) - (a.value || 0));\n\n d3.treemap().size([width, height]).padding(2).paddingTop(20).round(true)(root);\n\n const svg = d3.select('#tab-multiuser').append('svg').attr('width', width).attr('height', height);\n const nodes = root.descendants().filter(d => d.depth > 0);\n\n const groups = svg.selectAll('g').data(nodes).join('g')\n .attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')');\n\n groups.append('rect')\n .attr('width', d => Math.max(0, d.x1 - d.x0))\n .attr('height', d => Math.max(0, d.y1 - d.y0))\n .attr('fill', d => d.data.data ? scoreColor(getUserScore(d.data.data)) : '#333')\n .attr('opacity', d => d.children ? 0.35 : 0.88)\n .attr('stroke', '#1a1a2e').attr('stroke-width', d => d.children ? 1 : 0.5).attr('rx', 2)\n .style('cursor', d => (d.data.data && d.data.data.type === 'folder') ? 'pointer' : 'default')\n .on('click', (event, d) => {\n if (d.data.data && d.data.data.type === 'folder') { event.stopPropagation(); zoomTo(d.data.data.path); }\n })\n .on('mouseover', function(event, d) {\n if (!d.data.data) return;\n d3.select(this).attr('opacity', d.children ? 0.5 : 1).attr('stroke', '#fff');\n let html = '<strong>' + (d.data.data.path || 'root') + '</strong>';\n if (d.data.data.userScores && d.data.data.userScores.length > 0) {\n html += '<table style=\"margin-top:6px;width:100%\">';\n d.data.data.userScores.forEach((s, i) => {\n const isCurrent = (i === currentUser);\n const style = isCurrent ? 'font-weight:bold;color:#5eadf7' : '';\n html += '<tr style=\"' + style + '\"><td>' + multiUserNames[i] + '</td><td style=\"text-align:right\">' + Math.round(s.score * 100) + '%</td></tr>';\n });\n html += '</table>';\n }\n if (d.data.data.type === 'folder') {\n html += '<br>Files: ' + d.data.data.fileCount;\n html += '<br><em style=\"color:#5eadf7\">Click to drill down \\\\u25B6</em>';\n } else {\n html += '<br>Lines: ' + d.data.data.lines.toLocaleString();\n }\n showTooltipAt(html, event);\n })\n .on('mousemove', moveTooltip)\n .on('mouseout', function(event, d) {\n d3.select(this).attr('opacity', d.children ? 0.35 : 0.88).attr('stroke', '#1a1a2e');\n hideTooltip();\n });\n\n groups.append('text')\n .attr('x', 4).attr('y', 14).attr('fill', '#fff')\n .attr('font-size', d => d.children ? '11px' : '10px')\n .attr('font-weight', d => d.children ? 'bold' : 'normal')\n .style('pointer-events', 'none')\n .text(d => truncateLabel(d.data.name || '', d.x1 - d.x0, d.y1 - d.y0));\n}\n\n// ── Init ──\nrendered.scoring = true;\nrenderScoring();\nwindow.addEventListener('resize', () => {\n if (activeTab === 'scoring') renderScoring();\n else if (activeTab === 'coverage') renderCoverage();\n else if (activeTab === 'hotspots') renderHotspot();\n else if (activeTab === 'multiuser') renderMultiUser();\n});\n</script>\n</body>\n</html>`;\n}\n\nexport async function generateAndOpenUnifiedHTML(\n data: UnifiedData,\n repoPath: string,\n): Promise<void> {\n const html = generateUnifiedHTML(data);\n const outputPath = join(repoPath, \"gitfamiliar-dashboard.html\");\n writeFileSync(outputPath, html, \"utf-8\");\n console.log(`Dashboard generated: ${outputPath}`);\n await openBrowser(outputPath);\n}\n","import { createProgram } from '../src/cli/index.js';\n\nconst program = createProgram();\nprogram.parse();\n"],"mappings":"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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmmBnD;AAEA,eAAsB,2BACpB,MACA,UACe;AACf,QAAM,OAAO,oBAAoB,IAAI;AACrC,QAAM,aAAaC,MAAK,UAAU,4BAA4B;AAC9D,EAAAC,eAAc,YAAY,MAAM,OAAO;AACvC,UAAQ,IAAI,wBAAwB,UAAU,EAAE;AAChD,QAAM,YAAY,UAAU;AAC9B;;;AlBt7BA,SAAS,QAAQ,OAAe,UAA8B;AAC5D,SAAO,SAAS,OAAO,CAAC,KAAK,CAAC;AAChC;AAEO,SAAS,gBAAyB;AACvC,QAAMC,WAAU,IAAI,QAAQ;AAE5B,EAAAA,SACG,KAAK,aAAa,EAClB,YAAY,kDAAkD,EAC9D,QAAQ,OAAO,EACf;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,IACA,CAAC;AAAA,EACH,EACC;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,UAAU,gCAAgC,KAAK,EACtD;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,UAAU,4BAA4B,KAAK,EAClD;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC,OAAO,oBAAoB,8CAA8C,EACzE;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,OAAO,eAAe;AAC5B,QAAI;AACF,YAAM,WAAW,QAAQ,IAAI;AAC7B,YAAM,UAAU,aAAa,YAAY,QAAQ;AAGjD,YAAM,mBACJ,QAAQ,QACP,MAAM,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,SAAS;AACxD,UACE,QAAQ,QACR,CAAC,QAAQ,WACT,CAAC,QAAQ,gBACT,CAAC,kBACD;AACA,cAAM,OAAO,MAAM,eAAe,OAAO;AACzC,cAAM,2BAA2B,MAAM,QAAQ;AAC/C;AAAA,MACF;AAGA,UAAI,QAAQ,SAAS;AACnB,cAAMC,UAAS,MAAM,gBAAgB,OAAO;AAC5C,YAAI,QAAQ,MAAM;AAChB,gBAAM,2BAA2BA,SAAQ,QAAQ;AAAA,QACnD,OAAO;AACL,gCAAsBA,OAAM;AAAA,QAC9B;AACA;AAAA,MACF;AAGA,UAAI,QAAQ,cAAc;AACxB,cAAMA,UAAS,MAAM,oBAAoB,OAAO;AAChD,YAAI,QAAQ,MAAM;AAChB,gBAAM,4BAA4BA,SAAQ,QAAQ;AAAA,QACpD,OAAO;AACL,iCAAuBA,OAAM;AAAA,QAC/B;AACA;AAAA,MACF;AAGA,YAAM,cACJ,QAAQ,QACP,MAAM,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,SAAS;AACxD,UAAI,aAAa;AACf,cAAMA,UAAS,MAAM,iBAAiB,OAAO;AAC7C,YAAI,QAAQ,MAAM;AAChB,gBAAM,6BAA6BA,SAAQ,QAAQ;AAAA,QACrD,OAAO;AACL,kCAAwBA,OAAM;AAAA,QAChC;AACA;AAAA,MACF;AAGA,YAAM,SAAS,MAAM,mBAAmB,OAAO;AAC/C,UAAI,QAAQ,MAAM;AAChB,cAAM,oBAAoB,QAAQ,QAAQ;AAAA,MAC5C,OAAO;AACL,uBAAe,MAAM;AAAA,MACvB;AAAA,IACF,SAAS,OAAY;AACnB,cAAQ,MAAM,UAAU,MAAM,OAAO,EAAE;AACvC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,CAAC;AAEH,SAAOD;AACT;;;AmBhIA,IAAM,UAAU,cAAc;AAC9B,QAAQ,MAAM;","names":["files","chalk","renderFolder","writeFileSync","join","join","writeFileSync","userScores","avgScore","chalk","BAR_WIDTH","FILLED_CHAR","EMPTY_CHAR","makeBar","formatPercent","getModeLabel","renderFolder","writeFileSync","join","join","writeFileSync","COMMIT_SEP","chalk","riskBadge","riskColor","writeFileSync","join","join","writeFileSync","writeFileSync","join","join","writeFileSync","program","result"]}