gitfamiliar 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/gitfamiliar.js +69 -2
- package/dist/bin/gitfamiliar.js.map +1 -1
- package/dist/{chunk-HA6XTZPE.js → chunk-V27FX6R6.js} +80 -18
- package/dist/chunk-V27FX6R6.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-HA6XTZPE.js.map +0 -1
package/dist/bin/gitfamiliar.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
GitClient,
|
|
4
|
+
GitHubClient,
|
|
4
5
|
buildFileTree,
|
|
5
6
|
computeFamiliarity,
|
|
6
7
|
createFilter,
|
|
7
8
|
parseExpirationConfig,
|
|
8
9
|
processBatch,
|
|
10
|
+
resolveGitHubToken,
|
|
9
11
|
resolveUser,
|
|
10
12
|
walkFiles
|
|
11
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-V27FX6R6.js";
|
|
12
14
|
|
|
13
15
|
// src/cli/index.ts
|
|
14
16
|
import { Command } from "commander";
|
|
@@ -58,7 +60,9 @@ function parseOptions(raw, repoPath) {
|
|
|
58
60
|
team: raw.team || false,
|
|
59
61
|
teamCoverage: raw.teamCoverage || false,
|
|
60
62
|
hotspot,
|
|
61
|
-
window: windowDays
|
|
63
|
+
window: windowDays,
|
|
64
|
+
githubUrl: raw.githubUrl,
|
|
65
|
+
checkGithub: raw.checkGithub || false
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
function validateMode(mode) {
|
|
@@ -2145,6 +2149,58 @@ async function generateAndOpenHotspotHTML(result, repoPath) {
|
|
|
2145
2149
|
await openBrowser(outputPath);
|
|
2146
2150
|
}
|
|
2147
2151
|
|
|
2152
|
+
// src/github/check.ts
|
|
2153
|
+
async function checkGitHubConnection(repoPath, githubUrl) {
|
|
2154
|
+
const gitClient = new GitClient(repoPath);
|
|
2155
|
+
if (!await gitClient.isRepo()) {
|
|
2156
|
+
console.error("Error: Not a git repository.");
|
|
2157
|
+
process.exit(1);
|
|
2158
|
+
}
|
|
2159
|
+
const remoteUrl = await gitClient.getRemoteUrl();
|
|
2160
|
+
if (!remoteUrl) {
|
|
2161
|
+
console.error("Error: No git remote found.");
|
|
2162
|
+
process.exit(1);
|
|
2163
|
+
}
|
|
2164
|
+
console.log(`Remote URL: ${remoteUrl}`);
|
|
2165
|
+
const parsed = GitHubClient.parseRemoteUrl(remoteUrl, githubUrl);
|
|
2166
|
+
if (!parsed) {
|
|
2167
|
+
console.error("Error: Could not parse remote URL as a GitHub repository.");
|
|
2168
|
+
process.exit(1);
|
|
2169
|
+
}
|
|
2170
|
+
console.log(`Hostname: ${parsed.hostname}`);
|
|
2171
|
+
console.log(`Repository: ${parsed.owner}/${parsed.repo}`);
|
|
2172
|
+
console.log(`API Base URL: ${parsed.apiBaseUrl}`);
|
|
2173
|
+
console.log(`
|
|
2174
|
+
Resolving token for hostname: ${parsed.hostname}`);
|
|
2175
|
+
const token = resolveGitHubToken(parsed.hostname);
|
|
2176
|
+
if (!token) {
|
|
2177
|
+
console.error(
|
|
2178
|
+
`No GitHub token found.
|
|
2179
|
+
Tried:
|
|
2180
|
+
1. Environment variables: GITHUB_TOKEN, GH_TOKEN
|
|
2181
|
+
2. gh auth token --hostname ${parsed.hostname}
|
|
2182
|
+
` + (parsed.hostname !== "github.com" ? ` 3. gh auth token (default host fallback)
|
|
2183
|
+
` : "") + `
|
|
2184
|
+
Please run: gh auth login` + (parsed.hostname !== "github.com" ? ` --hostname ${parsed.hostname}` : "")
|
|
2185
|
+
);
|
|
2186
|
+
process.exit(1);
|
|
2187
|
+
}
|
|
2188
|
+
console.log(`Token: ****${token.slice(-4)}`);
|
|
2189
|
+
console.log("\nVerifying API connectivity...");
|
|
2190
|
+
try {
|
|
2191
|
+
const client = new GitHubClient(token, parsed.apiBaseUrl);
|
|
2192
|
+
const user = await client.verifyConnection();
|
|
2193
|
+
console.log(
|
|
2194
|
+
`Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`
|
|
2195
|
+
);
|
|
2196
|
+
console.log("\nGitHub connection OK.");
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
console.error(`
|
|
2199
|
+
API connection failed: ${error.message}`);
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2148
2204
|
// src/cli/index.ts
|
|
2149
2205
|
function collect(value, previous) {
|
|
2150
2206
|
return previous.concat([value]);
|
|
@@ -2178,10 +2234,21 @@ function createProgram() {
|
|
|
2178
2234
|
).option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option(
|
|
2179
2235
|
"--window <days>",
|
|
2180
2236
|
"Time window for hotspot analysis in days (default: 90)"
|
|
2237
|
+
).option(
|
|
2238
|
+
"--github-url <hostname>",
|
|
2239
|
+
"GitHub Enterprise hostname (e.g. ghe.example.com). Auto-detected from git remote if omitted."
|
|
2240
|
+
).option(
|
|
2241
|
+
"--check-github",
|
|
2242
|
+
"Verify GitHub API connectivity and show connection info",
|
|
2243
|
+
false
|
|
2181
2244
|
).action(async (rawOptions) => {
|
|
2182
2245
|
try {
|
|
2183
2246
|
const repoPath = process.cwd();
|
|
2184
2247
|
const options = parseOptions(rawOptions, repoPath);
|
|
2248
|
+
if (options.checkGithub) {
|
|
2249
|
+
await checkGitHubConnection(repoPath, options.githubUrl);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2185
2252
|
if (options.hotspot) {
|
|
2186
2253
|
const result2 = await computeHotspots(options);
|
|
2187
2254
|
if (options.html) {
|
|
@@ -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, review-coverage, 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 \"-f, --filter <filter>\",\n \"Filter mode: all, written, reviewed\",\n \"all\",\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,review (e.g., \"0.5,0.35,0.15\")',\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 =\n | \"binary\"\n | \"authorship\"\n | \"review-coverage\"\n | \"weighted\";\nexport type FilterMode = \"all\" | \"written\" | \"reviewed\";\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.35\n review: number; // default 0.15\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 filter: FilterMode;\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 isReviewed?: boolean;\n blameScore?: number;\n commitScore?: number;\n reviewScore?: 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 interface ReviewInfo {\n date: Date;\n type: \"approved\" | \"commented\" | \"changes_requested\";\n filesInPR: number;\n}\n\nexport const DEFAULT_WEIGHTS: WeightConfig = {\n blame: 0.5,\n commit: 0.35,\n review: 0.15,\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 isReviewed?: 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 reviewedCount: 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// ── CI / PR Analysis Types ──\n\nexport interface ReviewerSuggestion {\n user: UserIdentity;\n relevantFiles: string[];\n avgFamiliarity: number;\n}\n\nexport interface PRAnalysisResult {\n prNumber: number;\n author: string;\n changedFiles: string[];\n familiarityScores: Map<string, number>;\n unfamiliarFiles: string[];\n suggestedReviewers: ReviewerSuggestion[];\n riskLevel: RiskLevel;\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 FilterMode,\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 filter?: 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 const filter = validateFilter(raw.filter || \"all\");\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 filter,\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[] = [\n \"binary\",\n \"authorship\",\n \"review-coverage\",\n \"weighted\",\n ];\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 validateFilter(filter: string): FilterMode {\n const valid: FilterMode[] = [\"all\", \"written\", \"reviewed\"];\n if (!valid.includes(filter as FilterMode)) {\n throw new Error(\n `Invalid filter: \"${filter}\". Valid filters: ${valid.join(\", \")}`,\n );\n }\n return filter as FilterMode;\n}\n\nfunction parseWeights(s: string): WeightConfig {\n const parts = s.split(\",\").map(Number);\n if (parts.length !== 3 || parts.some(isNaN)) {\n throw new Error(\n `Invalid weights: \"${s}\". Expected format: \"0.5,0.35,0.15\"`,\n );\n }\n const sum = parts[0] + parts[1] + parts[2];\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], review: parts[2] };\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': return 'Binary mode';\n case 'authorship': return 'Authorship mode';\n case 'review-coverage': return 'Review Coverage mode';\n case 'weighted': return 'Weighted mode';\n default: 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(chalk.bold(`GitFamiliar \\u2014 ${repoName} (${getModeLabel(mode)})`));\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, reviewedCount, bothCount } = result;\n console.log(\n `Written: ${writtenCount} files | Reviewed: ${reviewedCount} files | Both: ${bothCount} files`,\n );\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 #controls {\n padding: 8px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n gap: 12px;\n align-items: center;\n }\n #controls button {\n padding: 4px 12px;\n border: 1px solid #0f3460;\n background: #1a1a2e;\n color: #e0e0e0;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n }\n #controls button.active {\n background: #e94560;\n border-color: #e94560;\n color: white;\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 mode === \"binary\"\n ? `\n<div id=\"controls\">\n <span style=\"font-size:12px;color:#888;\">Filter:</span>\n <button class=\"active\" onclick=\"setFilter('all')\">All</button>\n <button onclick=\"setFilter('written')\">Written only</button>\n <button onclick=\"setFilter('reviewed')\">Reviewed only</button>\n</div>`\n : \"\"\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 currentFilter = 'all';\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 if (mode !== 'binary') return node.score;\n if (currentFilter === 'written') return node.isWritten ? 1 : 0;\n if (currentFilter === 'reviewed') return node.isReviewed ? 1 : 0;\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 controlsEl = document.getElementById('controls');\n const controlsH = controlsEl ? controlsEl.offsetHeight : 0;\n const width = window.innerWidth;\n const height = window.innerHeight - headerH - breadcrumbH - controlsH;\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 if (data.reviewScore !== undefined) {\n html += '<br>Review: ' + Math.round(data.reviewScore * 100) + '%';\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\nfunction setFilter(f) {\n currentFilter = f;\n document.querySelectorAll('#controls button').forEach(btn => {\n btn.classList.toggle('active', btn.textContent.toLowerCase().includes(f));\n });\n render();\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 reviewedCount: r.result.reviewedCount,\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 isReviewed: file.isReviewed,\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\": return \"Binary mode\";\n case \"authorship\": return \"Authorship mode\";\n case \"review-coverage\": return \"Review Coverage mode\";\n case \"weighted\": return \"Weighted mode\";\n default: 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 + summary.reviewedCount}/${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(\n chalk.bold(\"Folders:\") + \" \".repeat(nameWidth - 4) + headerNames,\n );\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/github/check.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 { checkGitHubConnection } from \"../github/check.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, review-coverage, 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 \"-f, --filter <filter>\",\n \"Filter mode: all, written, reviewed\",\n \"all\",\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,review (e.g., \"0.5,0.35,0.15\")',\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 .option(\n \"--github-url <hostname>\",\n \"GitHub Enterprise hostname (e.g. ghe.example.com). Auto-detected from git remote if omitted.\",\n )\n .option(\n \"--check-github\",\n \"Verify GitHub API connectivity and show connection info\",\n false,\n )\n .action(async (rawOptions) => {\n try {\n const repoPath = process.cwd();\n const options = parseOptions(rawOptions, repoPath);\n\n // Route: check GitHub connectivity\n if (options.checkGithub) {\n await checkGitHubConnection(repoPath, options.githubUrl);\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 =\n | \"binary\"\n | \"authorship\"\n | \"review-coverage\"\n | \"weighted\";\nexport type FilterMode = \"all\" | \"written\" | \"reviewed\";\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.35\n review: number; // default 0.15\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 filter: FilterMode;\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 githubUrl?: string; // GitHub Enterprise hostname override\n checkGithub?: boolean; // verify GitHub connectivity\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 isReviewed?: boolean;\n blameScore?: number;\n commitScore?: number;\n reviewScore?: 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 interface ReviewInfo {\n date: Date;\n type: \"approved\" | \"commented\" | \"changes_requested\";\n filesInPR: number;\n}\n\nexport const DEFAULT_WEIGHTS: WeightConfig = {\n blame: 0.5,\n commit: 0.35,\n review: 0.15,\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 isReviewed?: 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 reviewedCount: 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// ── CI / PR Analysis Types ──\n\nexport interface ReviewerSuggestion {\n user: UserIdentity;\n relevantFiles: string[];\n avgFamiliarity: number;\n}\n\nexport interface PRAnalysisResult {\n prNumber: number;\n author: string;\n changedFiles: string[];\n familiarityScores: Map<string, number>;\n unfamiliarFiles: string[];\n suggestedReviewers: ReviewerSuggestion[];\n riskLevel: RiskLevel;\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 FilterMode,\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 filter?: string;\n expiration?: string;\n html?: boolean;\n weights?: string;\n team?: boolean;\n teamCoverage?: boolean;\n hotspot?: string;\n window?: string;\n githubUrl?: string;\n checkGithub?: boolean;\n}\n\nexport function parseOptions(raw: RawCliOptions, repoPath: string): CliOptions {\n const mode = validateMode(raw.mode || \"binary\");\n const filter = validateFilter(raw.filter || \"all\");\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 filter,\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 githubUrl: raw.githubUrl,\n checkGithub: raw.checkGithub || false,\n };\n}\n\nfunction validateMode(mode: string): ScoringMode {\n const valid: ScoringMode[] = [\n \"binary\",\n \"authorship\",\n \"review-coverage\",\n \"weighted\",\n ];\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 validateFilter(filter: string): FilterMode {\n const valid: FilterMode[] = [\"all\", \"written\", \"reviewed\"];\n if (!valid.includes(filter as FilterMode)) {\n throw new Error(\n `Invalid filter: \"${filter}\". Valid filters: ${valid.join(\", \")}`,\n );\n }\n return filter as FilterMode;\n}\n\nfunction parseWeights(s: string): WeightConfig {\n const parts = s.split(\",\").map(Number);\n if (parts.length !== 3 || parts.some(isNaN)) {\n throw new Error(\n `Invalid weights: \"${s}\". Expected format: \"0.5,0.35,0.15\"`,\n );\n }\n const sum = parts[0] + parts[1] + parts[2];\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], review: parts[2] };\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': return 'Binary mode';\n case 'authorship': return 'Authorship mode';\n case 'review-coverage': return 'Review Coverage mode';\n case 'weighted': return 'Weighted mode';\n default: 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(chalk.bold(`GitFamiliar \\u2014 ${repoName} (${getModeLabel(mode)})`));\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, reviewedCount, bothCount } = result;\n console.log(\n `Written: ${writtenCount} files | Reviewed: ${reviewedCount} files | Both: ${bothCount} files`,\n );\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 #controls {\n padding: 8px 24px;\n background: #16213e;\n border-bottom: 1px solid #0f3460;\n display: flex;\n gap: 12px;\n align-items: center;\n }\n #controls button {\n padding: 4px 12px;\n border: 1px solid #0f3460;\n background: #1a1a2e;\n color: #e0e0e0;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n }\n #controls button.active {\n background: #e94560;\n border-color: #e94560;\n color: white;\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 mode === \"binary\"\n ? `\n<div id=\"controls\">\n <span style=\"font-size:12px;color:#888;\">Filter:</span>\n <button class=\"active\" onclick=\"setFilter('all')\">All</button>\n <button onclick=\"setFilter('written')\">Written only</button>\n <button onclick=\"setFilter('reviewed')\">Reviewed only</button>\n</div>`\n : \"\"\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 currentFilter = 'all';\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 if (mode !== 'binary') return node.score;\n if (currentFilter === 'written') return node.isWritten ? 1 : 0;\n if (currentFilter === 'reviewed') return node.isReviewed ? 1 : 0;\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 controlsEl = document.getElementById('controls');\n const controlsH = controlsEl ? controlsEl.offsetHeight : 0;\n const width = window.innerWidth;\n const height = window.innerHeight - headerH - breadcrumbH - controlsH;\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 if (data.reviewScore !== undefined) {\n html += '<br>Review: ' + Math.round(data.reviewScore * 100) + '%';\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\nfunction setFilter(f) {\n currentFilter = f;\n document.querySelectorAll('#controls button').forEach(btn => {\n btn.classList.toggle('active', btn.textContent.toLowerCase().includes(f));\n });\n render();\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 reviewedCount: r.result.reviewedCount,\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 isReviewed: file.isReviewed,\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\": return \"Binary mode\";\n case \"authorship\": return \"Authorship mode\";\n case \"review-coverage\": return \"Review Coverage mode\";\n case \"weighted\": return \"Weighted mode\";\n default: 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 + summary.reviewedCount}/${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(\n chalk.bold(\"Folders:\") + \" \".repeat(nameWidth - 4) + headerNames,\n );\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 { GitClient } from \"../git/client.js\";\nimport { GitHubClient } from \"./client.js\";\nimport { resolveGitHubToken } from \"./auth.js\";\n\n/**\n * Verify GitHub API connectivity and display connection info.\n */\nexport async function checkGitHubConnection(\n repoPath: string,\n githubUrl?: string,\n): Promise<void> {\n const gitClient = new GitClient(repoPath);\n\n if (!(await gitClient.isRepo())) {\n console.error(\"Error: Not a git repository.\");\n process.exit(1);\n }\n\n // 1. Check remote URL\n const remoteUrl = await gitClient.getRemoteUrl();\n if (!remoteUrl) {\n console.error(\"Error: No git remote found.\");\n process.exit(1);\n }\n console.log(`Remote URL: ${remoteUrl}`);\n\n // 2. Parse remote to get hostname / owner / repo\n const parsed = GitHubClient.parseRemoteUrl(remoteUrl, githubUrl);\n if (!parsed) {\n console.error(\"Error: Could not parse remote URL as a GitHub repository.\");\n process.exit(1);\n }\n console.log(`Hostname: ${parsed.hostname}`);\n console.log(`Repository: ${parsed.owner}/${parsed.repo}`);\n console.log(`API Base URL: ${parsed.apiBaseUrl}`);\n\n // 3. Resolve token\n console.log(`\\nResolving token for hostname: ${parsed.hostname}`);\n const token = resolveGitHubToken(parsed.hostname);\n if (!token) {\n console.error(\n `No GitHub token found.\\n` +\n `Tried:\\n` +\n ` 1. Environment variables: GITHUB_TOKEN, GH_TOKEN\\n` +\n ` 2. gh auth token --hostname ${parsed.hostname}\\n` +\n (parsed.hostname !== \"github.com\"\n ? ` 3. gh auth token (default host fallback)\\n`\n : \"\") +\n `\\nPlease run: gh auth login` +\n (parsed.hostname !== \"github.com\"\n ? ` --hostname ${parsed.hostname}`\n : \"\"),\n );\n process.exit(1);\n }\n console.log(`Token: ****${token.slice(-4)}`);\n\n // 4. Verify API connectivity\n console.log(\"\\nVerifying API connectivity...\");\n try {\n const client = new GitHubClient(token, parsed.apiBaseUrl);\n const user = await client.verifyConnection();\n console.log(\n `Authenticated as: ${user.login}${user.name ? ` (${user.name})` : \"\"}`,\n );\n console.log(\"\\nGitHub connection OK.\");\n } catch (error: any) {\n console.error(`\\nAPI connection failed: ${error.message}`);\n process.exit(1);\n }\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"]}
|
|
@@ -648,9 +648,10 @@ async function getExpiredFiles(gitClient, files, user, config) {
|
|
|
648
648
|
// src/github/client.ts
|
|
649
649
|
var GitHubClient = class {
|
|
650
650
|
token;
|
|
651
|
-
baseUrl
|
|
652
|
-
constructor(token) {
|
|
651
|
+
baseUrl;
|
|
652
|
+
constructor(token, apiBaseUrl = "https://api.github.com") {
|
|
653
653
|
this.token = token;
|
|
654
|
+
this.baseUrl = apiBaseUrl.replace(/\/+$/, "");
|
|
654
655
|
}
|
|
655
656
|
async fetch(path) {
|
|
656
657
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -663,21 +664,62 @@ var GitHubClient = class {
|
|
|
663
664
|
});
|
|
664
665
|
if (!response.ok) {
|
|
665
666
|
if (response.status === 403) {
|
|
666
|
-
throw new Error(
|
|
667
|
+
throw new Error(
|
|
668
|
+
"GitHub API rate limit exceeded. Please wait or use a token with higher limits."
|
|
669
|
+
);
|
|
667
670
|
}
|
|
668
|
-
throw new Error(
|
|
671
|
+
throw new Error(
|
|
672
|
+
`GitHub API error: ${response.status} ${response.statusText}`
|
|
673
|
+
);
|
|
669
674
|
}
|
|
670
675
|
return response.json();
|
|
671
676
|
}
|
|
672
677
|
/**
|
|
673
|
-
*
|
|
678
|
+
* Verify API connectivity by fetching the authenticated user.
|
|
679
|
+
*/
|
|
680
|
+
async verifyConnection() {
|
|
681
|
+
const user = await this.fetch("/user");
|
|
682
|
+
return { login: user.login, name: user.name };
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Parse owner/repo/hostname from a git remote URL.
|
|
686
|
+
* Supports github.com and GitHub Enterprise hosts.
|
|
674
687
|
*/
|
|
675
|
-
static parseRemoteUrl(url) {
|
|
676
|
-
let
|
|
688
|
+
static parseRemoteUrl(url, overrideHostname) {
|
|
689
|
+
let hostname;
|
|
690
|
+
let owner;
|
|
691
|
+
let repo;
|
|
692
|
+
let match = url.match(/git@([^:]+):([^/]+)\/([^/.]+)(\.git)?$/);
|
|
677
693
|
if (match) {
|
|
678
|
-
|
|
694
|
+
hostname = match[1];
|
|
695
|
+
owner = match[2];
|
|
696
|
+
repo = match[3];
|
|
697
|
+
} else if (
|
|
698
|
+
// SSH URL format: ssh://git@hostname(:port)?/owner/repo.git
|
|
699
|
+
match = url.match(
|
|
700
|
+
/ssh:\/\/[^@]+@([^:/]+)(?::\d+)?\/([^/]+)\/([^/.]+?)(\.git)?$/
|
|
701
|
+
)
|
|
702
|
+
) {
|
|
703
|
+
hostname = match[1];
|
|
704
|
+
owner = match[2];
|
|
705
|
+
repo = match[3];
|
|
706
|
+
} else if (
|
|
707
|
+
// HTTPS format: https://hostname(:port)?/owner/repo.git
|
|
708
|
+
match = url.match(
|
|
709
|
+
/https?:\/\/([^/:]+)(?::\d+)?\/([^/]+)\/([^/.]+?)(\.git)?$/
|
|
710
|
+
)
|
|
711
|
+
) {
|
|
712
|
+
hostname = match[1];
|
|
713
|
+
owner = match[2];
|
|
714
|
+
repo = match[3];
|
|
715
|
+
} else {
|
|
716
|
+
return null;
|
|
679
717
|
}
|
|
680
|
-
|
|
718
|
+
if (overrideHostname) {
|
|
719
|
+
hostname = overrideHostname;
|
|
720
|
+
}
|
|
721
|
+
const apiBaseUrl = hostname === "github.com" ? "https://api.github.com" : `https://${hostname}/api/v3`;
|
|
722
|
+
return { hostname, owner, repo, apiBaseUrl };
|
|
681
723
|
}
|
|
682
724
|
/**
|
|
683
725
|
* Get all files reviewed by a user across all PRs they reviewed.
|
|
@@ -741,29 +783,43 @@ function mapReviewState(state) {
|
|
|
741
783
|
|
|
742
784
|
// src/github/auth.ts
|
|
743
785
|
import { execSync } from "child_process";
|
|
744
|
-
function resolveGitHubToken() {
|
|
786
|
+
function resolveGitHubToken(hostname) {
|
|
745
787
|
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
746
788
|
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
747
789
|
try {
|
|
748
|
-
const
|
|
790
|
+
const host = hostname || "github.com";
|
|
791
|
+
const token = execSync(`gh auth token --hostname ${host}`, {
|
|
792
|
+
encoding: "utf-8",
|
|
793
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
794
|
+
}).trim();
|
|
749
795
|
if (token) return token;
|
|
750
796
|
} catch {
|
|
751
797
|
}
|
|
798
|
+
if (hostname && hostname !== "github.com") {
|
|
799
|
+
try {
|
|
800
|
+
const token = execSync("gh auth token", {
|
|
801
|
+
encoding: "utf-8",
|
|
802
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
803
|
+
}).trim();
|
|
804
|
+
if (token) return token;
|
|
805
|
+
} catch {
|
|
806
|
+
}
|
|
807
|
+
}
|
|
752
808
|
return null;
|
|
753
809
|
}
|
|
754
810
|
|
|
755
811
|
// src/github/reviews.ts
|
|
756
|
-
async function fetchReviewData(gitClient, username) {
|
|
757
|
-
const token = resolveGitHubToken();
|
|
758
|
-
if (!token) return null;
|
|
812
|
+
async function fetchReviewData(gitClient, username, githubUrl) {
|
|
759
813
|
const remoteUrl = await gitClient.getRemoteUrl();
|
|
760
814
|
if (!remoteUrl) return null;
|
|
761
|
-
const parsed = GitHubClient.parseRemoteUrl(remoteUrl);
|
|
815
|
+
const parsed = GitHubClient.parseRemoteUrl(remoteUrl, githubUrl);
|
|
762
816
|
if (!parsed) return null;
|
|
817
|
+
const token = resolveGitHubToken(parsed.hostname);
|
|
818
|
+
if (!token) return null;
|
|
763
819
|
if (!username) return null;
|
|
764
820
|
const ghUsername = username;
|
|
765
821
|
try {
|
|
766
|
-
const githubClient = new GitHubClient(token);
|
|
822
|
+
const githubClient = new GitHubClient(token, parsed.apiBaseUrl);
|
|
767
823
|
const reviewedFiles = await githubClient.getReviewedFiles(
|
|
768
824
|
parsed.owner,
|
|
769
825
|
parsed.repo,
|
|
@@ -792,7 +848,11 @@ async function computeFamiliarity(options) {
|
|
|
792
848
|
let reviewData;
|
|
793
849
|
let reviewedFileSet = /* @__PURE__ */ new Set();
|
|
794
850
|
if (options.mode !== "authorship") {
|
|
795
|
-
const reviewResult = await fetchReviewData(
|
|
851
|
+
const reviewResult = await fetchReviewData(
|
|
852
|
+
gitClient,
|
|
853
|
+
userFlag,
|
|
854
|
+
options.githubUrl
|
|
855
|
+
);
|
|
796
856
|
if (reviewResult) {
|
|
797
857
|
reviewData = reviewResult.reviewedFiles;
|
|
798
858
|
reviewedFileSet = reviewResult.reviewedFileSet;
|
|
@@ -869,6 +929,8 @@ export {
|
|
|
869
929
|
walkFiles,
|
|
870
930
|
processBatch,
|
|
871
931
|
parseExpirationConfig,
|
|
932
|
+
GitHubClient,
|
|
933
|
+
resolveGitHubToken,
|
|
872
934
|
computeFamiliarity
|
|
873
935
|
};
|
|
874
|
-
//# sourceMappingURL=chunk-
|
|
936
|
+
//# sourceMappingURL=chunk-V27FX6R6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/git/client.ts","../src/git/identity.ts","../src/filter/ignore.ts","../src/filter/defaults.ts","../src/utils/line-count.ts","../src/core/file-tree.ts","../src/git/log.ts","../src/scoring/binary.ts","../src/git/blame.ts","../src/utils/batch.ts","../src/scoring/authorship.ts","../src/scoring/review-coverage.ts","../src/utils/math.ts","../src/scoring/weighted.ts","../src/git/diff.ts","../src/scoring/expiration.ts","../src/github/client.ts","../src/github/auth.ts","../src/github/reviews.ts","../src/core/familiarity.ts"],"sourcesContent":["import simpleGit, { type SimpleGit } from 'simple-git';\n\nexport class GitClient {\n private git: SimpleGit;\n readonly repoPath: string;\n\n constructor(repoPath: string) {\n this.repoPath = repoPath;\n this.git = simpleGit(repoPath);\n }\n\n async isRepo(): Promise<boolean> {\n return this.git.checkIsRepo();\n }\n\n async getRepoRoot(): Promise<string> {\n return (await this.git.revparse(['--show-toplevel'])).trim();\n }\n\n async getRepoName(): Promise<string> {\n const root = await this.getRepoRoot();\n return root.split('/').pop() || 'unknown';\n }\n\n async listFiles(): Promise<string[]> {\n const result = await this.git.raw(['ls-files']);\n return result.trim().split('\\n').filter(Boolean);\n }\n\n async getUserName(): Promise<string> {\n return (await this.git.raw(['config', 'user.name'])).trim();\n }\n\n async getUserEmail(): Promise<string> {\n return (await this.git.raw(['config', 'user.email'])).trim();\n }\n\n async getLog(args: string[]): Promise<string> {\n return this.git.raw(['log', ...args]);\n }\n\n async blame(filePath: string, options: string[] = []): Promise<string> {\n return this.git.raw(['blame', ...options, '--', filePath]);\n }\n\n async diff(args: string[]): Promise<string> {\n return this.git.raw(['diff', ...args]);\n }\n\n async show(args: string[]): Promise<string> {\n return this.git.raw(['show', ...args]);\n }\n\n async raw(args: string[]): Promise<string> {\n return this.git.raw(args);\n }\n\n async getRemoteUrl(): Promise<string | null> {\n try {\n const result = await this.git.raw(['remote', 'get-url', 'origin']);\n return result.trim();\n } catch {\n return null;\n }\n }\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity } from \"../core/types.js\";\n\nexport async function resolveUser(\n gitClient: GitClient,\n userFlag?: string,\n): Promise<UserIdentity> {\n if (userFlag) {\n // If it looks like an email, use it as email; otherwise as name\n if (userFlag.includes(\"@\")) {\n return { name: userFlag, email: userFlag };\n }\n return { name: userFlag, email: userFlag };\n }\n\n const name = await gitClient.getUserName();\n const email = await gitClient.getUserEmail();\n\n if (!name && !email) {\n throw new Error(\n \"Could not determine git user. Set git config user.name/user.email or use --user flag.\",\n );\n }\n\n return { name, email };\n}\n\n/**\n * Build git --author args for a user identity.\n */\nexport function getAuthorArgs(user: UserIdentity): string[] {\n return [\"--author\", user.email || user.name];\n}\n\nexport function matchesUser(\n authorName: string,\n authorEmail: string,\n user: UserIdentity,\n): boolean {\n // Match by email (primary) or name (fallback)\n if (user.email && authorEmail) {\n if (authorEmail.toLowerCase() === user.email.toLowerCase()) return true;\n }\n if (user.name && authorName) {\n if (authorName.toLowerCase() === user.name.toLowerCase()) return true;\n }\n return false;\n}\n","import ignore from 'ignore';\nimport { readFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DEFAULT_IGNORE_PATTERNS } from './defaults.js';\n\nexport type FileFilter = (filePath: string) => boolean;\n\n/**\n * Create a file filter based on .gitfamiliarignore patterns.\n * Returns a function that returns true if the file should be INCLUDED.\n */\nexport function createFilter(repoRoot: string): FileFilter {\n const ig = ignore();\n\n const ignorePath = join(repoRoot, '.gitfamiliarignore');\n\n if (existsSync(ignorePath)) {\n const content = readFileSync(ignorePath, 'utf-8');\n ig.add(content);\n } else {\n ig.add(DEFAULT_IGNORE_PATTERNS);\n }\n\n return (filePath: string) => !ig.ignores(filePath);\n}\n","export const DEFAULT_IGNORE_PATTERNS = `# Lock files\npackage-lock.json\nyarn.lock\npnpm-lock.yaml\nGemfile.lock\npoetry.lock\nCargo.lock\ncomposer.lock\n\n# Auto-generated\n*.generated.*\n*.min.js\n*.min.css\n*.map\n\n# Build outputs (if git-tracked)\ndist/\nbuild/\n.next/\n\n# Config that rarely needs understanding\n.eslintrc*\n.prettierrc*\ntsconfig.json\n`;\n","import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Count lines in a file. Returns 0 for binary or unreadable files.\n */\nexport function countLines(repoRoot: string, filePath: string): number {\n try {\n const fullPath = join(repoRoot, filePath);\n const content = readFileSync(fullPath, 'utf-8');\n if (content.length === 0) return 0;\n return content.split('\\n').length;\n } catch {\n return 0;\n }\n}\n","import type { GitClient } from '../git/client.js';\nimport type { FileFilter } from '../filter/ignore.js';\nimport type { FolderScore, FileScore, TreeNode } from './types.js';\nimport { countLines } from '../utils/line-count.js';\n\n/**\n * Build a hierarchical file tree from git-tracked files.\n */\nexport async function buildFileTree(\n gitClient: GitClient,\n filter: FileFilter,\n): Promise<FolderScore> {\n const repoRoot = await gitClient.getRepoRoot();\n const allFiles = await gitClient.listFiles();\n const filteredFiles = allFiles.filter(filter);\n\n // Build flat file scores\n const fileScores: FileScore[] = filteredFiles.map((filePath) => ({\n type: 'file' as const,\n path: filePath,\n lines: countLines(repoRoot, filePath),\n score: 0,\n }));\n\n // Build tree structure\n return buildTreeFromFiles(fileScores);\n}\n\nfunction buildTreeFromFiles(files: FileScore[]): FolderScore {\n const root: FolderScore = {\n type: 'folder',\n path: '',\n lines: 0,\n score: 0,\n fileCount: 0,\n children: [],\n };\n\n // Group files by directory path\n const folderMap = new Map<string, FolderScore>();\n folderMap.set('', root);\n\n for (const file of files) {\n const parts = file.path.split('/');\n let currentPath = '';\n\n // Ensure all ancestor folders exist\n for (let i = 0; i < parts.length - 1; i++) {\n const parentPath = currentPath;\n currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];\n\n if (!folderMap.has(currentPath)) {\n const folder: FolderScore = {\n type: 'folder',\n path: currentPath,\n lines: 0,\n score: 0,\n fileCount: 0,\n children: [],\n };\n folderMap.set(currentPath, folder);\n\n // Add to parent\n const parent = folderMap.get(parentPath)!;\n parent.children.push(folder);\n }\n }\n\n // Add file to its parent folder\n const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : '';\n const parent = folderMap.get(parentPath)!;\n parent.children.push(file);\n }\n\n // Calculate aggregate line counts and file counts\n computeAggregates(root);\n\n return root;\n}\n\nfunction computeAggregates(node: FolderScore): void {\n let totalLines = 0;\n let totalFiles = 0;\n\n for (const child of node.children) {\n if (child.type === 'file') {\n totalLines += child.lines;\n totalFiles += 1;\n } else {\n computeAggregates(child);\n totalLines += child.lines;\n totalFiles += child.fileCount;\n }\n }\n\n node.lines = totalLines;\n node.fileCount = totalFiles;\n}\n\n/**\n * Walk all files in a tree, calling the visitor on each FileScore.\n */\nexport function walkFiles(\n node: TreeNode,\n visitor: (file: FileScore) => void,\n): void {\n if (node.type === 'file') {\n visitor(node);\n } else {\n for (const child of node.children) {\n walkFiles(child, visitor);\n }\n }\n}\n\n/**\n * Recompute folder scores after file scores have been updated.\n * For binary mode: score = readCount / fileCount\n * For other modes: score = weighted average by line count\n */\nexport function recomputeFolderScores(\n node: FolderScore,\n mode: 'binary' | 'continuous',\n): void {\n let readCount = 0;\n let totalFiles = 0;\n let weightedScore = 0;\n let totalLines = 0;\n\n for (const child of node.children) {\n if (child.type === 'file') {\n totalFiles += 1;\n totalLines += child.lines;\n weightedScore += child.score * child.lines;\n if (child.score > 0) readCount += 1;\n } else {\n recomputeFolderScores(child, mode);\n totalFiles += child.fileCount;\n totalLines += child.lines;\n weightedScore += child.score * child.lines;\n readCount += child.readCount || 0;\n }\n }\n\n node.fileCount = totalFiles;\n node.readCount = readCount;\n\n if (mode === 'binary') {\n node.score = totalFiles > 0 ? readCount / totalFiles : 0;\n } else {\n node.score = totalLines > 0 ? weightedScore / totalLines : 0;\n }\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity, CommitInfo } from \"../core/types.js\";\nimport { getAuthorArgs } from \"./identity.js\";\n\n/**\n * Get the set of files that a user has committed to (any commit, any time).\n * Used for Binary mode.\n */\nexport async function getFilesCommittedByUser(\n gitClient: GitClient,\n user: UserIdentity,\n): Promise<Set<string>> {\n const files = new Set<string>();\n\n // Query by both email and name to catch alias mismatches\n const queries: string[][] = [];\n if (user.email) {\n queries.push([\"--author\", user.email]);\n }\n if (user.name && user.name !== user.email) {\n queries.push([\"--author\", user.name]);\n }\n\n for (const authorArgs of queries) {\n try {\n const output = await gitClient.getLog([\n ...authorArgs,\n \"--name-only\",\n \"--pretty=format:\",\n \"--all\",\n ]);\n for (const line of output.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed) {\n files.add(trimmed);\n }\n }\n } catch {\n // Skip if no commits found\n }\n }\n\n return files;\n}\n\n/**\n * Get detailed commit information for a specific file by a specific user.\n * Used for Weighted mode's commit_score.\n */\nexport async function getDetailedCommits(\n gitClient: GitClient,\n user: UserIdentity,\n filePath: string,\n): Promise<CommitInfo[]> {\n const commits: CommitInfo[] = [];\n\n try {\n const output = await gitClient.getLog([\n ...getAuthorArgs(user),\n \"--numstat\",\n \"--pretty=format:%H|%aI\",\n \"--\",\n filePath,\n ]);\n\n const lines = output.trim().split(\"\\n\");\n let currentHash = \"\";\n let currentDate = new Date();\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n if (trimmed.includes(\"|\")) {\n const parts = trimmed.split(\"|\");\n currentHash = parts[0];\n currentDate = new Date(parts[1]);\n } else {\n const statMatch = trimmed.match(/^(\\d+|-)\\t(\\d+|-)\\t(.+)$/);\n if (statMatch && statMatch[3] === filePath) {\n const added = statMatch[1] === \"-\" ? 0 : parseInt(statMatch[1], 10);\n const deleted = statMatch[2] === \"-\" ? 0 : parseInt(statMatch[2], 10);\n\n // Get file size at that commit\n let fileSizeAtCommit = 1;\n try {\n const content = await gitClient.show([\n `${currentHash}:${filePath}`,\n ]);\n fileSizeAtCommit = Math.max(1, content.split(\"\\n\").length);\n } catch {\n fileSizeAtCommit = Math.max(1, added);\n }\n\n commits.push({\n hash: currentHash,\n date: currentDate,\n addedLines: added,\n deletedLines: deleted,\n fileSizeAtCommit,\n });\n }\n }\n }\n } catch {\n // No commits found\n }\n\n return commits;\n}\n\n/**\n * Get the last date a user touched a specific file (commit).\n */\nexport async function getLastCommitDate(\n gitClient: GitClient,\n user: UserIdentity,\n filePath: string,\n): Promise<Date | null> {\n try {\n const output = await gitClient.getLog([\n ...getAuthorArgs(user),\n \"-1\",\n \"--pretty=format:%aI\",\n \"--\",\n filePath,\n ]);\n const trimmed = output.trim();\n if (trimmed) {\n return new Date(trimmed);\n }\n } catch {\n // No commits found\n }\n return null;\n}\n","import type { FolderScore, FilterMode } from \"../core/types.js\";\nimport { walkFiles, recomputeFolderScores } from \"../core/file-tree.js\";\n\n/**\n * Score files in binary mode (read / not read).\n */\nexport function scoreBinary(\n tree: FolderScore,\n writtenFiles: Set<string>,\n reviewedFiles: Set<string>,\n filterMode: FilterMode,\n expiredFiles?: Set<string>,\n): void {\n walkFiles(tree, (file) => {\n const isWritten = writtenFiles.has(file.path);\n const isReviewed =\n reviewedFiles.has(file.path) && !writtenFiles.has(file.path);\n const isExpired = expiredFiles?.has(file.path) ?? false;\n\n file.isWritten = isWritten;\n file.isReviewed = isReviewed;\n file.isExpired = isExpired;\n\n if (isExpired) {\n file.score = 0;\n return;\n }\n\n switch (filterMode) {\n case \"written\":\n file.score = isWritten ? 1 : 0;\n break;\n case \"reviewed\":\n file.score = isReviewed ? 1 : 0;\n break;\n case \"all\":\n default:\n file.score = isWritten || isReviewed ? 1 : 0;\n break;\n }\n });\n\n recomputeFolderScores(tree, \"binary\");\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity } from \"../core/types.js\";\nimport { matchesUser } from \"./identity.js\";\n\nexport interface BlameEntry {\n email: string;\n name: string;\n lines: number;\n}\n\nexport interface BlameResult {\n entries: BlameEntry[];\n totalLines: number;\n}\n\n/**\n * Run git blame on a file and return per-author line counts.\n * Uses -w to ignore whitespace changes.\n */\nexport async function getBlameData(\n gitClient: GitClient,\n filePath: string,\n): Promise<BlameResult> {\n const authorMap = new Map<string, BlameEntry>();\n let totalLines = 0;\n\n try {\n const output = await gitClient.blame(filePath, [\"-w\", \"--porcelain\"]);\n const lines = output.split(\"\\n\");\n\n let currentName = \"\";\n let currentEmail = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"author \")) {\n currentName = line.slice(\"author \".length).trim();\n } else if (line.startsWith(\"author-mail \")) {\n currentEmail = line\n .slice(\"author-mail \".length)\n .replace(/[<>]/g, \"\")\n .trim();\n } else if (line.startsWith(\"\\t\")) {\n // Content line - count for current author\n if (currentEmail || currentName) {\n totalLines++;\n const key = `${currentEmail}|${currentName}`;\n const existing = authorMap.get(key);\n if (existing) {\n existing.lines++;\n } else {\n authorMap.set(key, {\n email: currentEmail,\n name: currentName,\n lines: 1,\n });\n }\n }\n }\n }\n } catch {\n // Binary file or other error - return empty\n }\n\n return { entries: Array.from(authorMap.values()), totalLines };\n}\n\n/**\n * Get the number of lines authored by a specific user in a file.\n */\nexport async function getUserBlameLines(\n gitClient: GitClient,\n filePath: string,\n user: UserIdentity,\n): Promise<{ userLines: number; totalLines: number }> {\n const { entries, totalLines } = await getBlameData(gitClient, filePath);\n\n let userLines = 0;\n for (const entry of entries) {\n if (matchesUser(entry.name, entry.email, user)) {\n userLines += entry.lines;\n }\n }\n\n return { userLines, totalLines };\n}\n","const DEFAULT_BATCH_SIZE = 10;\n\n/**\n * Process items in batches with concurrent execution within each batch.\n */\nexport async function processBatch<T>(\n items: T[],\n fn: (item: T) => Promise<void>,\n batchSize: number = DEFAULT_BATCH_SIZE,\n): Promise<void> {\n for (let i = 0; i < items.length; i += batchSize) {\n const batch = items.slice(i, i + batchSize);\n await Promise.all(batch.map(fn));\n }\n}\n","import type { FolderScore, UserIdentity } from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { getUserBlameLines } from \"../git/blame.js\";\nimport { walkFiles, recomputeFolderScores } from \"../core/file-tree.js\";\nimport { processBatch } from \"../utils/batch.js\";\n\n/**\n * Score files by authorship (git blame-based).\n * score(file) = blame_lines(user) / total_lines(file)\n */\nexport async function scoreAuthorship(\n tree: FolderScore,\n gitClient: GitClient,\n user: UserIdentity,\n): Promise<void> {\n const files: Array<{ path: string; setScore: (s: number) => void }> = [];\n\n walkFiles(tree, (file) => {\n files.push({\n path: file.path,\n setScore: (s) => {\n file.score = s;\n file.blameScore = s;\n },\n });\n });\n\n await processBatch(files, async ({ path, setScore }) => {\n const { userLines, totalLines } = await getUserBlameLines(\n gitClient,\n path,\n user,\n );\n setScore(totalLines > 0 ? userLines / totalLines : 0);\n });\n\n recomputeFolderScores(tree, \"continuous\");\n}\n","import type { FolderScore, ReviewInfo } from '../core/types.js';\nimport { walkFiles, recomputeFolderScores } from '../core/file-tree.js';\n\n/**\n * Score files by review coverage.\n * Files that the user has reviewed (via PR) get score 1, others 0.\n * User's own commits are excluded.\n */\nexport function scoreReviewCoverage(\n tree: FolderScore,\n reviewedFiles: Set<string>,\n): void {\n walkFiles(tree, (file) => {\n file.isReviewed = reviewedFiles.has(file.path);\n file.score = file.isReviewed ? 1 : 0;\n });\n\n recomputeFolderScores(tree, 'binary');\n}\n","/**\n * Sigmoid function: x / (x + k)\n * Saturates contribution of a single commit to 0-1 range.\n */\nexport function sigmoid(x: number, k: number = 0.3): number {\n if (x <= 0) return 0;\n return x / (x + k);\n}\n\n/**\n * Recency decay: e^(-lambda * t)\n * Models memory decay over time.\n * @param days - days since the event\n * @param halfLife - number of days for score to halve (default 180)\n */\nexport function recencyDecay(days: number, halfLife: number = 180): number {\n if (days <= 0) return 1;\n const lambda = Math.LN2 / halfLife;\n return Math.exp(-lambda * days);\n}\n\n/**\n * Scope factor: min(1, attentionThreshold / filesInPR)\n * Models attention dilution in large PRs.\n */\nexport function scopeFactor(\n filesInPR: number,\n attentionThreshold: number = 20,\n): number {\n if (filesInPR <= 0) return 1;\n return Math.min(1, attentionThreshold / filesInPR);\n}\n\n/**\n * Normalized diff: (added + 0.5 * deleted) / fileSize\n */\nexport function normalizedDiff(\n added: number,\n deleted: number,\n fileSize: number,\n): number {\n if (fileSize <= 0) return 0;\n return (added + 0.5 * deleted) / fileSize;\n}\n\n/**\n * Calculate the number of days between two dates.\n */\nexport function daysBetween(a: Date, b: Date): number {\n const ms = Math.abs(b.getTime() - a.getTime());\n return ms / (1000 * 60 * 60 * 24);\n}\n","import type {\n FolderScore,\n UserIdentity,\n WeightConfig,\n ReviewInfo,\n CommitInfo,\n} from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { getUserBlameLines } from \"../git/blame.js\";\nimport { getDetailedCommits } from \"../git/log.js\";\nimport { walkFiles, recomputeFolderScores } from \"../core/file-tree.js\";\nimport { processBatch } from \"../utils/batch.js\";\nimport {\n sigmoid,\n recencyDecay,\n scopeFactor,\n normalizedDiff,\n daysBetween,\n} from \"../utils/math.js\";\n\nconst REVIEW_BASE_WEIGHTS: Record<string, number> = {\n approved: 0.3,\n commented: 0.15,\n changes_requested: 0.35,\n};\n\nfunction calculateCommitScore(commits: CommitInfo[], now: Date): number {\n let raw = 0;\n for (const c of commits) {\n const nd = normalizedDiff(c.addedLines, c.deletedLines, c.fileSizeAtCommit);\n raw += sigmoid(nd) * recencyDecay(daysBetween(now, c.date));\n }\n return Math.min(1, raw);\n}\n\nfunction calculateReviewScore(\n reviews: ReviewInfo[] | undefined,\n now: Date,\n): number {\n if (!reviews) return 0;\n let raw = 0;\n for (const r of reviews) {\n const baseWeight = REVIEW_BASE_WEIGHTS[r.type] || 0.15;\n raw +=\n baseWeight *\n scopeFactor(r.filesInPR) *\n recencyDecay(daysBetween(now, r.date));\n }\n return Math.min(1, raw);\n}\n\n/**\n * Score files using the weighted mode (blame + commit + review signals).\n */\nexport async function scoreWeighted(\n tree: FolderScore,\n gitClient: GitClient,\n user: UserIdentity,\n weights: WeightConfig,\n reviewData?: Map<string, ReviewInfo[]>,\n now?: Date,\n): Promise<void> {\n const currentDate = now || new Date();\n const files: Array<{\n path: string;\n setScores: (b: number, c: number, r: number, total: number) => void;\n }> = [];\n\n walkFiles(tree, (file) => {\n files.push({\n path: file.path,\n setScores: (b, c, r, total) => {\n file.blameScore = b;\n file.commitScore = c;\n file.reviewScore = r;\n file.score = total;\n },\n });\n });\n\n await processBatch(files, async ({ path, setScores }) => {\n const { userLines, totalLines } = await getUserBlameLines(\n gitClient,\n path,\n user,\n );\n const blameScore = totalLines > 0 ? userLines / totalLines : 0;\n const commitScore = calculateCommitScore(\n await getDetailedCommits(gitClient, user, path),\n currentDate,\n );\n const reviewScore = calculateReviewScore(\n reviewData?.get(path),\n currentDate,\n );\n\n const total =\n weights.blame * blameScore +\n weights.commit * commitScore +\n weights.review * reviewScore;\n\n setScores(blameScore, commitScore, reviewScore, total);\n });\n\n recomputeFolderScores(tree, \"continuous\");\n}\n","import type { GitClient } from './client.js';\n\n/**\n * Calculate how much a file has changed since a given commit.\n * Returns the ratio of changed lines to current total lines.\n * Used for change-based expiration policy.\n */\nexport async function getChangeRatio(\n gitClient: GitClient,\n filePath: string,\n sinceCommit: string,\n): Promise<number> {\n try {\n const output = await gitClient.diff([\n '--numstat',\n `${sinceCommit}..HEAD`,\n '--',\n filePath,\n ]);\n\n const trimmed = output.trim();\n if (!trimmed) return 0;\n\n const match = trimmed.match(/^(\\d+|-)\\t(\\d+|-)\\t/);\n if (!match) return 0;\n\n const added = match[1] === '-' ? 0 : parseInt(match[1], 10);\n const deleted = match[2] === '-' ? 0 : parseInt(match[2], 10);\n const changedLines = added + deleted;\n\n // Get current file line count\n const currentContent = await gitClient.show([`HEAD:${filePath}`]);\n const currentLines = Math.max(1, currentContent.split('\\n').length);\n\n return changedLines / currentLines;\n } catch {\n return 0;\n }\n}\n\n/**\n * Get the commit hash for the last time a user touched a file.\n */\nexport async function getLastTouchCommit(\n gitClient: GitClient,\n filePath: string,\n userEmail: string,\n): Promise<string | null> {\n try {\n const output = await gitClient.getLog([\n '--author', userEmail,\n '-1',\n '--pretty=format:%H',\n '--',\n filePath,\n ]);\n const hash = output.trim();\n return hash || null;\n } catch {\n return null;\n }\n}\n","import type { ExpirationConfig, UserIdentity } from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { getLastCommitDate } from \"../git/log.js\";\nimport { getLastTouchCommit, getChangeRatio } from \"../git/diff.js\";\nimport { processBatch } from \"../utils/batch.js\";\n\n/**\n * Parse an expiration string from CLI into ExpirationConfig.\n * Formats: \"never\", \"time:180d\", \"change:50%\", \"combined:365d:50%\"\n */\nexport function parseExpirationConfig(input: string): ExpirationConfig {\n if (!input || input === \"never\") {\n return { policy: \"never\" };\n }\n\n if (input.startsWith(\"time:\")) {\n const duration = parseDays(input.slice(\"time:\".length));\n return { policy: \"time\", duration };\n }\n\n if (input.startsWith(\"change:\")) {\n const threshold = parsePercentage(input.slice(\"change:\".length));\n return { policy: \"change\", threshold };\n }\n\n if (input.startsWith(\"combined:\")) {\n const parts = input.slice(\"combined:\".length).split(\":\");\n const duration = parseDays(parts[0]);\n const threshold = parts[1] ? parsePercentage(parts[1]) : 0.5;\n return { policy: \"combined\", duration, threshold };\n }\n\n return { policy: \"never\" };\n}\n\nfunction parseDays(s: string): number {\n const match = s.match(/^(\\d+)d$/);\n if (!match)\n throw new Error(\n `Invalid duration format: \"${s}\". Expected format like \"180d\".`,\n );\n return parseInt(match[1], 10);\n}\n\nfunction parsePercentage(s: string): number {\n const match = s.match(/^(\\d+)%$/);\n if (!match)\n throw new Error(\n `Invalid percentage format: \"${s}\". Expected format like \"50%\".`,\n );\n return parseInt(match[1], 10) / 100;\n}\n\n/**\n * Check if a file's familiarity has expired according to the given policy.\n */\nexport async function isExpired(\n gitClient: GitClient,\n filePath: string,\n user: UserIdentity,\n config: ExpirationConfig,\n now?: Date,\n): Promise<boolean> {\n if (config.policy === \"never\") return false;\n\n const currentDate = now || new Date();\n const email = user.email || user.name;\n\n if (config.policy === \"time\" || config.policy === \"combined\") {\n const lastTouch = await getLastCommitDate(gitClient, user, filePath);\n if (lastTouch && config.duration) {\n const daysSince =\n (currentDate.getTime() - lastTouch.getTime()) / (1000 * 60 * 60 * 24);\n if (daysSince > config.duration) return true;\n }\n }\n\n if (config.policy === \"change\" || config.policy === \"combined\") {\n const lastCommit = await getLastTouchCommit(gitClient, filePath, email);\n if (lastCommit && config.threshold) {\n const ratio = await getChangeRatio(gitClient, filePath, lastCommit);\n if (ratio > config.threshold) return true;\n }\n }\n\n return false;\n}\n\n/**\n * Get the set of expired files for a given user and config.\n */\nexport async function getExpiredFiles(\n gitClient: GitClient,\n files: string[],\n user: UserIdentity,\n config: ExpirationConfig,\n): Promise<Set<string>> {\n if (config.policy === \"never\") return new Set();\n\n const expiredSet = new Set<string>();\n\n await processBatch(files, async (filePath) => {\n if (await isExpired(gitClient, filePath, user, config)) {\n expiredSet.add(filePath);\n }\n });\n\n return expiredSet;\n}\n","import type { ReviewInfo } from \"../core/types.js\";\n\ninterface GitHubReview {\n state: string;\n submitted_at: string;\n}\n\nexport interface GitHubRemoteInfo {\n hostname: string; // e.g. \"github.com\" or \"ghe.example.com\"\n owner: string;\n repo: string;\n apiBaseUrl: string; // e.g. \"https://api.github.com\" or \"https://ghe.example.com/api/v3\"\n}\n\n/**\n * Minimal GitHub client using fetch (no external dependency).\n * Supports both github.com and GitHub Enterprise.\n */\nexport class GitHubClient {\n private token: string;\n private baseUrl: string;\n\n constructor(token: string, apiBaseUrl: string = \"https://api.github.com\") {\n this.token = token;\n this.baseUrl = apiBaseUrl.replace(/\\/+$/, \"\");\n }\n\n private async fetch(path: string): Promise<any> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: \"application/vnd.github.v3+json\",\n \"User-Agent\": \"gitfamiliar\",\n },\n });\n\n if (!response.ok) {\n if (response.status === 403) {\n throw new Error(\n \"GitHub API rate limit exceeded. Please wait or use a token with higher limits.\",\n );\n }\n throw new Error(\n `GitHub API error: ${response.status} ${response.statusText}`,\n );\n }\n\n return response.json();\n }\n\n /**\n * Verify API connectivity by fetching the authenticated user.\n */\n async verifyConnection(): Promise<{ login: string; name: string | null }> {\n const user = await this.fetch(\"/user\");\n return { login: user.login, name: user.name };\n }\n\n /**\n * Parse owner/repo/hostname from a git remote URL.\n * Supports github.com and GitHub Enterprise hosts.\n */\n static parseRemoteUrl(\n url: string,\n overrideHostname?: string,\n ): GitHubRemoteInfo | null {\n let hostname: string;\n let owner: string;\n let repo: string;\n\n // SSH format: git@hostname:owner/repo.git\n let match = url.match(/git@([^:]+):([^/]+)\\/([^/.]+)(\\.git)?$/);\n if (match) {\n hostname = match[1];\n owner = match[2];\n repo = match[3];\n } else if (\n // SSH URL format: ssh://git@hostname(:port)?/owner/repo.git\n (match = url.match(\n /ssh:\\/\\/[^@]+@([^:/]+)(?::\\d+)?\\/([^/]+)\\/([^/.]+?)(\\.git)?$/,\n ))\n ) {\n hostname = match[1];\n owner = match[2];\n repo = match[3];\n } else if (\n // HTTPS format: https://hostname(:port)?/owner/repo.git\n (match = url.match(\n /https?:\\/\\/([^/:]+)(?::\\d+)?\\/([^/]+)\\/([^/.]+?)(\\.git)?$/,\n ))\n ) {\n hostname = match[1];\n owner = match[2];\n repo = match[3];\n } else {\n return null;\n }\n\n if (overrideHostname) {\n hostname = overrideHostname;\n }\n\n const apiBaseUrl =\n hostname === \"github.com\"\n ? \"https://api.github.com\"\n : `https://${hostname}/api/v3`;\n\n return { hostname, owner, repo, apiBaseUrl };\n }\n\n /**\n * Get all files reviewed by a user across all PRs they reviewed.\n */\n async getReviewedFiles(\n owner: string,\n repo: string,\n username: string,\n ): Promise<Map<string, ReviewInfo[]>> {\n const reviewedFiles = new Map<string, ReviewInfo[]>();\n\n let page = 1;\n const perPage = 100;\n\n while (true) {\n const searchResult = await this.fetch(\n `/search/issues?q=type:pr+repo:${owner}/${repo}+reviewed-by:${username}&per_page=${perPage}&page=${page}`,\n );\n\n if (!searchResult.items || searchResult.items.length === 0) break;\n\n for (const item of searchResult.items) {\n const prNumber = item.number;\n\n const reviews: GitHubReview[] = await this.fetch(\n `/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,\n );\n\n const userReviews = reviews.filter(\n (r: any) => r.user?.login?.toLowerCase() === username.toLowerCase(),\n );\n\n if (userReviews.length === 0) continue;\n\n const prFiles: any[] = await this.fetch(\n `/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`,\n );\n\n const fileCount = prFiles.length;\n\n for (const review of userReviews) {\n const reviewType = mapReviewState(review.state);\n const reviewDate = new Date(review.submitted_at);\n\n for (const prFile of prFiles) {\n const filePath = prFile.filename;\n const info: ReviewInfo = {\n date: reviewDate,\n type: reviewType,\n filesInPR: fileCount,\n };\n\n if (reviewedFiles.has(filePath)) {\n reviewedFiles.get(filePath)!.push(info);\n } else {\n reviewedFiles.set(filePath, [info]);\n }\n }\n }\n }\n\n if (searchResult.items.length < perPage) break;\n page++;\n }\n\n return reviewedFiles;\n }\n}\n\nfunction mapReviewState(state: string): ReviewInfo[\"type\"] {\n switch (state.toUpperCase()) {\n case \"APPROVED\":\n return \"approved\";\n case \"CHANGES_REQUESTED\":\n return \"changes_requested\";\n default:\n return \"commented\";\n }\n}\n","import { execSync } from \"node:child_process\";\n\n/**\n * Resolve GitHub token from environment or gh CLI.\n * For GitHub Enterprise, pass the hostname (e.g. \"ghe.example.com\")\n * to use `gh auth token --hostname <host>`.\n */\nexport function resolveGitHubToken(hostname?: string): string | null {\n // Check environment variables\n if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;\n if (process.env.GH_TOKEN) return process.env.GH_TOKEN;\n\n // Try gh CLI (always pass --hostname for explicit host resolution)\n try {\n const host = hostname || \"github.com\";\n const token = execSync(`gh auth token --hostname ${host}`, {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n if (token) return token;\n } catch {\n // gh CLI not available or not authenticated for this hostname\n }\n\n // Fallback: try gh auth token without --hostname (uses default host)\n if (hostname && hostname !== \"github.com\") {\n try {\n const token = execSync(\"gh auth token\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n if (token) return token;\n } catch {\n // gh CLI not available or not authenticated\n }\n }\n\n return null;\n}\n","import type { ReviewInfo } from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { GitHubClient } from \"./client.js\";\nimport { resolveGitHubToken } from \"./auth.js\";\n\n/**\n * Attempt to fetch review data from GitHub.\n * Returns null if no token or not a GitHub repo.\n * Supports GitHub Enterprise by auto-detecting the hostname from git remote.\n * @param githubUrl - Optional override for GitHub hostname (e.g. \"ghe.example.com\")\n */\nexport async function fetchReviewData(\n gitClient: GitClient,\n username?: string,\n githubUrl?: string,\n): Promise<{\n reviewedFiles: Map<string, ReviewInfo[]>;\n reviewedFileSet: Set<string>;\n} | null> {\n const remoteUrl = await gitClient.getRemoteUrl();\n if (!remoteUrl) return null;\n\n const parsed = GitHubClient.parseRemoteUrl(remoteUrl, githubUrl);\n if (!parsed) return null;\n\n const token = resolveGitHubToken(parsed.hostname);\n if (!token) return null;\n\n // GitHub username is required for review API queries\n if (!username) return null;\n const ghUsername = username;\n\n try {\n const githubClient = new GitHubClient(token, parsed.apiBaseUrl);\n const reviewedFiles = await githubClient.getReviewedFiles(\n parsed.owner,\n parsed.repo,\n ghUsername,\n );\n\n const reviewedFileSet = new Set(reviewedFiles.keys());\n\n return { reviewedFiles, reviewedFileSet };\n } catch {\n return null;\n }\n}\n","import type { CliOptions, FolderScore, ReviewInfo } from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { resolveUser } from \"../git/identity.js\";\nimport { createFilter } from \"../filter/ignore.js\";\nimport { buildFileTree, walkFiles } from \"./file-tree.js\";\nimport { getFilesCommittedByUser } from \"../git/log.js\";\nimport { scoreBinary } from \"../scoring/binary.js\";\nimport { scoreAuthorship } from \"../scoring/authorship.js\";\nimport { scoreReviewCoverage } from \"../scoring/review-coverage.js\";\nimport { scoreWeighted } from \"../scoring/weighted.js\";\nimport { getExpiredFiles } from \"../scoring/expiration.js\";\nimport { fetchReviewData } from \"../github/reviews.js\";\n\nexport interface FamiliarityResult {\n tree: FolderScore;\n repoName: string;\n userName: string;\n mode: string;\n writtenCount: number;\n reviewedCount: number;\n bothCount: number;\n totalFiles: number;\n}\n\nexport async function computeFamiliarity(\n options: CliOptions,\n): Promise<FamiliarityResult> {\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 userFlag = Array.isArray(options.user) ? options.user[0] : options.user;\n const user = await resolveUser(gitClient, userFlag);\n const filter = createFilter(repoRoot);\n const tree = await buildFileTree(gitClient, filter);\n\n // Get written files (used by binary, weighted)\n const writtenFiles = await getFilesCommittedByUser(gitClient, user);\n\n // Get review data (if available)\n let reviewData: Map<string, ReviewInfo[]> | undefined;\n let reviewedFileSet = new Set<string>();\n\n if (options.mode !== \"authorship\") {\n const reviewResult = await fetchReviewData(\n gitClient,\n userFlag,\n options.githubUrl,\n );\n if (reviewResult) {\n reviewData = reviewResult.reviewedFiles;\n reviewedFileSet = reviewResult.reviewedFileSet;\n }\n }\n\n // Get expired files\n let expiredFiles: Set<string> | undefined;\n if (options.expiration.policy !== \"never\") {\n const allFiles: string[] = [];\n walkFiles(tree, (f) => allFiles.push(f.path));\n expiredFiles = await getExpiredFiles(\n gitClient,\n allFiles,\n user,\n options.expiration,\n );\n }\n\n // Score based on mode\n switch (options.mode) {\n case \"binary\":\n scoreBinary(\n tree,\n writtenFiles,\n reviewedFileSet,\n options.filter,\n expiredFiles,\n );\n break;\n\n case \"authorship\":\n await scoreAuthorship(tree, gitClient, user);\n break;\n\n case \"review-coverage\":\n if (reviewedFileSet.size === 0) {\n console.error(\n \"Warning: No review data available. Set GITHUB_TOKEN or use --user with your GitHub username.\",\n );\n }\n scoreReviewCoverage(tree, reviewedFileSet);\n break;\n\n case \"weighted\":\n await scoreWeighted(tree, gitClient, user, options.weights, reviewData);\n break;\n }\n\n return {\n tree,\n repoName,\n userName: user.name || user.email,\n mode: options.mode,\n ...computeSummary(tree, writtenFiles, reviewedFileSet),\n totalFiles: tree.fileCount,\n };\n}\n\nfunction computeSummary(\n tree: FolderScore,\n writtenFiles: Set<string>,\n reviewedFileSet: Set<string>,\n): { writtenCount: number; reviewedCount: number; bothCount: number } {\n let writtenOnly = 0;\n let reviewedOnly = 0;\n let both = 0;\n\n walkFiles(tree, (file) => {\n const w = writtenFiles.has(file.path);\n const r = reviewedFileSet.has(file.path);\n if (w && r) both++;\n else if (w) writtenOnly++;\n else if (r) reviewedOnly++;\n });\n\n return {\n writtenCount: writtenOnly + both,\n reviewedCount: reviewedOnly + both,\n bothCount: both,\n };\n}\n"],"mappings":"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","names":["readFileSync","join","parentPath","parent","isExpired"]}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/git/client.ts","../src/git/identity.ts","../src/filter/ignore.ts","../src/filter/defaults.ts","../src/utils/line-count.ts","../src/core/file-tree.ts","../src/git/log.ts","../src/scoring/binary.ts","../src/git/blame.ts","../src/utils/batch.ts","../src/scoring/authorship.ts","../src/scoring/review-coverage.ts","../src/utils/math.ts","../src/scoring/weighted.ts","../src/git/diff.ts","../src/scoring/expiration.ts","../src/github/client.ts","../src/github/auth.ts","../src/github/reviews.ts","../src/core/familiarity.ts"],"sourcesContent":["import simpleGit, { type SimpleGit } from 'simple-git';\n\nexport class GitClient {\n private git: SimpleGit;\n readonly repoPath: string;\n\n constructor(repoPath: string) {\n this.repoPath = repoPath;\n this.git = simpleGit(repoPath);\n }\n\n async isRepo(): Promise<boolean> {\n return this.git.checkIsRepo();\n }\n\n async getRepoRoot(): Promise<string> {\n return (await this.git.revparse(['--show-toplevel'])).trim();\n }\n\n async getRepoName(): Promise<string> {\n const root = await this.getRepoRoot();\n return root.split('/').pop() || 'unknown';\n }\n\n async listFiles(): Promise<string[]> {\n const result = await this.git.raw(['ls-files']);\n return result.trim().split('\\n').filter(Boolean);\n }\n\n async getUserName(): Promise<string> {\n return (await this.git.raw(['config', 'user.name'])).trim();\n }\n\n async getUserEmail(): Promise<string> {\n return (await this.git.raw(['config', 'user.email'])).trim();\n }\n\n async getLog(args: string[]): Promise<string> {\n return this.git.raw(['log', ...args]);\n }\n\n async blame(filePath: string, options: string[] = []): Promise<string> {\n return this.git.raw(['blame', ...options, '--', filePath]);\n }\n\n async diff(args: string[]): Promise<string> {\n return this.git.raw(['diff', ...args]);\n }\n\n async show(args: string[]): Promise<string> {\n return this.git.raw(['show', ...args]);\n }\n\n async raw(args: string[]): Promise<string> {\n return this.git.raw(args);\n }\n\n async getRemoteUrl(): Promise<string | null> {\n try {\n const result = await this.git.raw(['remote', 'get-url', 'origin']);\n return result.trim();\n } catch {\n return null;\n }\n }\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity } from \"../core/types.js\";\n\nexport async function resolveUser(\n gitClient: GitClient,\n userFlag?: string,\n): Promise<UserIdentity> {\n if (userFlag) {\n // If it looks like an email, use it as email; otherwise as name\n if (userFlag.includes(\"@\")) {\n return { name: userFlag, email: userFlag };\n }\n return { name: userFlag, email: userFlag };\n }\n\n const name = await gitClient.getUserName();\n const email = await gitClient.getUserEmail();\n\n if (!name && !email) {\n throw new Error(\n \"Could not determine git user. Set git config user.name/user.email or use --user flag.\",\n );\n }\n\n return { name, email };\n}\n\n/**\n * Build git --author args for a user identity.\n */\nexport function getAuthorArgs(user: UserIdentity): string[] {\n return [\"--author\", user.email || user.name];\n}\n\nexport function matchesUser(\n authorName: string,\n authorEmail: string,\n user: UserIdentity,\n): boolean {\n // Match by email (primary) or name (fallback)\n if (user.email && authorEmail) {\n if (authorEmail.toLowerCase() === user.email.toLowerCase()) return true;\n }\n if (user.name && authorName) {\n if (authorName.toLowerCase() === user.name.toLowerCase()) return true;\n }\n return false;\n}\n","import ignore from 'ignore';\nimport { readFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DEFAULT_IGNORE_PATTERNS } from './defaults.js';\n\nexport type FileFilter = (filePath: string) => boolean;\n\n/**\n * Create a file filter based on .gitfamiliarignore patterns.\n * Returns a function that returns true if the file should be INCLUDED.\n */\nexport function createFilter(repoRoot: string): FileFilter {\n const ig = ignore();\n\n const ignorePath = join(repoRoot, '.gitfamiliarignore');\n\n if (existsSync(ignorePath)) {\n const content = readFileSync(ignorePath, 'utf-8');\n ig.add(content);\n } else {\n ig.add(DEFAULT_IGNORE_PATTERNS);\n }\n\n return (filePath: string) => !ig.ignores(filePath);\n}\n","export const DEFAULT_IGNORE_PATTERNS = `# Lock files\npackage-lock.json\nyarn.lock\npnpm-lock.yaml\nGemfile.lock\npoetry.lock\nCargo.lock\ncomposer.lock\n\n# Auto-generated\n*.generated.*\n*.min.js\n*.min.css\n*.map\n\n# Build outputs (if git-tracked)\ndist/\nbuild/\n.next/\n\n# Config that rarely needs understanding\n.eslintrc*\n.prettierrc*\ntsconfig.json\n`;\n","import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Count lines in a file. Returns 0 for binary or unreadable files.\n */\nexport function countLines(repoRoot: string, filePath: string): number {\n try {\n const fullPath = join(repoRoot, filePath);\n const content = readFileSync(fullPath, 'utf-8');\n if (content.length === 0) return 0;\n return content.split('\\n').length;\n } catch {\n return 0;\n }\n}\n","import type { GitClient } from '../git/client.js';\nimport type { FileFilter } from '../filter/ignore.js';\nimport type { FolderScore, FileScore, TreeNode } from './types.js';\nimport { countLines } from '../utils/line-count.js';\n\n/**\n * Build a hierarchical file tree from git-tracked files.\n */\nexport async function buildFileTree(\n gitClient: GitClient,\n filter: FileFilter,\n): Promise<FolderScore> {\n const repoRoot = await gitClient.getRepoRoot();\n const allFiles = await gitClient.listFiles();\n const filteredFiles = allFiles.filter(filter);\n\n // Build flat file scores\n const fileScores: FileScore[] = filteredFiles.map((filePath) => ({\n type: 'file' as const,\n path: filePath,\n lines: countLines(repoRoot, filePath),\n score: 0,\n }));\n\n // Build tree structure\n return buildTreeFromFiles(fileScores);\n}\n\nfunction buildTreeFromFiles(files: FileScore[]): FolderScore {\n const root: FolderScore = {\n type: 'folder',\n path: '',\n lines: 0,\n score: 0,\n fileCount: 0,\n children: [],\n };\n\n // Group files by directory path\n const folderMap = new Map<string, FolderScore>();\n folderMap.set('', root);\n\n for (const file of files) {\n const parts = file.path.split('/');\n let currentPath = '';\n\n // Ensure all ancestor folders exist\n for (let i = 0; i < parts.length - 1; i++) {\n const parentPath = currentPath;\n currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];\n\n if (!folderMap.has(currentPath)) {\n const folder: FolderScore = {\n type: 'folder',\n path: currentPath,\n lines: 0,\n score: 0,\n fileCount: 0,\n children: [],\n };\n folderMap.set(currentPath, folder);\n\n // Add to parent\n const parent = folderMap.get(parentPath)!;\n parent.children.push(folder);\n }\n }\n\n // Add file to its parent folder\n const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : '';\n const parent = folderMap.get(parentPath)!;\n parent.children.push(file);\n }\n\n // Calculate aggregate line counts and file counts\n computeAggregates(root);\n\n return root;\n}\n\nfunction computeAggregates(node: FolderScore): void {\n let totalLines = 0;\n let totalFiles = 0;\n\n for (const child of node.children) {\n if (child.type === 'file') {\n totalLines += child.lines;\n totalFiles += 1;\n } else {\n computeAggregates(child);\n totalLines += child.lines;\n totalFiles += child.fileCount;\n }\n }\n\n node.lines = totalLines;\n node.fileCount = totalFiles;\n}\n\n/**\n * Walk all files in a tree, calling the visitor on each FileScore.\n */\nexport function walkFiles(\n node: TreeNode,\n visitor: (file: FileScore) => void,\n): void {\n if (node.type === 'file') {\n visitor(node);\n } else {\n for (const child of node.children) {\n walkFiles(child, visitor);\n }\n }\n}\n\n/**\n * Recompute folder scores after file scores have been updated.\n * For binary mode: score = readCount / fileCount\n * For other modes: score = weighted average by line count\n */\nexport function recomputeFolderScores(\n node: FolderScore,\n mode: 'binary' | 'continuous',\n): void {\n let readCount = 0;\n let totalFiles = 0;\n let weightedScore = 0;\n let totalLines = 0;\n\n for (const child of node.children) {\n if (child.type === 'file') {\n totalFiles += 1;\n totalLines += child.lines;\n weightedScore += child.score * child.lines;\n if (child.score > 0) readCount += 1;\n } else {\n recomputeFolderScores(child, mode);\n totalFiles += child.fileCount;\n totalLines += child.lines;\n weightedScore += child.score * child.lines;\n readCount += child.readCount || 0;\n }\n }\n\n node.fileCount = totalFiles;\n node.readCount = readCount;\n\n if (mode === 'binary') {\n node.score = totalFiles > 0 ? readCount / totalFiles : 0;\n } else {\n node.score = totalLines > 0 ? weightedScore / totalLines : 0;\n }\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity, CommitInfo } from \"../core/types.js\";\nimport { getAuthorArgs } from \"./identity.js\";\n\n/**\n * Get the set of files that a user has committed to (any commit, any time).\n * Used for Binary mode.\n */\nexport async function getFilesCommittedByUser(\n gitClient: GitClient,\n user: UserIdentity,\n): Promise<Set<string>> {\n const files = new Set<string>();\n\n // Query by both email and name to catch alias mismatches\n const queries: string[][] = [];\n if (user.email) {\n queries.push([\"--author\", user.email]);\n }\n if (user.name && user.name !== user.email) {\n queries.push([\"--author\", user.name]);\n }\n\n for (const authorArgs of queries) {\n try {\n const output = await gitClient.getLog([\n ...authorArgs,\n \"--name-only\",\n \"--pretty=format:\",\n \"--all\",\n ]);\n for (const line of output.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed) {\n files.add(trimmed);\n }\n }\n } catch {\n // Skip if no commits found\n }\n }\n\n return files;\n}\n\n/**\n * Get detailed commit information for a specific file by a specific user.\n * Used for Weighted mode's commit_score.\n */\nexport async function getDetailedCommits(\n gitClient: GitClient,\n user: UserIdentity,\n filePath: string,\n): Promise<CommitInfo[]> {\n const commits: CommitInfo[] = [];\n\n try {\n const output = await gitClient.getLog([\n ...getAuthorArgs(user),\n \"--numstat\",\n \"--pretty=format:%H|%aI\",\n \"--\",\n filePath,\n ]);\n\n const lines = output.trim().split(\"\\n\");\n let currentHash = \"\";\n let currentDate = new Date();\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n if (trimmed.includes(\"|\")) {\n const parts = trimmed.split(\"|\");\n currentHash = parts[0];\n currentDate = new Date(parts[1]);\n } else {\n const statMatch = trimmed.match(/^(\\d+|-)\\t(\\d+|-)\\t(.+)$/);\n if (statMatch && statMatch[3] === filePath) {\n const added = statMatch[1] === \"-\" ? 0 : parseInt(statMatch[1], 10);\n const deleted = statMatch[2] === \"-\" ? 0 : parseInt(statMatch[2], 10);\n\n // Get file size at that commit\n let fileSizeAtCommit = 1;\n try {\n const content = await gitClient.show([\n `${currentHash}:${filePath}`,\n ]);\n fileSizeAtCommit = Math.max(1, content.split(\"\\n\").length);\n } catch {\n fileSizeAtCommit = Math.max(1, added);\n }\n\n commits.push({\n hash: currentHash,\n date: currentDate,\n addedLines: added,\n deletedLines: deleted,\n fileSizeAtCommit,\n });\n }\n }\n }\n } catch {\n // No commits found\n }\n\n return commits;\n}\n\n/**\n * Get the last date a user touched a specific file (commit).\n */\nexport async function getLastCommitDate(\n gitClient: GitClient,\n user: UserIdentity,\n filePath: string,\n): Promise<Date | null> {\n try {\n const output = await gitClient.getLog([\n ...getAuthorArgs(user),\n \"-1\",\n \"--pretty=format:%aI\",\n \"--\",\n filePath,\n ]);\n const trimmed = output.trim();\n if (trimmed) {\n return new Date(trimmed);\n }\n } catch {\n // No commits found\n }\n return null;\n}\n","import type { FolderScore, FilterMode } from \"../core/types.js\";\nimport { walkFiles, recomputeFolderScores } from \"../core/file-tree.js\";\n\n/**\n * Score files in binary mode (read / not read).\n */\nexport function scoreBinary(\n tree: FolderScore,\n writtenFiles: Set<string>,\n reviewedFiles: Set<string>,\n filterMode: FilterMode,\n expiredFiles?: Set<string>,\n): void {\n walkFiles(tree, (file) => {\n const isWritten = writtenFiles.has(file.path);\n const isReviewed =\n reviewedFiles.has(file.path) && !writtenFiles.has(file.path);\n const isExpired = expiredFiles?.has(file.path) ?? false;\n\n file.isWritten = isWritten;\n file.isReviewed = isReviewed;\n file.isExpired = isExpired;\n\n if (isExpired) {\n file.score = 0;\n return;\n }\n\n switch (filterMode) {\n case \"written\":\n file.score = isWritten ? 1 : 0;\n break;\n case \"reviewed\":\n file.score = isReviewed ? 1 : 0;\n break;\n case \"all\":\n default:\n file.score = isWritten || isReviewed ? 1 : 0;\n break;\n }\n });\n\n recomputeFolderScores(tree, \"binary\");\n}\n","import type { GitClient } from \"./client.js\";\nimport type { UserIdentity } from \"../core/types.js\";\nimport { matchesUser } from \"./identity.js\";\n\nexport interface BlameEntry {\n email: string;\n name: string;\n lines: number;\n}\n\nexport interface BlameResult {\n entries: BlameEntry[];\n totalLines: number;\n}\n\n/**\n * Run git blame on a file and return per-author line counts.\n * Uses -w to ignore whitespace changes.\n */\nexport async function getBlameData(\n gitClient: GitClient,\n filePath: string,\n): Promise<BlameResult> {\n const authorMap = new Map<string, BlameEntry>();\n let totalLines = 0;\n\n try {\n const output = await gitClient.blame(filePath, [\"-w\", \"--porcelain\"]);\n const lines = output.split(\"\\n\");\n\n let currentName = \"\";\n let currentEmail = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"author \")) {\n currentName = line.slice(\"author \".length).trim();\n } else if (line.startsWith(\"author-mail \")) {\n currentEmail = line\n .slice(\"author-mail \".length)\n .replace(/[<>]/g, \"\")\n .trim();\n } else if (line.startsWith(\"\\t\")) {\n // Content line - count for current author\n if (currentEmail || currentName) {\n totalLines++;\n const key = `${currentEmail}|${currentName}`;\n const existing = authorMap.get(key);\n if (existing) {\n existing.lines++;\n } else {\n authorMap.set(key, {\n email: currentEmail,\n name: currentName,\n lines: 1,\n });\n }\n }\n }\n }\n } catch {\n // Binary file or other error - return empty\n }\n\n return { entries: Array.from(authorMap.values()), totalLines };\n}\n\n/**\n * Get the number of lines authored by a specific user in a file.\n */\nexport async function getUserBlameLines(\n gitClient: GitClient,\n filePath: string,\n user: UserIdentity,\n): Promise<{ userLines: number; totalLines: number }> {\n const { entries, totalLines } = await getBlameData(gitClient, filePath);\n\n let userLines = 0;\n for (const entry of entries) {\n if (matchesUser(entry.name, entry.email, user)) {\n userLines += entry.lines;\n }\n }\n\n return { userLines, totalLines };\n}\n","const DEFAULT_BATCH_SIZE = 10;\n\n/**\n * Process items in batches with concurrent execution within each batch.\n */\nexport async function processBatch<T>(\n items: T[],\n fn: (item: T) => Promise<void>,\n batchSize: number = DEFAULT_BATCH_SIZE,\n): Promise<void> {\n for (let i = 0; i < items.length; i += batchSize) {\n const batch = items.slice(i, i + batchSize);\n await Promise.all(batch.map(fn));\n }\n}\n","import type { FolderScore, UserIdentity } from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { getUserBlameLines } from \"../git/blame.js\";\nimport { walkFiles, recomputeFolderScores } from \"../core/file-tree.js\";\nimport { processBatch } from \"../utils/batch.js\";\n\n/**\n * Score files by authorship (git blame-based).\n * score(file) = blame_lines(user) / total_lines(file)\n */\nexport async function scoreAuthorship(\n tree: FolderScore,\n gitClient: GitClient,\n user: UserIdentity,\n): Promise<void> {\n const files: Array<{ path: string; setScore: (s: number) => void }> = [];\n\n walkFiles(tree, (file) => {\n files.push({\n path: file.path,\n setScore: (s) => {\n file.score = s;\n file.blameScore = s;\n },\n });\n });\n\n await processBatch(files, async ({ path, setScore }) => {\n const { userLines, totalLines } = await getUserBlameLines(\n gitClient,\n path,\n user,\n );\n setScore(totalLines > 0 ? userLines / totalLines : 0);\n });\n\n recomputeFolderScores(tree, \"continuous\");\n}\n","import type { FolderScore, ReviewInfo } from '../core/types.js';\nimport { walkFiles, recomputeFolderScores } from '../core/file-tree.js';\n\n/**\n * Score files by review coverage.\n * Files that the user has reviewed (via PR) get score 1, others 0.\n * User's own commits are excluded.\n */\nexport function scoreReviewCoverage(\n tree: FolderScore,\n reviewedFiles: Set<string>,\n): void {\n walkFiles(tree, (file) => {\n file.isReviewed = reviewedFiles.has(file.path);\n file.score = file.isReviewed ? 1 : 0;\n });\n\n recomputeFolderScores(tree, 'binary');\n}\n","/**\n * Sigmoid function: x / (x + k)\n * Saturates contribution of a single commit to 0-1 range.\n */\nexport function sigmoid(x: number, k: number = 0.3): number {\n if (x <= 0) return 0;\n return x / (x + k);\n}\n\n/**\n * Recency decay: e^(-lambda * t)\n * Models memory decay over time.\n * @param days - days since the event\n * @param halfLife - number of days for score to halve (default 180)\n */\nexport function recencyDecay(days: number, halfLife: number = 180): number {\n if (days <= 0) return 1;\n const lambda = Math.LN2 / halfLife;\n return Math.exp(-lambda * days);\n}\n\n/**\n * Scope factor: min(1, attentionThreshold / filesInPR)\n * Models attention dilution in large PRs.\n */\nexport function scopeFactor(\n filesInPR: number,\n attentionThreshold: number = 20,\n): number {\n if (filesInPR <= 0) return 1;\n return Math.min(1, attentionThreshold / filesInPR);\n}\n\n/**\n * Normalized diff: (added + 0.5 * deleted) / fileSize\n */\nexport function normalizedDiff(\n added: number,\n deleted: number,\n fileSize: number,\n): number {\n if (fileSize <= 0) return 0;\n return (added + 0.5 * deleted) / fileSize;\n}\n\n/**\n * Calculate the number of days between two dates.\n */\nexport function daysBetween(a: Date, b: Date): number {\n const ms = Math.abs(b.getTime() - a.getTime());\n return ms / (1000 * 60 * 60 * 24);\n}\n","import type {\n FolderScore,\n UserIdentity,\n WeightConfig,\n ReviewInfo,\n CommitInfo,\n} from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { getUserBlameLines } from \"../git/blame.js\";\nimport { getDetailedCommits } from \"../git/log.js\";\nimport { walkFiles, recomputeFolderScores } from \"../core/file-tree.js\";\nimport { processBatch } from \"../utils/batch.js\";\nimport {\n sigmoid,\n recencyDecay,\n scopeFactor,\n normalizedDiff,\n daysBetween,\n} from \"../utils/math.js\";\n\nconst REVIEW_BASE_WEIGHTS: Record<string, number> = {\n approved: 0.3,\n commented: 0.15,\n changes_requested: 0.35,\n};\n\nfunction calculateCommitScore(commits: CommitInfo[], now: Date): number {\n let raw = 0;\n for (const c of commits) {\n const nd = normalizedDiff(c.addedLines, c.deletedLines, c.fileSizeAtCommit);\n raw += sigmoid(nd) * recencyDecay(daysBetween(now, c.date));\n }\n return Math.min(1, raw);\n}\n\nfunction calculateReviewScore(\n reviews: ReviewInfo[] | undefined,\n now: Date,\n): number {\n if (!reviews) return 0;\n let raw = 0;\n for (const r of reviews) {\n const baseWeight = REVIEW_BASE_WEIGHTS[r.type] || 0.15;\n raw +=\n baseWeight *\n scopeFactor(r.filesInPR) *\n recencyDecay(daysBetween(now, r.date));\n }\n return Math.min(1, raw);\n}\n\n/**\n * Score files using the weighted mode (blame + commit + review signals).\n */\nexport async function scoreWeighted(\n tree: FolderScore,\n gitClient: GitClient,\n user: UserIdentity,\n weights: WeightConfig,\n reviewData?: Map<string, ReviewInfo[]>,\n now?: Date,\n): Promise<void> {\n const currentDate = now || new Date();\n const files: Array<{\n path: string;\n setScores: (b: number, c: number, r: number, total: number) => void;\n }> = [];\n\n walkFiles(tree, (file) => {\n files.push({\n path: file.path,\n setScores: (b, c, r, total) => {\n file.blameScore = b;\n file.commitScore = c;\n file.reviewScore = r;\n file.score = total;\n },\n });\n });\n\n await processBatch(files, async ({ path, setScores }) => {\n const { userLines, totalLines } = await getUserBlameLines(\n gitClient,\n path,\n user,\n );\n const blameScore = totalLines > 0 ? userLines / totalLines : 0;\n const commitScore = calculateCommitScore(\n await getDetailedCommits(gitClient, user, path),\n currentDate,\n );\n const reviewScore = calculateReviewScore(\n reviewData?.get(path),\n currentDate,\n );\n\n const total =\n weights.blame * blameScore +\n weights.commit * commitScore +\n weights.review * reviewScore;\n\n setScores(blameScore, commitScore, reviewScore, total);\n });\n\n recomputeFolderScores(tree, \"continuous\");\n}\n","import type { GitClient } from './client.js';\n\n/**\n * Calculate how much a file has changed since a given commit.\n * Returns the ratio of changed lines to current total lines.\n * Used for change-based expiration policy.\n */\nexport async function getChangeRatio(\n gitClient: GitClient,\n filePath: string,\n sinceCommit: string,\n): Promise<number> {\n try {\n const output = await gitClient.diff([\n '--numstat',\n `${sinceCommit}..HEAD`,\n '--',\n filePath,\n ]);\n\n const trimmed = output.trim();\n if (!trimmed) return 0;\n\n const match = trimmed.match(/^(\\d+|-)\\t(\\d+|-)\\t/);\n if (!match) return 0;\n\n const added = match[1] === '-' ? 0 : parseInt(match[1], 10);\n const deleted = match[2] === '-' ? 0 : parseInt(match[2], 10);\n const changedLines = added + deleted;\n\n // Get current file line count\n const currentContent = await gitClient.show([`HEAD:${filePath}`]);\n const currentLines = Math.max(1, currentContent.split('\\n').length);\n\n return changedLines / currentLines;\n } catch {\n return 0;\n }\n}\n\n/**\n * Get the commit hash for the last time a user touched a file.\n */\nexport async function getLastTouchCommit(\n gitClient: GitClient,\n filePath: string,\n userEmail: string,\n): Promise<string | null> {\n try {\n const output = await gitClient.getLog([\n '--author', userEmail,\n '-1',\n '--pretty=format:%H',\n '--',\n filePath,\n ]);\n const hash = output.trim();\n return hash || null;\n } catch {\n return null;\n }\n}\n","import type { ExpirationConfig, UserIdentity } from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { getLastCommitDate } from \"../git/log.js\";\nimport { getLastTouchCommit, getChangeRatio } from \"../git/diff.js\";\nimport { processBatch } from \"../utils/batch.js\";\n\n/**\n * Parse an expiration string from CLI into ExpirationConfig.\n * Formats: \"never\", \"time:180d\", \"change:50%\", \"combined:365d:50%\"\n */\nexport function parseExpirationConfig(input: string): ExpirationConfig {\n if (!input || input === \"never\") {\n return { policy: \"never\" };\n }\n\n if (input.startsWith(\"time:\")) {\n const duration = parseDays(input.slice(\"time:\".length));\n return { policy: \"time\", duration };\n }\n\n if (input.startsWith(\"change:\")) {\n const threshold = parsePercentage(input.slice(\"change:\".length));\n return { policy: \"change\", threshold };\n }\n\n if (input.startsWith(\"combined:\")) {\n const parts = input.slice(\"combined:\".length).split(\":\");\n const duration = parseDays(parts[0]);\n const threshold = parts[1] ? parsePercentage(parts[1]) : 0.5;\n return { policy: \"combined\", duration, threshold };\n }\n\n return { policy: \"never\" };\n}\n\nfunction parseDays(s: string): number {\n const match = s.match(/^(\\d+)d$/);\n if (!match)\n throw new Error(\n `Invalid duration format: \"${s}\". Expected format like \"180d\".`,\n );\n return parseInt(match[1], 10);\n}\n\nfunction parsePercentage(s: string): number {\n const match = s.match(/^(\\d+)%$/);\n if (!match)\n throw new Error(\n `Invalid percentage format: \"${s}\". Expected format like \"50%\".`,\n );\n return parseInt(match[1], 10) / 100;\n}\n\n/**\n * Check if a file's familiarity has expired according to the given policy.\n */\nexport async function isExpired(\n gitClient: GitClient,\n filePath: string,\n user: UserIdentity,\n config: ExpirationConfig,\n now?: Date,\n): Promise<boolean> {\n if (config.policy === \"never\") return false;\n\n const currentDate = now || new Date();\n const email = user.email || user.name;\n\n if (config.policy === \"time\" || config.policy === \"combined\") {\n const lastTouch = await getLastCommitDate(gitClient, user, filePath);\n if (lastTouch && config.duration) {\n const daysSince =\n (currentDate.getTime() - lastTouch.getTime()) / (1000 * 60 * 60 * 24);\n if (daysSince > config.duration) return true;\n }\n }\n\n if (config.policy === \"change\" || config.policy === \"combined\") {\n const lastCommit = await getLastTouchCommit(gitClient, filePath, email);\n if (lastCommit && config.threshold) {\n const ratio = await getChangeRatio(gitClient, filePath, lastCommit);\n if (ratio > config.threshold) return true;\n }\n }\n\n return false;\n}\n\n/**\n * Get the set of expired files for a given user and config.\n */\nexport async function getExpiredFiles(\n gitClient: GitClient,\n files: string[],\n user: UserIdentity,\n config: ExpirationConfig,\n): Promise<Set<string>> {\n if (config.policy === \"never\") return new Set();\n\n const expiredSet = new Set<string>();\n\n await processBatch(files, async (filePath) => {\n if (await isExpired(gitClient, filePath, user, config)) {\n expiredSet.add(filePath);\n }\n });\n\n return expiredSet;\n}\n","import type { ReviewInfo } from '../core/types.js';\n\ninterface GitHubPR {\n number: number;\n files: string[];\n}\n\ninterface GitHubReview {\n state: string;\n submitted_at: string;\n}\n\n/**\n * Minimal GitHub client using fetch (no external dependency).\n */\nexport class GitHubClient {\n private token: string;\n private baseUrl = 'https://api.github.com';\n\n constructor(token: string) {\n this.token = token;\n }\n\n private async fetch(path: string): Promise<any> {\n const url = `${this.baseUrl}${path}`;\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: 'application/vnd.github.v3+json',\n 'User-Agent': 'gitfamiliar',\n },\n });\n\n if (!response.ok) {\n if (response.status === 403) {\n throw new Error('GitHub API rate limit exceeded. Please wait or use a token with higher limits.');\n }\n throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);\n }\n\n return response.json();\n }\n\n /**\n * Parse owner/repo from a git remote URL.\n */\n static parseRemoteUrl(url: string): { owner: string; repo: string } | null {\n // SSH format: git@github.com:owner/repo.git\n let match = url.match(/github\\.com[:/]([^/]+)\\/([^/.]+)(\\.git)?$/);\n if (match) {\n return { owner: match[1], repo: match[2] };\n }\n return null;\n }\n\n /**\n * Get all files reviewed by a user across all PRs they reviewed.\n */\n async getReviewedFiles(\n owner: string,\n repo: string,\n username: string,\n ): Promise<Map<string, ReviewInfo[]>> {\n const reviewedFiles = new Map<string, ReviewInfo[]>();\n\n // Get PRs reviewed by the user (search API)\n let page = 1;\n const perPage = 100;\n\n while (true) {\n const searchResult = await this.fetch(\n `/search/issues?q=type:pr+repo:${owner}/${repo}+reviewed-by:${username}&per_page=${perPage}&page=${page}`,\n );\n\n if (!searchResult.items || searchResult.items.length === 0) break;\n\n for (const item of searchResult.items) {\n const prNumber = item.number;\n\n // Get the user's reviews for this PR\n const reviews: GitHubReview[] = await this.fetch(\n `/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,\n );\n\n const userReviews = reviews.filter(\n (r: any) => r.user?.login?.toLowerCase() === username.toLowerCase(),\n );\n\n if (userReviews.length === 0) continue;\n\n // Get files in this PR\n const prFiles: any[] = await this.fetch(\n `/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100`,\n );\n\n const fileCount = prFiles.length;\n\n for (const review of userReviews) {\n const reviewType = mapReviewState(review.state);\n const reviewDate = new Date(review.submitted_at);\n\n for (const prFile of prFiles) {\n const filePath = prFile.filename;\n const info: ReviewInfo = {\n date: reviewDate,\n type: reviewType,\n filesInPR: fileCount,\n };\n\n if (reviewedFiles.has(filePath)) {\n reviewedFiles.get(filePath)!.push(info);\n } else {\n reviewedFiles.set(filePath, [info]);\n }\n }\n }\n }\n\n if (searchResult.items.length < perPage) break;\n page++;\n }\n\n return reviewedFiles;\n }\n}\n\nfunction mapReviewState(state: string): ReviewInfo['type'] {\n switch (state.toUpperCase()) {\n case 'APPROVED':\n return 'approved';\n case 'CHANGES_REQUESTED':\n return 'changes_requested';\n default:\n return 'commented';\n }\n}\n","import { execSync } from 'node:child_process';\n\n/**\n * Resolve GitHub token from environment or gh CLI.\n */\nexport function resolveGitHubToken(): string | null {\n // Check environment variables\n if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;\n if (process.env.GH_TOKEN) return process.env.GH_TOKEN;\n\n // Try gh CLI\n try {\n const token = execSync('gh auth token', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();\n if (token) return token;\n } catch {\n // gh CLI not available or not authenticated\n }\n\n return null;\n}\n","import type { ReviewInfo } from \"../core/types.js\";\nimport type { GitClient } from \"../git/client.js\";\nimport { GitHubClient } from \"./client.js\";\nimport { resolveGitHubToken } from \"./auth.js\";\n\n/**\n * Attempt to fetch review data from GitHub.\n * Returns null if no token or not a GitHub repo.\n */\nexport async function fetchReviewData(\n gitClient: GitClient,\n username?: string,\n): Promise<{\n reviewedFiles: Map<string, ReviewInfo[]>;\n reviewedFileSet: Set<string>;\n} | null> {\n const token = resolveGitHubToken();\n if (!token) return null;\n\n const remoteUrl = await gitClient.getRemoteUrl();\n if (!remoteUrl) return null;\n\n const parsed = GitHubClient.parseRemoteUrl(remoteUrl);\n if (!parsed) return null;\n\n // GitHub username is required for review API queries\n if (!username) return null;\n const ghUsername = username;\n\n try {\n const githubClient = new GitHubClient(token);\n const reviewedFiles = await githubClient.getReviewedFiles(\n parsed.owner,\n parsed.repo,\n ghUsername,\n );\n\n const reviewedFileSet = new Set(reviewedFiles.keys());\n\n return { reviewedFiles, reviewedFileSet };\n } catch {\n return null;\n }\n}\n","import type { CliOptions, FolderScore, ReviewInfo } from \"./types.js\";\nimport { GitClient } from \"../git/client.js\";\nimport { resolveUser } from \"../git/identity.js\";\nimport { createFilter } from \"../filter/ignore.js\";\nimport { buildFileTree, walkFiles } from \"./file-tree.js\";\nimport { getFilesCommittedByUser } from \"../git/log.js\";\nimport { scoreBinary } from \"../scoring/binary.js\";\nimport { scoreAuthorship } from \"../scoring/authorship.js\";\nimport { scoreReviewCoverage } from \"../scoring/review-coverage.js\";\nimport { scoreWeighted } from \"../scoring/weighted.js\";\nimport { getExpiredFiles } from \"../scoring/expiration.js\";\nimport { fetchReviewData } from \"../github/reviews.js\";\n\nexport interface FamiliarityResult {\n tree: FolderScore;\n repoName: string;\n userName: string;\n mode: string;\n writtenCount: number;\n reviewedCount: number;\n bothCount: number;\n totalFiles: number;\n}\n\nexport async function computeFamiliarity(\n options: CliOptions,\n): Promise<FamiliarityResult> {\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 userFlag = Array.isArray(options.user) ? options.user[0] : options.user;\n const user = await resolveUser(gitClient, userFlag);\n const filter = createFilter(repoRoot);\n const tree = await buildFileTree(gitClient, filter);\n\n // Get written files (used by binary, weighted)\n const writtenFiles = await getFilesCommittedByUser(gitClient, user);\n\n // Get review data (if available)\n let reviewData: Map<string, ReviewInfo[]> | undefined;\n let reviewedFileSet = new Set<string>();\n\n if (options.mode !== \"authorship\") {\n const reviewResult = await fetchReviewData(gitClient, userFlag);\n if (reviewResult) {\n reviewData = reviewResult.reviewedFiles;\n reviewedFileSet = reviewResult.reviewedFileSet;\n }\n }\n\n // Get expired files\n let expiredFiles: Set<string> | undefined;\n if (options.expiration.policy !== \"never\") {\n const allFiles: string[] = [];\n walkFiles(tree, (f) => allFiles.push(f.path));\n expiredFiles = await getExpiredFiles(\n gitClient,\n allFiles,\n user,\n options.expiration,\n );\n }\n\n // Score based on mode\n switch (options.mode) {\n case \"binary\":\n scoreBinary(\n tree,\n writtenFiles,\n reviewedFileSet,\n options.filter,\n expiredFiles,\n );\n break;\n\n case \"authorship\":\n await scoreAuthorship(tree, gitClient, user);\n break;\n\n case \"review-coverage\":\n if (reviewedFileSet.size === 0) {\n console.error(\n \"Warning: No review data available. Set GITHUB_TOKEN or use --user with your GitHub username.\",\n );\n }\n scoreReviewCoverage(tree, reviewedFileSet);\n break;\n\n case \"weighted\":\n await scoreWeighted(tree, gitClient, user, options.weights, reviewData);\n break;\n }\n\n return {\n tree,\n repoName,\n userName: user.name || user.email,\n mode: options.mode,\n ...computeSummary(tree, writtenFiles, reviewedFileSet),\n totalFiles: tree.fileCount,\n };\n}\n\nfunction computeSummary(\n tree: FolderScore,\n writtenFiles: Set<string>,\n reviewedFileSet: Set<string>,\n): { writtenCount: number; reviewedCount: number; bothCount: number } {\n let writtenOnly = 0;\n let reviewedOnly = 0;\n let both = 0;\n\n walkFiles(tree, (file) => {\n const w = writtenFiles.has(file.path);\n const r = reviewedFileSet.has(file.path);\n if (w && r) both++;\n else if (w) writtenOnly++;\n else if (r) reviewedOnly++;\n });\n\n return {\n writtenCount: writtenOnly + both,\n reviewedCount: reviewedOnly + both,\n bothCount: both,\n };\n}\n"],"mappings":"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","names":["readFileSync","join","parentPath","parent","isExpired"]}
|