code-gauge 9.0.0 → 10.0.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.
Files changed (43) hide show
  1. package/README.md +41 -18
  2. package/dist/checkCommand.cjs +2 -2
  3. package/dist/checkCommand.cjs.map +1 -1
  4. package/dist/checkCommand.d.ts +3 -1
  5. package/dist/checkCommand.js +2 -2
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/cli.cjs +3 -3
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.js +3 -3
  10. package/dist/cli.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/metrics.cjs +1 -1
  13. package/dist/metrics.cjs.map +1 -1
  14. package/dist/metrics.js +1 -1
  15. package/dist/metrics.js.map +1 -1
  16. package/dist/nativeMetrics.cjs +1 -1
  17. package/dist/nativeMetrics.cjs.map +1 -1
  18. package/dist/nativeMetrics.d.ts +4 -2
  19. package/dist/nativeMetrics.js +1 -1
  20. package/dist/nativeMetrics.js.map +1 -1
  21. package/dist/thresholdCheck.cjs +1 -1
  22. package/dist/thresholdCheck.cjs.map +1 -1
  23. package/dist/thresholdCheck.d.ts +18 -4
  24. package/dist/thresholdCheck.js +1 -1
  25. package/dist/thresholdCheck.js.map +1 -1
  26. package/dist/thresholds.cjs +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +5 -3
  29. package/dist/thresholds.js +1 -1
  30. package/dist/thresholds.js.map +1 -1
  31. package/dist/types.d.ts +37 -0
  32. package/dist/wasmBinding.cjs +1 -1
  33. package/dist/wasmBinding.cjs.map +1 -1
  34. package/dist/wasmBinding.js +1 -1
  35. package/dist/wasmBinding.js.map +1 -1
  36. package/native/code-gauge.wasm +0 -0
  37. package/native/src/complexity.rs +173 -19
  38. package/native/src/container.rs +413 -0
  39. package/native/src/functions.rs +2 -5
  40. package/native/src/lib.rs +2 -1
  41. package/native/src/measure.rs +24 -8
  42. package/native/src/types.rs +21 -0
  43. package/package.json +8 -8
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["getErrorMessage","Command","levels","thresholds","runCheckCommand","configFileName","duplicationThreshold","fileThresholds","functionThresholds","resolveTarget","configSearchDirectory","loadConfig","resolveOptions","scanTarget","loadExclusion","formatPath","collectDuplicatedLineNumbers","InvalidArgumentError","parsePositiveInteger"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command, InvalidArgumentError } from 'commander';\nimport { getErrorMessage, parsePositiveInteger } from '@willbooster/shared-lib';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runCheckCommand, type CheckCliOptions } from './checkCommand.js';\nimport { loadExclusion } from './exclusion.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n thresholds,\n type Level,\n type Limits,\n} from './thresholds.js';\nimport type { FunctionMetrics } from './types.js';\n\n/** The worst (highest-cognitive-complexity) function of a file, reported as the ranking evidence. */\ninterface WorstFunction {\n name: string;\n startLine: number;\n endLine: number;\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n}\n\n/**\n * One ranked refactoring candidate. The score is the sum of the file's repo-relative percentile\n * ranks (each in [0, 1)) over three dimensions: worst-function cognitive complexity, duplicated\n * lines (within-file and cross-file combined), and file NCSS. Ranking is relative to the scanned\n * project, so no absolute threshold is involved.\n */\ninterface RankedFile {\n file: string;\n score: number;\n worstFunction?: WorstFunction;\n /** Distinct lines covered by within-file duplicate blocks or cross-file duplicate occurrences. */\n duplicatedLineCount: number;\n /** duplicatedLineCount / code lines (0 when the file has no code). */\n duplicatedLineRatio: number;\n /** Other files sharing cross-file duplicate blocks; filled only for the reported top files. */\n crossFilePartners: string[];\n ncss: number;\n codeLines: number;\n}\n\n/** Caps how many cross-file partners a ranked file lists so the report stays scannable. */\nconst maxCrossFilePartners = 3;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Measure code metrics: rank refactoring candidates (the default command) or check thresholds.');\n\n addSharedOptions(\n program\n .command('rank', { isDefault: true })\n .description('Rank the files of a project by refactoring priority')\n .argument('[target]', 'file or directory to measure', '.')\n )\n .option('--top <number>', 'number of top-ranked files to report (default: 10)', parsePositiveIntegerOption)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .action(runRankCommand);\n\n const check = addSharedOptions(\n program\n .command('check')\n .description(\n 'Report every exceeded threshold as a warning or an error (exit 1 on errors, 2 when files cannot be measured); with --base, only in what the change touches'\n )\n .argument('[target]', 'file or directory to check', '.')\n ).option('--base <ref>', 'check only what the working tree changed since the merge-base of this git ref and HEAD');\n for (const level of levels) {\n for (const { key, defaultLimits } of thresholds) {\n const defaultLimit = defaultLimits[level];\n check.option(\n `--${level}-${toKebabCase(key)} <number>`,\n `${describeLimit(key, level)} (default ${Number.isFinite(defaultLimit) ? defaultLimit : 'off'}); \"off\" disables it`,\n parseLimit\n );\n }\n }\n // Exit code 1 means error-level violations, so a rejected command line must not use it.\n check.exitOverride((error) => process.exit(error.exitCode === 0 ? 0 : 2));\n check.action(async (target: string, cliOptions: CheckCliOptions & Record<string, unknown>) => {\n const readLimits = (level: Level): Limits =>\n Object.fromEntries(\n thresholds.flatMap(({ key }) => {\n const limit = cliOptions[`${level}${key.charAt(0).toUpperCase()}${key.slice(1)}`];\n return limit === undefined ? [] : [[key, limit as number]];\n })\n );\n await runCheckCommand(target, cliOptions, { warning: readLimits('warning'), error: readLimits('error') });\n });\n\n await program.parseAsync();\n}\n\n/** Registers the options shared by the ranking and check commands. */\nfunction addSharedOptions(command: Command): Command {\n return command\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveIntegerOption\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output');\n}\n\nfunction describeLimit(key: string, level: Level): string {\n if (key === duplicationThreshold.key) {\n return `line count from which a duplicated block is ${level === 'error' ? 'an error' : 'a warning'}`;\n }\n const fileThreshold = fileThresholds.find((threshold) => threshold.key === key);\n const subject = fileThreshold\n ? fileThreshold.label\n : `${functionThresholds.find((threshold) => threshold.key === key)?.label} of a function`;\n return `maximum ${subject} without ${level === 'error' ? 'an error' : 'a warning'}`;\n}\n\nfunction toKebabCase(key: string): string {\n return key.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);\n}\n\nasync function runRankCommand(target: string, cliOptions: CliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const searchDirectory = await configSearchDirectory(resolvedTarget);\n const config = await loadConfig(cliOptions.config, searchDirectory);\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n addCrossFileDuplication(result, options);\n const rankedFiles = rankFiles(result, options.top);\n\n if (options.json) {\n printJson(result, rankedFiles, options);\n } else {\n printTextReport(resolvedTarget, result, rankedFiles, options);\n }\n\n if (result.fatalError || (options.failOnError && result.errors.length > 0)) {\n process.exitCode = 1;\n }\n}\n\nfunction rankFiles(result: ScanResult, top: number): RankedFile[] {\n const candidates = result.files.map(({ file, metrics }) => {\n const formattedFile = formatPath(file, result.displayRoot);\n const duplicatedLines = collectDuplicatedLineNumbers(metrics, result.crossFileDuplication, formattedFile);\n return {\n file: formattedFile,\n worstFunction: findWorstFunction(metrics.functions),\n duplicatedLineCount: duplicatedLines.size,\n duplicatedLineRatio: metrics.lines.code === 0 ? 0 : duplicatedLines.size / metrics.lines.code,\n crossFilePartners: [] as string[],\n ncss: metrics.ncssCount,\n codeLines: metrics.lines.code,\n };\n });\n\n const cognitivePercentile = makePercentile(candidates.map((c) => c.worstFunction?.cognitiveComplexity ?? 0));\n const duplicationPercentile = makePercentile(candidates.map((c) => c.duplicatedLineCount));\n const ncssPercentile = makePercentile(candidates.map((c) => c.ncss));\n\n const ranked = candidates\n .map((candidate) => ({\n ...candidate,\n score:\n cognitivePercentile(candidate.worstFunction?.cognitiveComplexity ?? 0) +\n duplicationPercentile(candidate.duplicatedLineCount) +\n ncssPercentile(candidate.ncss),\n }))\n .toSorted(\n (left, right) => right.score - left.score || right.ncss - left.ncss || left.file.localeCompare(right.file)\n );\n // Partner evidence is attached only to the files that will be reported: expanding partners for\n // every scanned file first would retain TH(F^2) strings when one clone group spans F files, and\n // rescanning all groups per file would be O(files x groups) on the post-scan ranking step.\n attachCrossFilePartners(ranked.slice(0, top), result.crossFileDuplication);\n return ranked;\n}\n\n/** One pass over the groups fills the reported files' partner lists (other files sharing a group). */\nfunction attachCrossFilePartners(\n reportedFiles: RankedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined\n): void {\n if (!crossFileDuplication) {\n return;\n }\n const partnersByFile = new Map(reportedFiles.map((ranked) => [ranked.file, new Set<string>()]));\n for (const group of crossFileDuplication.groups) {\n for (const file of group.files) {\n const partners = partnersByFile.get(file);\n if (!partners) {\n continue;\n }\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n for (const ranked of reportedFiles) {\n ranked.crossFilePartners = [...(partnersByFile.get(ranked.file) ?? [])].toSorted();\n }\n}\n\n/**\n * Percentile rank within the scanned project: the fraction of files with a strictly smaller\n * value, in [0, 1). Relative ranking needs no absolute threshold, which sidesteps the metric\n * calibration problem entirely — the top of the list is worth refactoring first regardless of\n * where any cutoff would sit.\n */\nfunction makePercentile(values: number[]): (value: number) => number {\n const sorted = values.toSorted((left, right) => left - right);\n return (value: number) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((sorted[middle] as number) < value) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return sorted.length === 0 ? 0 : low / sorted.length;\n };\n}\n\n/** The highest-cognitive-complexity function; NCSS breaks ties so the larger body is reported. */\nfunction findWorstFunction(functions: FunctionMetrics[]): WorstFunction | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (\n !worst ||\n fn.cognitiveComplexity > worst.cognitiveComplexity ||\n (fn.cognitiveComplexity === worst.cognitiveComplexity && fn.ncss > worst.ncss)\n ) {\n worst = fn;\n }\n }\n if (!worst) {\n return undefined;\n }\n return {\n name: worst.name ?? '<anonymous>',\n startLine: worst.startLine,\n endLine: worst.endLine,\n cognitiveComplexity: worst.cognitiveComplexity,\n ncss: worst.ncss,\n nestingDepth: worst.nestingDepth,\n };\n}\n\nfunction printJson(result: ScanResult, rankedFiles: RankedFile[], options: ResolvedOptions): void {\n const reportedFiles = rankedFiles.slice(0, options.top);\n writeStdout(\n JSON.stringify(\n {\n summary: summarize(result.files),\n totalRankedFiles: rankedFiles.length,\n truncated: reportedFiles.length < rankedFiles.length,\n files: reportedFiles,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(\n target: string,\n result: ScanResult,\n rankedFiles: RankedFile[],\n options: ResolvedOptions\n): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(\n `Measured ${summary.fileCount} files under ${target} (code LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount})\\n`\n );\n\n if (rankedFiles.length === 0) {\n writeStdout('No measurable files found.\\n');\n } else {\n const reportedFiles = rankedFiles.slice(0, options.top);\n const totalSuffix = rankedFiles.length > reportedFiles.length ? ` of ${rankedFiles.length}` : '';\n writeStdout(`\\nRefactoring candidates (top ${reportedFiles.length}${totalSuffix}):\\n`);\n for (const [index, ranked] of reportedFiles.entries()) {\n writeStdout(`${index + 1}. ${formatRankedFile(ranked)}\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\n/** One ranked file as a single line: the location, the evidence, and where the duplication points. */\nfunction formatRankedFile(ranked: RankedFile): string {\n const reasons: string[] = [];\n if (ranked.worstFunction) {\n const fn = ranked.worstFunction;\n reasons.push(\n `worst function ${fn.name} (L${fn.startLine}-${fn.endLine}) cognitive ${fn.cognitiveComplexity}, NCSS ${fn.ncss}, nesting ${fn.nestingDepth}`\n );\n }\n if (ranked.duplicatedLineCount > 0) {\n const partnersSuffix =\n ranked.crossFilePartners.length > 0\n ? `, shared with ${ranked.crossFilePartners.slice(0, maxCrossFilePartners).join(', ')}${ranked.crossFilePartners.length > maxCrossFilePartners ? ', ...' : ''}`\n : '';\n reasons.push(\n `duplicated lines ${ranked.duplicatedLineCount} (${Math.round(ranked.duplicatedLineRatio * 100)}%${partnersSuffix})`\n );\n }\n reasons.push(`file NCSS ${ranked.ncss}`);\n return `${ranked.file} (score ${ranked.score.toFixed(2)}): ${reasons.join('; ')}`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n ncssCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functions.length;\n linesOfCode += file.metrics.lines.code;\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n }\n\n return { fileCount: files.length, functionCount, linesOfCode, maxCognitiveComplexity, ncssCount };\n}\n\nfunction parseLimit(value: string): number {\n if (value === 'off') {\n return Infinity;\n }\n const parsed = value.trim() === '' ? Number.NaN : Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative number or \"off\".');\n }\n return parsed;\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveIntegerOption(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveIntegerOption(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed === undefined) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","names":["getErrorMessage","Command","levels","thresholds","runCheckCommand","configFileName","duplicationThreshold","fileThresholds","functionThresholds","resolveTarget","configSearchDirectory","loadConfig","resolveOptions","scanTarget","loadExclusion","formatPath","collectDuplicatedLineNumbers","qualifyFunctionName","InvalidArgumentError","parsePositiveInteger"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command, InvalidArgumentError } from 'commander';\nimport { getErrorMessage, parsePositiveInteger } from '@willbooster/shared-lib';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runCheckCommand, type CheckCliOptions } from './checkCommand.js';\nimport { loadExclusion } from './exclusion.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n thresholds,\n type Level,\n type Limits,\n} from './thresholds.js';\nimport { qualifyFunctionName } from './thresholdCheck.js';\nimport type { FunctionMetrics } from './types.js';\n\n/** The worst (highest-cognitive-complexity) function of a file, reported as the ranking evidence. */\ninterface WorstFunction {\n name: string;\n startLine: number;\n endLine: number;\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n}\n\n/**\n * One ranked refactoring candidate. The score is the sum of the file's repo-relative percentile\n * ranks (each in [0, 1)) over three dimensions: worst-function cognitive complexity, duplicated\n * lines (within-file and cross-file combined), and file NCSS. Ranking is relative to the scanned\n * project, so no absolute threshold is involved.\n */\ninterface RankedFile {\n file: string;\n score: number;\n worstFunction?: WorstFunction;\n /** Distinct lines covered by within-file duplicate blocks or cross-file duplicate occurrences. */\n duplicatedLineCount: number;\n /** duplicatedLineCount / code lines (0 when the file has no code). */\n duplicatedLineRatio: number;\n /** Other files sharing cross-file duplicate blocks; filled only for the reported top files. */\n crossFilePartners: string[];\n ncss: number;\n codeLines: number;\n}\n\n/** Caps how many cross-file partners a ranked file lists so the report stays scannable. */\nconst maxCrossFilePartners = 3;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Measure code metrics: rank refactoring candidates (the default command) or check thresholds.');\n\n addSharedOptions(\n program\n .command('rank', { isDefault: true })\n .description('Rank the files of a project by refactoring priority')\n .argument('[target]', 'file or directory to measure', '.')\n )\n .option('--top <number>', 'number of top-ranked files to report (default: 10)', parsePositiveIntegerOption)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .action(runRankCommand);\n\n const check = addSharedOptions(\n program\n .command('check')\n .description(\n 'Report every exceeded threshold as a warning or an error (exit 1 on errors, 2 when files cannot be measured); with --base, only in what the change touches'\n )\n .argument('[target]', 'file or directory to check', '.')\n ).option('--base <ref>', 'check only what the working tree changed since the merge-base of this git ref and HEAD');\n for (const level of levels) {\n for (const { key, defaultLimits } of thresholds) {\n const defaultLimit = defaultLimits[level];\n check.option(\n `--${level}-${toKebabCase(key)} <number>`,\n `${describeLimit(key, level)} (default ${Number.isFinite(defaultLimit) ? defaultLimit : 'off'}); \"off\" disables it`,\n parseLimit\n );\n }\n }\n // Exit code 1 means error-level violations, so a rejected command line must not use it.\n check.exitOverride((error) => process.exit(error.exitCode === 0 ? 0 : 2));\n check.action(async (target: string, cliOptions: CheckCliOptions & Record<string, unknown>) => {\n const readLimits = (level: Level): Limits =>\n Object.fromEntries(\n thresholds.flatMap(({ key }) => {\n const limit = cliOptions[`${level}${key.charAt(0).toUpperCase()}${key.slice(1)}`];\n return limit === undefined ? [] : [[key, limit as number]];\n })\n );\n await runCheckCommand(target, cliOptions, { warning: readLimits('warning'), error: readLimits('error') });\n });\n\n await program.parseAsync();\n}\n\n/** Registers the options shared by the ranking and check commands. */\nfunction addSharedOptions(command: Command): Command {\n return command\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveIntegerOption\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output');\n}\n\nfunction describeLimit(key: string, level: Level): string {\n if (key === duplicationThreshold.key) {\n return `code-line count from which a duplicated block is ${level === 'error' ? 'an error' : 'a warning'}`;\n }\n const fileThreshold = fileThresholds.find((threshold) => threshold.key === key);\n const subject = fileThreshold\n ? fileThreshold.label\n : `${functionThresholds.find((threshold) => threshold.key === key)?.label} of a function`;\n return `maximum ${subject} without ${level === 'error' ? 'an error' : 'a warning'}`;\n}\n\nfunction toKebabCase(key: string): string {\n return key.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);\n}\n\nasync function runRankCommand(target: string, cliOptions: CliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const searchDirectory = await configSearchDirectory(resolvedTarget);\n const config = await loadConfig(cliOptions.config, searchDirectory);\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n addCrossFileDuplication(result, options);\n const rankedFiles = rankFiles(result, options.top);\n\n if (options.json) {\n printJson(result, rankedFiles, options);\n } else {\n printTextReport(resolvedTarget, result, rankedFiles, options);\n }\n\n if (result.fatalError || (options.failOnError && result.errors.length > 0)) {\n process.exitCode = 1;\n }\n}\n\nfunction rankFiles(result: ScanResult, top: number): RankedFile[] {\n const candidates = result.files.map(({ file, metrics }) => {\n const formattedFile = formatPath(file, result.displayRoot);\n const duplicatedLines = collectDuplicatedLineNumbers(metrics, result.crossFileDuplication, formattedFile);\n return {\n file: formattedFile,\n worstFunction: findWorstFunction(metrics.functions),\n duplicatedLineCount: duplicatedLines.size,\n duplicatedLineRatio: metrics.lines.code === 0 ? 0 : duplicatedLines.size / metrics.lines.code,\n crossFilePartners: [] as string[],\n ncss: metrics.ncssCount,\n codeLines: metrics.lines.code,\n };\n });\n\n const cognitivePercentile = makePercentile(candidates.map((c) => c.worstFunction?.cognitiveComplexity ?? 0));\n const duplicationPercentile = makePercentile(candidates.map((c) => c.duplicatedLineCount));\n const ncssPercentile = makePercentile(candidates.map((c) => c.ncss));\n\n const ranked = candidates\n .map((candidate) => ({\n ...candidate,\n score:\n cognitivePercentile(candidate.worstFunction?.cognitiveComplexity ?? 0) +\n duplicationPercentile(candidate.duplicatedLineCount) +\n ncssPercentile(candidate.ncss),\n }))\n .toSorted(\n (left, right) => right.score - left.score || right.ncss - left.ncss || left.file.localeCompare(right.file)\n );\n // Partner evidence is attached only to the files that will be reported: expanding partners for\n // every scanned file first would retain TH(F^2) strings when one clone group spans F files, and\n // rescanning all groups per file would be O(files x groups) on the post-scan ranking step.\n attachCrossFilePartners(ranked.slice(0, top), result.crossFileDuplication);\n return ranked;\n}\n\n/** One pass over the groups fills the reported files' partner lists (other files sharing a group). */\nfunction attachCrossFilePartners(\n reportedFiles: RankedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined\n): void {\n if (!crossFileDuplication) {\n return;\n }\n const partnersByFile = new Map(reportedFiles.map((ranked) => [ranked.file, new Set<string>()]));\n for (const group of crossFileDuplication.groups) {\n for (const file of group.files) {\n const partners = partnersByFile.get(file);\n if (!partners) {\n continue;\n }\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n for (const ranked of reportedFiles) {\n ranked.crossFilePartners = [...(partnersByFile.get(ranked.file) ?? [])].toSorted();\n }\n}\n\n/**\n * Percentile rank within the scanned project: the fraction of files with a strictly smaller\n * value, in [0, 1). Relative ranking needs no absolute threshold, which sidesteps the metric\n * calibration problem entirely — the top of the list is worth refactoring first regardless of\n * where any cutoff would sit.\n */\nfunction makePercentile(values: number[]): (value: number) => number {\n const sorted = values.toSorted((left, right) => left - right);\n return (value: number) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((sorted[middle] as number) < value) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return sorted.length === 0 ? 0 : low / sorted.length;\n };\n}\n\n/** The highest-cognitive-complexity function; NCSS breaks ties so the larger body is reported. */\nfunction findWorstFunction(functions: FunctionMetrics[]): WorstFunction | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (\n !worst ||\n fn.cognitiveComplexity > worst.cognitiveComplexity ||\n (fn.cognitiveComplexity === worst.cognitiveComplexity && fn.ncss > worst.ncss)\n ) {\n worst = fn;\n }\n }\n if (!worst) {\n return undefined;\n }\n return {\n name: qualifyFunctionName(worst),\n startLine: worst.startLine,\n endLine: worst.endLine,\n cognitiveComplexity: worst.cognitiveComplexity,\n ncss: worst.ncss,\n nestingDepth: worst.nestingDepth,\n };\n}\n\nfunction printJson(result: ScanResult, rankedFiles: RankedFile[], options: ResolvedOptions): void {\n const reportedFiles = rankedFiles.slice(0, options.top);\n writeStdout(\n JSON.stringify(\n {\n summary: summarize(result.files),\n totalRankedFiles: rankedFiles.length,\n truncated: reportedFiles.length < rankedFiles.length,\n files: reportedFiles,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(\n target: string,\n result: ScanResult,\n rankedFiles: RankedFile[],\n options: ResolvedOptions\n): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(\n `Measured ${summary.fileCount} files under ${target} (code LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount})\\n`\n );\n\n if (rankedFiles.length === 0) {\n writeStdout('No measurable files found.\\n');\n } else {\n const reportedFiles = rankedFiles.slice(0, options.top);\n const totalSuffix = rankedFiles.length > reportedFiles.length ? ` of ${rankedFiles.length}` : '';\n writeStdout(`\\nRefactoring candidates (top ${reportedFiles.length}${totalSuffix}):\\n`);\n for (const [index, ranked] of reportedFiles.entries()) {\n writeStdout(`${index + 1}. ${formatRankedFile(ranked)}\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\n/** One ranked file as a single line: the location, the evidence, and where the duplication points. */\nfunction formatRankedFile(ranked: RankedFile): string {\n const reasons: string[] = [];\n if (ranked.worstFunction) {\n const fn = ranked.worstFunction;\n reasons.push(\n `worst function ${fn.name} (L${fn.startLine}-${fn.endLine}) cognitive ${fn.cognitiveComplexity}, NCSS ${fn.ncss}, nesting ${fn.nestingDepth}`\n );\n }\n if (ranked.duplicatedLineCount > 0) {\n const partnersSuffix =\n ranked.crossFilePartners.length > 0\n ? `, shared with ${ranked.crossFilePartners.slice(0, maxCrossFilePartners).join(', ')}${ranked.crossFilePartners.length > maxCrossFilePartners ? ', ...' : ''}`\n : '';\n reasons.push(\n `duplicated lines ${ranked.duplicatedLineCount} (${Math.round(ranked.duplicatedLineRatio * 100)}%${partnersSuffix})`\n );\n }\n reasons.push(`file NCSS ${ranked.ncss}`);\n return `${ranked.file} (score ${ranked.score.toFixed(2)}): ${reasons.join('; ')}`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n ncssCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functions.length;\n linesOfCode += file.metrics.lines.code;\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n }\n\n return { fileCount: files.length, functionCount, linesOfCode, maxCognitiveComplexity, ncssCount };\n}\n\nfunction parseLimit(value: string): number {\n if (value === 'off') {\n return Infinity;\n }\n const parsed = value.trim() === '' ? Number.NaN : Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative number or \"off\".');\n }\n return parsed;\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveIntegerOption(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveIntegerOption(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed === undefined) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,thresholds as i}from"./thresholds.js";import{configFileName as a,loadConfig as o,resolveOptions as s}from"./cliConfig.js";import{loadExclusion as c}from"./exclusion.js";import{addCrossFileDuplication as l,collectDuplicatedLineNumbers as u,configSearchDirectory as d,formatPath as f,resolveTarget as p,scanTarget as m,writeStderr as h,writeStdout as g}from"./scan.js";import{runCheckCommand as _}from"./checkCommand.js";import{getErrorMessage as v,parsePositiveInteger as y}from"@willbooster/shared-lib";import{Command as b,InvalidArgumentError as x}from"commander";S().catch(e=>{h(`Error: ${v(e)}\n`),process.exitCode=1});async function S(){let e=new b().name(`code-gauge`).description(`Measure code metrics: rank refactoring candidates (the default command) or check thresholds.`);C(e.command(`rank`,{isDefault:!0}).description(`Rank the files of a project by refactoring priority`).argument(`[target]`,`file or directory to measure`,`.`)).option(`--top <number>`,`number of top-ranked files to report (default: 10)`,R).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`).action(E);let t=C(e.command(`check`).description(`Report every exceeded threshold as a warning or an error (exit 1 on errors, 2 when files cannot be measured); with --base, only in what the change touches`).argument(`[target]`,`file or directory to check`,`.`)).option(`--base <ref>`,`check only what the working tree changed since the merge-base of this git ref and HEAD`);for(let e of r)for(let{key:n,defaultLimits:r}of i){let i=r[e];t.option(`--${e}-${T(n)} <number>`,`${w(n,e)} (default ${Number.isFinite(i)?i:`off`}); "off" disables it`,F)}t.exitOverride(e=>process.exit(e.exitCode===0?0:2)),t.action(async(e,t)=>{let n=e=>Object.fromEntries(i.flatMap(({key:n})=>{let r=t[`${e}${n.charAt(0).toUpperCase()}${n.slice(1)}`];return r===void 0?[]:[[n,r]]}));await _(e,t,{warning:n(`warning`),error:n(`error`)})}),await e.parseAsync()}function C(e){return e.option(`--config <path>`,`config file to use instead of the auto-detected ${a}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,R).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,L).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,I).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}function w(r,i){if(r===e.key)return`line count from which a duplicated block is ${i===`error`?`an error`:`a warning`}`;let a=t.find(e=>e.key===r);return`maximum ${a?a.label:`${n.find(e=>e.key===r)?.label} of a function`} without ${i===`error`?`an error`:`a warning`}`}function T(e){return e.replaceAll(/[A-Z]/gu,e=>`-${e.toLowerCase()}`)}async function E(e,t){let n=p(e),r=await d(n),i=await o(t.config,r),a=s(t,i),u=await m(n,{...a,loadExclusion:e=>c(r,a.exclude,e)});l(u,a);let f=D(u,a.top);a.json?j(u,f,a):M(n,u,f,a),(u.fatalError||a.failOnError&&u.errors.length>0)&&(process.exitCode=1)}function D(e,t){let n=e.files.map(({file:t,metrics:n})=>{let r=f(t,e.displayRoot),i=u(n,e.crossFileDuplication,r);return{file:r,worstFunction:A(n.functions),duplicatedLineCount:i.size,duplicatedLineRatio:n.lines.code===0?0:i.size/n.lines.code,crossFilePartners:[],ncss:n.ncssCount,codeLines:n.lines.code}}),r=k(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),i=k(n.map(e=>e.duplicatedLineCount)),a=k(n.map(e=>e.ncss)),o=n.map(e=>({...e,score:r(e.worstFunction?.cognitiveComplexity??0)+i(e.duplicatedLineCount)+a(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return O(o.slice(0,t),e.crossFileDuplication),o}function O(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,/* @__PURE__ */ new Set]));for(let e of t.groups)for(let t of e.files){let r=n.get(t);if(r)for(let n of e.files)n!==t&&r.add(n)}for(let t of e)t.crossFilePartners=[...n.get(t.file)??[]].toSorted()}function k(e){let t=e.toSorted((e,t)=>e-t);return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>1;t[i]<e?n=i+1:r=i}return t.length===0?0:n/t.length}}function A(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity||n.cognitiveComplexity===t.cognitiveComplexity&&n.ncss>t.ncss)&&(t=n);if(t)return{name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,cognitiveComplexity:t.cognitiveComplexity,ncss:t.ncss,nestingDepth:t.nestingDepth}}function j(e,t,n){let r=t.slice(0,n.top);g(JSON.stringify({summary:P(e.files),totalRankedFiles:t.length,truncated:r.length<t.length,files:r,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
- `)}function M(e,t,n,r){if(t.fatalError){h(`Error: ${t.fatalError}\n`);return}let i=P(t.files);if(g(`Measured ${i.fileCount} files under ${e} (code LOC ${i.linesOfCode}, NCSS ${i.ncssCount}, functions ${i.functionCount})\n`),n.length===0)g(`No measurable files found.
4
- `);else{let e=n.slice(0,r.top),t=n.length>e.length?` of ${n.length}`:``;g(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())g(`${t+1}. ${N(n)}\n`)}if(t.warnings.length>0){h(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))h(`- ${e}\n`);t.warnings.length>10&&h(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){h(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))h(`- ${e}\n`);t.errors.length>10&&h(`- ... ${t.errors.length-10} more\n`)}}function N(e){let t=[];if(e.worstFunction){let n=e.worstFunction;t.push(`worst function ${n.name} (L${n.startLine}-${n.endLine}) cognitive ${n.cognitiveComplexity}, NCSS ${n.ncss}, nesting ${n.nestingDepth}`)}if(e.duplicatedLineCount>0){let n=e.crossFilePartners.length>0?`, shared with ${e.crossFilePartners.slice(0,3).join(`, `)}${e.crossFilePartners.length>3?`, ...`:``}`:``;t.push(`duplicated lines ${e.duplicatedLineCount} (${Math.round(e.duplicatedLineRatio*100)}%${n})`)}return t.push(`file NCSS ${e.ncss}`),`${e.file} (score ${e.score.toFixed(2)}): ${t.join(`; `)}`}function P(e){let t=0,n=0,r=0,i=0;for(let a of e)t+=a.metrics.functions.length,n+=a.metrics.lines.code,r=Math.max(r,a.metrics.maxCognitiveComplexity),i+=a.metrics.ncssCount;return{fileCount:e.length,functionCount:t,linesOfCode:n,maxCognitiveComplexity:r,ncssCount:i}}function F(e){if(e===`off`)return 1/0;let t=e.trim()===``?NaN:Number(e);if(!Number.isFinite(t)||t<0)throw new x(`Expected a non-negative number or "off".`);return t}function I(e){let t=R(e);if(t>100)throw new x(`Expected an integer between 1 and 100.`);return t}function L(e){if(!/^\d+$/u.test(e))throw new x(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new x(`Expected a non-negative integer.`);return t}function R(e){let t=y(e);if(t===void 0)throw new x(`Expected a positive integer.`);return t}
2
+ import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,levels as r,thresholds as i}from"./thresholds.js";import{configFileName as a,loadConfig as o,resolveOptions as s}from"./cliConfig.js";import{loadExclusion as c}from"./exclusion.js";import{addCrossFileDuplication as l,collectDuplicatedLineNumbers as u,configSearchDirectory as d,formatPath as f,resolveTarget as p,scanTarget as m,writeStderr as h,writeStdout as g}from"./scan.js";import{qualifyFunctionName as _}from"./thresholdCheck.js";import{runCheckCommand as v}from"./checkCommand.js";import{getErrorMessage as y,parsePositiveInteger as b}from"@willbooster/shared-lib";import{Command as x,InvalidArgumentError as S}from"commander";C().catch(e=>{h(`Error: ${y(e)}\n`),process.exitCode=1});async function C(){let e=new x().name(`code-gauge`).description(`Measure code metrics: rank refactoring candidates (the default command) or check thresholds.`);w(e.command(`rank`,{isDefault:!0}).description(`Rank the files of a project by refactoring priority`).argument(`[target]`,`file or directory to measure`,`.`)).option(`--top <number>`,`number of top-ranked files to report (default: 10)`,z).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`).action(D);let t=w(e.command(`check`).description(`Report every exceeded threshold as a warning or an error (exit 1 on errors, 2 when files cannot be measured); with --base, only in what the change touches`).argument(`[target]`,`file or directory to check`,`.`)).option(`--base <ref>`,`check only what the working tree changed since the merge-base of this git ref and HEAD`);for(let e of r)for(let{key:n,defaultLimits:r}of i){let i=r[e];t.option(`--${e}-${E(n)} <number>`,`${T(n,e)} (default ${Number.isFinite(i)?i:`off`}); "off" disables it`,I)}t.exitOverride(e=>process.exit(e.exitCode===0?0:2)),t.action(async(e,t)=>{let n=e=>Object.fromEntries(i.flatMap(({key:n})=>{let r=t[`${e}${n.charAt(0).toUpperCase()}${n.slice(1)}`];return r===void 0?[]:[[n,r]]}));await v(e,t,{warning:n(`warning`),error:n(`error`)})}),await e.parseAsync()}function w(e){return e.option(`--config <path>`,`config file to use instead of the auto-detected ${a}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,z).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,R).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,L).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}function T(r,i){if(r===e.key)return`code-line count from which a duplicated block is ${i===`error`?`an error`:`a warning`}`;let a=t.find(e=>e.key===r);return`maximum ${a?a.label:`${n.find(e=>e.key===r)?.label} of a function`} without ${i===`error`?`an error`:`a warning`}`}function E(e){return e.replaceAll(/[A-Z]/gu,e=>`-${e.toLowerCase()}`)}async function D(e,t){let n=p(e),r=await d(n),i=await o(t.config,r),a=s(t,i),u=await m(n,{...a,loadExclusion:e=>c(r,a.exclude,e)});l(u,a);let f=O(u,a.top);a.json?M(u,f,a):N(n,u,f,a),(u.fatalError||a.failOnError&&u.errors.length>0)&&(process.exitCode=1)}function O(e,t){let n=e.files.map(({file:t,metrics:n})=>{let r=f(t,e.displayRoot),i=u(n,e.crossFileDuplication,r);return{file:r,worstFunction:j(n.functions),duplicatedLineCount:i.size,duplicatedLineRatio:n.lines.code===0?0:i.size/n.lines.code,crossFilePartners:[],ncss:n.ncssCount,codeLines:n.lines.code}}),r=A(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),i=A(n.map(e=>e.duplicatedLineCount)),a=A(n.map(e=>e.ncss)),o=n.map(e=>({...e,score:r(e.worstFunction?.cognitiveComplexity??0)+i(e.duplicatedLineCount)+a(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return k(o.slice(0,t),e.crossFileDuplication),o}function k(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,/* @__PURE__ */ new Set]));for(let e of t.groups)for(let t of e.files){let r=n.get(t);if(r)for(let n of e.files)n!==t&&r.add(n)}for(let t of e)t.crossFilePartners=[...n.get(t.file)??[]].toSorted()}function A(e){let t=e.toSorted((e,t)=>e-t);return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>1;t[i]<e?n=i+1:r=i}return t.length===0?0:n/t.length}}function j(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity||n.cognitiveComplexity===t.cognitiveComplexity&&n.ncss>t.ncss)&&(t=n);if(t)return{name:_(t),startLine:t.startLine,endLine:t.endLine,cognitiveComplexity:t.cognitiveComplexity,ncss:t.ncss,nestingDepth:t.nestingDepth}}function M(e,t,n){let r=t.slice(0,n.top);g(JSON.stringify({summary:F(e.files),totalRankedFiles:t.length,truncated:r.length<t.length,files:r,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
+ `)}function N(e,t,n,r){if(t.fatalError){h(`Error: ${t.fatalError}\n`);return}let i=F(t.files);if(g(`Measured ${i.fileCount} files under ${e} (code LOC ${i.linesOfCode}, NCSS ${i.ncssCount}, functions ${i.functionCount})\n`),n.length===0)g(`No measurable files found.
4
+ `);else{let e=n.slice(0,r.top),t=n.length>e.length?` of ${n.length}`:``;g(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())g(`${t+1}. ${P(n)}\n`)}if(t.warnings.length>0){h(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))h(`- ${e}\n`);t.warnings.length>10&&h(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){h(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))h(`- ${e}\n`);t.errors.length>10&&h(`- ... ${t.errors.length-10} more\n`)}}function P(e){let t=[];if(e.worstFunction){let n=e.worstFunction;t.push(`worst function ${n.name} (L${n.startLine}-${n.endLine}) cognitive ${n.cognitiveComplexity}, NCSS ${n.ncss}, nesting ${n.nestingDepth}`)}if(e.duplicatedLineCount>0){let n=e.crossFilePartners.length>0?`, shared with ${e.crossFilePartners.slice(0,3).join(`, `)}${e.crossFilePartners.length>3?`, ...`:``}`:``;t.push(`duplicated lines ${e.duplicatedLineCount} (${Math.round(e.duplicatedLineRatio*100)}%${n})`)}return t.push(`file NCSS ${e.ncss}`),`${e.file} (score ${e.score.toFixed(2)}): ${t.join(`; `)}`}function F(e){let t=0,n=0,r=0,i=0;for(let a of e)t+=a.metrics.functions.length,n+=a.metrics.lines.code,r=Math.max(r,a.metrics.maxCognitiveComplexity),i+=a.metrics.ncssCount;return{fileCount:e.length,functionCount:t,linesOfCode:n,maxCognitiveComplexity:r,ncssCount:i}}function I(e){if(e===`off`)return 1/0;let t=e.trim()===``?NaN:Number(e);if(!Number.isFinite(t)||t<0)throw new S(`Expected a non-negative number or "off".`);return t}function L(e){let t=z(e);if(t>100)throw new S(`Expected an integer between 1 and 100.`);return t}function R(e){if(!/^\d+$/u.test(e))throw new S(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new S(`Expected a non-negative integer.`);return t}function z(e){let t=b(e);if(t===void 0)throw new S(`Expected a positive integer.`);return t}
5
5
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command, InvalidArgumentError } from 'commander';\nimport { getErrorMessage, parsePositiveInteger } from '@willbooster/shared-lib';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runCheckCommand, type CheckCliOptions } from './checkCommand.js';\nimport { loadExclusion } from './exclusion.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n thresholds,\n type Level,\n type Limits,\n} from './thresholds.js';\nimport type { FunctionMetrics } from './types.js';\n\n/** The worst (highest-cognitive-complexity) function of a file, reported as the ranking evidence. */\ninterface WorstFunction {\n name: string;\n startLine: number;\n endLine: number;\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n}\n\n/**\n * One ranked refactoring candidate. The score is the sum of the file's repo-relative percentile\n * ranks (each in [0, 1)) over three dimensions: worst-function cognitive complexity, duplicated\n * lines (within-file and cross-file combined), and file NCSS. Ranking is relative to the scanned\n * project, so no absolute threshold is involved.\n */\ninterface RankedFile {\n file: string;\n score: number;\n worstFunction?: WorstFunction;\n /** Distinct lines covered by within-file duplicate blocks or cross-file duplicate occurrences. */\n duplicatedLineCount: number;\n /** duplicatedLineCount / code lines (0 when the file has no code). */\n duplicatedLineRatio: number;\n /** Other files sharing cross-file duplicate blocks; filled only for the reported top files. */\n crossFilePartners: string[];\n ncss: number;\n codeLines: number;\n}\n\n/** Caps how many cross-file partners a ranked file lists so the report stays scannable. */\nconst maxCrossFilePartners = 3;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Measure code metrics: rank refactoring candidates (the default command) or check thresholds.');\n\n addSharedOptions(\n program\n .command('rank', { isDefault: true })\n .description('Rank the files of a project by refactoring priority')\n .argument('[target]', 'file or directory to measure', '.')\n )\n .option('--top <number>', 'number of top-ranked files to report (default: 10)', parsePositiveIntegerOption)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .action(runRankCommand);\n\n const check = addSharedOptions(\n program\n .command('check')\n .description(\n 'Report every exceeded threshold as a warning or an error (exit 1 on errors, 2 when files cannot be measured); with --base, only in what the change touches'\n )\n .argument('[target]', 'file or directory to check', '.')\n ).option('--base <ref>', 'check only what the working tree changed since the merge-base of this git ref and HEAD');\n for (const level of levels) {\n for (const { key, defaultLimits } of thresholds) {\n const defaultLimit = defaultLimits[level];\n check.option(\n `--${level}-${toKebabCase(key)} <number>`,\n `${describeLimit(key, level)} (default ${Number.isFinite(defaultLimit) ? defaultLimit : 'off'}); \"off\" disables it`,\n parseLimit\n );\n }\n }\n // Exit code 1 means error-level violations, so a rejected command line must not use it.\n check.exitOverride((error) => process.exit(error.exitCode === 0 ? 0 : 2));\n check.action(async (target: string, cliOptions: CheckCliOptions & Record<string, unknown>) => {\n const readLimits = (level: Level): Limits =>\n Object.fromEntries(\n thresholds.flatMap(({ key }) => {\n const limit = cliOptions[`${level}${key.charAt(0).toUpperCase()}${key.slice(1)}`];\n return limit === undefined ? [] : [[key, limit as number]];\n })\n );\n await runCheckCommand(target, cliOptions, { warning: readLimits('warning'), error: readLimits('error') });\n });\n\n await program.parseAsync();\n}\n\n/** Registers the options shared by the ranking and check commands. */\nfunction addSharedOptions(command: Command): Command {\n return command\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveIntegerOption\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output');\n}\n\nfunction describeLimit(key: string, level: Level): string {\n if (key === duplicationThreshold.key) {\n return `line count from which a duplicated block is ${level === 'error' ? 'an error' : 'a warning'}`;\n }\n const fileThreshold = fileThresholds.find((threshold) => threshold.key === key);\n const subject = fileThreshold\n ? fileThreshold.label\n : `${functionThresholds.find((threshold) => threshold.key === key)?.label} of a function`;\n return `maximum ${subject} without ${level === 'error' ? 'an error' : 'a warning'}`;\n}\n\nfunction toKebabCase(key: string): string {\n return key.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);\n}\n\nasync function runRankCommand(target: string, cliOptions: CliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const searchDirectory = await configSearchDirectory(resolvedTarget);\n const config = await loadConfig(cliOptions.config, searchDirectory);\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n addCrossFileDuplication(result, options);\n const rankedFiles = rankFiles(result, options.top);\n\n if (options.json) {\n printJson(result, rankedFiles, options);\n } else {\n printTextReport(resolvedTarget, result, rankedFiles, options);\n }\n\n if (result.fatalError || (options.failOnError && result.errors.length > 0)) {\n process.exitCode = 1;\n }\n}\n\nfunction rankFiles(result: ScanResult, top: number): RankedFile[] {\n const candidates = result.files.map(({ file, metrics }) => {\n const formattedFile = formatPath(file, result.displayRoot);\n const duplicatedLines = collectDuplicatedLineNumbers(metrics, result.crossFileDuplication, formattedFile);\n return {\n file: formattedFile,\n worstFunction: findWorstFunction(metrics.functions),\n duplicatedLineCount: duplicatedLines.size,\n duplicatedLineRatio: metrics.lines.code === 0 ? 0 : duplicatedLines.size / metrics.lines.code,\n crossFilePartners: [] as string[],\n ncss: metrics.ncssCount,\n codeLines: metrics.lines.code,\n };\n });\n\n const cognitivePercentile = makePercentile(candidates.map((c) => c.worstFunction?.cognitiveComplexity ?? 0));\n const duplicationPercentile = makePercentile(candidates.map((c) => c.duplicatedLineCount));\n const ncssPercentile = makePercentile(candidates.map((c) => c.ncss));\n\n const ranked = candidates\n .map((candidate) => ({\n ...candidate,\n score:\n cognitivePercentile(candidate.worstFunction?.cognitiveComplexity ?? 0) +\n duplicationPercentile(candidate.duplicatedLineCount) +\n ncssPercentile(candidate.ncss),\n }))\n .toSorted(\n (left, right) => right.score - left.score || right.ncss - left.ncss || left.file.localeCompare(right.file)\n );\n // Partner evidence is attached only to the files that will be reported: expanding partners for\n // every scanned file first would retain TH(F^2) strings when one clone group spans F files, and\n // rescanning all groups per file would be O(files x groups) on the post-scan ranking step.\n attachCrossFilePartners(ranked.slice(0, top), result.crossFileDuplication);\n return ranked;\n}\n\n/** One pass over the groups fills the reported files' partner lists (other files sharing a group). */\nfunction attachCrossFilePartners(\n reportedFiles: RankedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined\n): void {\n if (!crossFileDuplication) {\n return;\n }\n const partnersByFile = new Map(reportedFiles.map((ranked) => [ranked.file, new Set<string>()]));\n for (const group of crossFileDuplication.groups) {\n for (const file of group.files) {\n const partners = partnersByFile.get(file);\n if (!partners) {\n continue;\n }\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n for (const ranked of reportedFiles) {\n ranked.crossFilePartners = [...(partnersByFile.get(ranked.file) ?? [])].toSorted();\n }\n}\n\n/**\n * Percentile rank within the scanned project: the fraction of files with a strictly smaller\n * value, in [0, 1). Relative ranking needs no absolute threshold, which sidesteps the metric\n * calibration problem entirely — the top of the list is worth refactoring first regardless of\n * where any cutoff would sit.\n */\nfunction makePercentile(values: number[]): (value: number) => number {\n const sorted = values.toSorted((left, right) => left - right);\n return (value: number) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((sorted[middle] as number) < value) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return sorted.length === 0 ? 0 : low / sorted.length;\n };\n}\n\n/** The highest-cognitive-complexity function; NCSS breaks ties so the larger body is reported. */\nfunction findWorstFunction(functions: FunctionMetrics[]): WorstFunction | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (\n !worst ||\n fn.cognitiveComplexity > worst.cognitiveComplexity ||\n (fn.cognitiveComplexity === worst.cognitiveComplexity && fn.ncss > worst.ncss)\n ) {\n worst = fn;\n }\n }\n if (!worst) {\n return undefined;\n }\n return {\n name: worst.name ?? '<anonymous>',\n startLine: worst.startLine,\n endLine: worst.endLine,\n cognitiveComplexity: worst.cognitiveComplexity,\n ncss: worst.ncss,\n nestingDepth: worst.nestingDepth,\n };\n}\n\nfunction printJson(result: ScanResult, rankedFiles: RankedFile[], options: ResolvedOptions): void {\n const reportedFiles = rankedFiles.slice(0, options.top);\n writeStdout(\n JSON.stringify(\n {\n summary: summarize(result.files),\n totalRankedFiles: rankedFiles.length,\n truncated: reportedFiles.length < rankedFiles.length,\n files: reportedFiles,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(\n target: string,\n result: ScanResult,\n rankedFiles: RankedFile[],\n options: ResolvedOptions\n): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(\n `Measured ${summary.fileCount} files under ${target} (code LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount})\\n`\n );\n\n if (rankedFiles.length === 0) {\n writeStdout('No measurable files found.\\n');\n } else {\n const reportedFiles = rankedFiles.slice(0, options.top);\n const totalSuffix = rankedFiles.length > reportedFiles.length ? ` of ${rankedFiles.length}` : '';\n writeStdout(`\\nRefactoring candidates (top ${reportedFiles.length}${totalSuffix}):\\n`);\n for (const [index, ranked] of reportedFiles.entries()) {\n writeStdout(`${index + 1}. ${formatRankedFile(ranked)}\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\n/** One ranked file as a single line: the location, the evidence, and where the duplication points. */\nfunction formatRankedFile(ranked: RankedFile): string {\n const reasons: string[] = [];\n if (ranked.worstFunction) {\n const fn = ranked.worstFunction;\n reasons.push(\n `worst function ${fn.name} (L${fn.startLine}-${fn.endLine}) cognitive ${fn.cognitiveComplexity}, NCSS ${fn.ncss}, nesting ${fn.nestingDepth}`\n );\n }\n if (ranked.duplicatedLineCount > 0) {\n const partnersSuffix =\n ranked.crossFilePartners.length > 0\n ? `, shared with ${ranked.crossFilePartners.slice(0, maxCrossFilePartners).join(', ')}${ranked.crossFilePartners.length > maxCrossFilePartners ? ', ...' : ''}`\n : '';\n reasons.push(\n `duplicated lines ${ranked.duplicatedLineCount} (${Math.round(ranked.duplicatedLineRatio * 100)}%${partnersSuffix})`\n );\n }\n reasons.push(`file NCSS ${ranked.ncss}`);\n return `${ranked.file} (score ${ranked.score.toFixed(2)}): ${reasons.join('; ')}`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n ncssCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functions.length;\n linesOfCode += file.metrics.lines.code;\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n }\n\n return { fileCount: files.length, functionCount, linesOfCode, maxCognitiveComplexity, ncssCount };\n}\n\nfunction parseLimit(value: string): number {\n if (value === 'off') {\n return Infinity;\n }\n const parsed = value.trim() === '' ? Number.NaN : Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative number or \"off\".');\n }\n return parsed;\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveIntegerOption(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveIntegerOption(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed === undefined) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command, InvalidArgumentError } from 'commander';\nimport { getErrorMessage, parsePositiveInteger } from '@willbooster/shared-lib';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runCheckCommand, type CheckCliOptions } from './checkCommand.js';\nimport { loadExclusion } from './exclusion.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n thresholds,\n type Level,\n type Limits,\n} from './thresholds.js';\nimport { qualifyFunctionName } from './thresholdCheck.js';\nimport type { FunctionMetrics } from './types.js';\n\n/** The worst (highest-cognitive-complexity) function of a file, reported as the ranking evidence. */\ninterface WorstFunction {\n name: string;\n startLine: number;\n endLine: number;\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n}\n\n/**\n * One ranked refactoring candidate. The score is the sum of the file's repo-relative percentile\n * ranks (each in [0, 1)) over three dimensions: worst-function cognitive complexity, duplicated\n * lines (within-file and cross-file combined), and file NCSS. Ranking is relative to the scanned\n * project, so no absolute threshold is involved.\n */\ninterface RankedFile {\n file: string;\n score: number;\n worstFunction?: WorstFunction;\n /** Distinct lines covered by within-file duplicate blocks or cross-file duplicate occurrences. */\n duplicatedLineCount: number;\n /** duplicatedLineCount / code lines (0 when the file has no code). */\n duplicatedLineRatio: number;\n /** Other files sharing cross-file duplicate blocks; filled only for the reported top files. */\n crossFilePartners: string[];\n ncss: number;\n codeLines: number;\n}\n\n/** Caps how many cross-file partners a ranked file lists so the report stays scannable. */\nconst maxCrossFilePartners = 3;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Measure code metrics: rank refactoring candidates (the default command) or check thresholds.');\n\n addSharedOptions(\n program\n .command('rank', { isDefault: true })\n .description('Rank the files of a project by refactoring priority')\n .argument('[target]', 'file or directory to measure', '.')\n )\n .option('--top <number>', 'number of top-ranked files to report (default: 10)', parsePositiveIntegerOption)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .action(runRankCommand);\n\n const check = addSharedOptions(\n program\n .command('check')\n .description(\n 'Report every exceeded threshold as a warning or an error (exit 1 on errors, 2 when files cannot be measured); with --base, only in what the change touches'\n )\n .argument('[target]', 'file or directory to check', '.')\n ).option('--base <ref>', 'check only what the working tree changed since the merge-base of this git ref and HEAD');\n for (const level of levels) {\n for (const { key, defaultLimits } of thresholds) {\n const defaultLimit = defaultLimits[level];\n check.option(\n `--${level}-${toKebabCase(key)} <number>`,\n `${describeLimit(key, level)} (default ${Number.isFinite(defaultLimit) ? defaultLimit : 'off'}); \"off\" disables it`,\n parseLimit\n );\n }\n }\n // Exit code 1 means error-level violations, so a rejected command line must not use it.\n check.exitOverride((error) => process.exit(error.exitCode === 0 ? 0 : 2));\n check.action(async (target: string, cliOptions: CheckCliOptions & Record<string, unknown>) => {\n const readLimits = (level: Level): Limits =>\n Object.fromEntries(\n thresholds.flatMap(({ key }) => {\n const limit = cliOptions[`${level}${key.charAt(0).toUpperCase()}${key.slice(1)}`];\n return limit === undefined ? [] : [[key, limit as number]];\n })\n );\n await runCheckCommand(target, cliOptions, { warning: readLimits('warning'), error: readLimits('error') });\n });\n\n await program.parseAsync();\n}\n\n/** Registers the options shared by the ranking and check commands. */\nfunction addSharedOptions(command: Command): Command {\n return command\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveIntegerOption\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output');\n}\n\nfunction describeLimit(key: string, level: Level): string {\n if (key === duplicationThreshold.key) {\n return `code-line count from which a duplicated block is ${level === 'error' ? 'an error' : 'a warning'}`;\n }\n const fileThreshold = fileThresholds.find((threshold) => threshold.key === key);\n const subject = fileThreshold\n ? fileThreshold.label\n : `${functionThresholds.find((threshold) => threshold.key === key)?.label} of a function`;\n return `maximum ${subject} without ${level === 'error' ? 'an error' : 'a warning'}`;\n}\n\nfunction toKebabCase(key: string): string {\n return key.replaceAll(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);\n}\n\nasync function runRankCommand(target: string, cliOptions: CliOptions): Promise<void> {\n const resolvedTarget = resolveTarget(target);\n const searchDirectory = await configSearchDirectory(resolvedTarget);\n const config = await loadConfig(cliOptions.config, searchDirectory);\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n addCrossFileDuplication(result, options);\n const rankedFiles = rankFiles(result, options.top);\n\n if (options.json) {\n printJson(result, rankedFiles, options);\n } else {\n printTextReport(resolvedTarget, result, rankedFiles, options);\n }\n\n if (result.fatalError || (options.failOnError && result.errors.length > 0)) {\n process.exitCode = 1;\n }\n}\n\nfunction rankFiles(result: ScanResult, top: number): RankedFile[] {\n const candidates = result.files.map(({ file, metrics }) => {\n const formattedFile = formatPath(file, result.displayRoot);\n const duplicatedLines = collectDuplicatedLineNumbers(metrics, result.crossFileDuplication, formattedFile);\n return {\n file: formattedFile,\n worstFunction: findWorstFunction(metrics.functions),\n duplicatedLineCount: duplicatedLines.size,\n duplicatedLineRatio: metrics.lines.code === 0 ? 0 : duplicatedLines.size / metrics.lines.code,\n crossFilePartners: [] as string[],\n ncss: metrics.ncssCount,\n codeLines: metrics.lines.code,\n };\n });\n\n const cognitivePercentile = makePercentile(candidates.map((c) => c.worstFunction?.cognitiveComplexity ?? 0));\n const duplicationPercentile = makePercentile(candidates.map((c) => c.duplicatedLineCount));\n const ncssPercentile = makePercentile(candidates.map((c) => c.ncss));\n\n const ranked = candidates\n .map((candidate) => ({\n ...candidate,\n score:\n cognitivePercentile(candidate.worstFunction?.cognitiveComplexity ?? 0) +\n duplicationPercentile(candidate.duplicatedLineCount) +\n ncssPercentile(candidate.ncss),\n }))\n .toSorted(\n (left, right) => right.score - left.score || right.ncss - left.ncss || left.file.localeCompare(right.file)\n );\n // Partner evidence is attached only to the files that will be reported: expanding partners for\n // every scanned file first would retain TH(F^2) strings when one clone group spans F files, and\n // rescanning all groups per file would be O(files x groups) on the post-scan ranking step.\n attachCrossFilePartners(ranked.slice(0, top), result.crossFileDuplication);\n return ranked;\n}\n\n/** One pass over the groups fills the reported files' partner lists (other files sharing a group). */\nfunction attachCrossFilePartners(\n reportedFiles: RankedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined\n): void {\n if (!crossFileDuplication) {\n return;\n }\n const partnersByFile = new Map(reportedFiles.map((ranked) => [ranked.file, new Set<string>()]));\n for (const group of crossFileDuplication.groups) {\n for (const file of group.files) {\n const partners = partnersByFile.get(file);\n if (!partners) {\n continue;\n }\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n for (const ranked of reportedFiles) {\n ranked.crossFilePartners = [...(partnersByFile.get(ranked.file) ?? [])].toSorted();\n }\n}\n\n/**\n * Percentile rank within the scanned project: the fraction of files with a strictly smaller\n * value, in [0, 1). Relative ranking needs no absolute threshold, which sidesteps the metric\n * calibration problem entirely — the top of the list is worth refactoring first regardless of\n * where any cutoff would sit.\n */\nfunction makePercentile(values: number[]): (value: number) => number {\n const sorted = values.toSorted((left, right) => left - right);\n return (value: number) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((sorted[middle] as number) < value) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return sorted.length === 0 ? 0 : low / sorted.length;\n };\n}\n\n/** The highest-cognitive-complexity function; NCSS breaks ties so the larger body is reported. */\nfunction findWorstFunction(functions: FunctionMetrics[]): WorstFunction | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (\n !worst ||\n fn.cognitiveComplexity > worst.cognitiveComplexity ||\n (fn.cognitiveComplexity === worst.cognitiveComplexity && fn.ncss > worst.ncss)\n ) {\n worst = fn;\n }\n }\n if (!worst) {\n return undefined;\n }\n return {\n name: qualifyFunctionName(worst),\n startLine: worst.startLine,\n endLine: worst.endLine,\n cognitiveComplexity: worst.cognitiveComplexity,\n ncss: worst.ncss,\n nestingDepth: worst.nestingDepth,\n };\n}\n\nfunction printJson(result: ScanResult, rankedFiles: RankedFile[], options: ResolvedOptions): void {\n const reportedFiles = rankedFiles.slice(0, options.top);\n writeStdout(\n JSON.stringify(\n {\n summary: summarize(result.files),\n totalRankedFiles: rankedFiles.length,\n truncated: reportedFiles.length < rankedFiles.length,\n files: reportedFiles,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(\n target: string,\n result: ScanResult,\n rankedFiles: RankedFile[],\n options: ResolvedOptions\n): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(\n `Measured ${summary.fileCount} files under ${target} (code LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount})\\n`\n );\n\n if (rankedFiles.length === 0) {\n writeStdout('No measurable files found.\\n');\n } else {\n const reportedFiles = rankedFiles.slice(0, options.top);\n const totalSuffix = rankedFiles.length > reportedFiles.length ? ` of ${rankedFiles.length}` : '';\n writeStdout(`\\nRefactoring candidates (top ${reportedFiles.length}${totalSuffix}):\\n`);\n for (const [index, ranked] of reportedFiles.entries()) {\n writeStdout(`${index + 1}. ${formatRankedFile(ranked)}\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\n/** One ranked file as a single line: the location, the evidence, and where the duplication points. */\nfunction formatRankedFile(ranked: RankedFile): string {\n const reasons: string[] = [];\n if (ranked.worstFunction) {\n const fn = ranked.worstFunction;\n reasons.push(\n `worst function ${fn.name} (L${fn.startLine}-${fn.endLine}) cognitive ${fn.cognitiveComplexity}, NCSS ${fn.ncss}, nesting ${fn.nestingDepth}`\n );\n }\n if (ranked.duplicatedLineCount > 0) {\n const partnersSuffix =\n ranked.crossFilePartners.length > 0\n ? `, shared with ${ranked.crossFilePartners.slice(0, maxCrossFilePartners).join(', ')}${ranked.crossFilePartners.length > maxCrossFilePartners ? ', ...' : ''}`\n : '';\n reasons.push(\n `duplicated lines ${ranked.duplicatedLineCount} (${Math.round(ranked.duplicatedLineRatio * 100)}%${partnersSuffix})`\n );\n }\n reasons.push(`file NCSS ${ranked.ncss}`);\n return `${ranked.file} (score ${ranked.score.toFixed(2)}): ${reasons.join('; ')}`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n ncssCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functions.length;\n linesOfCode += file.metrics.lines.code;\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n }\n\n return { fileCount: files.length, functionCount, linesOfCode, maxCognitiveComplexity, ncssCount };\n}\n\nfunction parseLimit(value: string): number {\n if (value === 'off') {\n return Infinity;\n }\n const parsed = value.trim() === '' ? Number.NaN : Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative number or \"off\".');\n }\n return parsed;\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveIntegerOption(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveIntegerOption(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed === undefined) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export type { CrossFileDuplicateBlockGroup, CrossFileDuplicateOccurrence, CrossF
3
3
  export type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';
4
4
  export { defaultLanguages, detectLanguage, supportedLanguages } from './languages.js';
5
5
  export { TreeMeasurer, collectCrossFileDuplicationFileData, collectDuplicationCandidates, collectFunctionTokenSequences, defaultMeasurer, measureCode, } from './metrics.js';
6
- export type { CodeMetrics, DuplicationMetrics, DuplicationOptions, FunctionMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, LineMetrics, MeasureOptions, SupportedLanguage, } from './types.js';
6
+ export type { CodeMetrics, CognitiveBlock, DuplicationMetrics, DuplicationOptions, FunctionMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, LineMetrics, MeasureOptions, SupportedLanguage, } from './types.js';
package/dist/metrics.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./languages.cjs"),t=require("./nativeMetrics.cjs");var n=class{registry=e.createLanguageRegistry();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,n){let r=this.resolveLanguage(n.language),i=n.includeSyntaxTree??!1;return a(t.measureCodeNative(e,r.name,i,n.duplication),i)}measureWithCrossFileData(e,n){let i=this.resolveLanguage(n.language),a=n.includeSyntaxTree??!1;return r(t.measureCodeNative(e,i.name,a,n.duplication,!0),a)}async measureWithCrossFileDataAsync(e,n){let i=this.resolveLanguage(n.language),a=n.includeSyntaxTree??!1;return r(await t.measureCodeNativeAsync(e,i.name,a,n.duplication,!0),a)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language);return i(t.collectCrossFileDataNative(e,r.name,n.duplication?.minTokens))}collectFunctionTokenSequences(e,n){let r=this.resolveLanguage(n.language);return t.collectFunctionTokenSequencesNative(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function r(e,t){if(!e.crossFileData)throw Error(`The native addon returned no cross-file duplication data`);return{metrics:a(e,t),crossFileData:i(e.crossFileData)}}function i(e){return{candidates:e.candidates,tokens:e.tokens,containerStatements:e.containerStatements,nearMissBlocks:e.nearMissBlocks,codeLineNumbers:new Set(e.codeLineNumbers)}}function a(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:o(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:o(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function o(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a),c=n===0?0:t/2*(i/n);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,difficulty:c,effort:c*s}}const s=new n;function c(e,t){return s.measure(e,t)}function l(e,t){return s.measureWithCrossFileDataAsync(e,t)}function u(e,t){return s.collectDuplicationCandidates(e,t)}function d(e,t){return s.collectFunctionTokenSequences(e,t)}function f(e,t){return s.collectCrossFileDuplicationFileData(e,t)}exports.TreeMeasurer=n,exports.collectCrossFileDuplicationFileData=f,exports.collectDuplicationCandidates=u,exports.collectFunctionTokenSequences=d,exports.defaultMeasurer=s,exports.measureCode=c,exports.measureCodeWithCrossFileDataAsync=l;
1
+ "use strict";const e=require("./languages.cjs"),t=require("./nativeMetrics.cjs");var n=class{registry=e.createLanguageRegistry();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,n){let r=this.resolveLanguage(n.language),i=n.includeSyntaxTree??!1;return a(t.measureCodeNative(e,r.name,i,n.duplication),i)}measureWithCrossFileData(e,n){let i=this.resolveLanguage(n.language),a=n.includeSyntaxTree??!1;return r(t.measureCodeNative(e,i.name,a,n.duplication,!0),a)}async measureWithCrossFileDataAsync(e,n){let i=this.resolveLanguage(n.language),a=n.includeSyntaxTree??!1;return r(await t.measureCodeNativeAsync(e,i.name,a,n.duplication,!0),a)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language);return i(t.collectCrossFileDataNative(e,r.name,n.duplication?.minTokens))}collectFunctionTokenSequences(e,n){let r=this.resolveLanguage(n.language);return t.collectFunctionTokenSequencesNative(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function r(e,t){if(!e.crossFileData)throw Error(`The native addon returned no cross-file duplication data`);return{metrics:a(e,t),crossFileData:i(e.crossFileData)}}function i(e){return{candidates:e.candidates,tokens:e.tokens,containerStatements:e.containerStatements,nearMissBlocks:e.nearMissBlocks,codeLineNumbers:new Set(e.codeLineNumbers)}}function a(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,codeLineNumbers:e.codeLineNumbers,functions:e.functions.map(e=>({name:e.name,containerName:e.containerName,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,cognitiveBlocks:e.cognitiveBlocks??[],nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:o(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:o(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function o(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a),c=n===0?0:t/2*(i/n);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,difficulty:c,effort:c*s}}const s=new n;function c(e,t){return s.measure(e,t)}function l(e,t){return s.measureWithCrossFileDataAsync(e,t)}function u(e,t){return s.collectDuplicationCandidates(e,t)}function d(e,t){return s.collectFunctionTokenSequences(e,t)}function f(e,t){return s.collectCrossFileDuplicationFileData(e,t)}exports.TreeMeasurer=n,exports.collectCrossFileDuplicationFileData=f,exports.collectDuplicationCandidates=u,exports.collectFunctionTokenSequences=d,exports.defaultMeasurer=s,exports.measureCode=c,exports.measureCodeWithCrossFileDataAsync=l;
2
2
  //# sourceMappingURL=metrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs","names":["createLanguageRegistry","measureCodeNative","measureCodeNativeAsync","collectCrossFileDataNative","collectFunctionTokenSequencesNative"],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n measureCodeNativeAsync,\n type NativeCrossFileDataPayload,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Measures a file and collects its cross-file clone-detection contribution from one parse. */\n measureWithCrossFileData(\n code: string,\n options: MeasureOptions\n ): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** measureWithCrossFileData on the native addon's worker threads, for measuring files in parallel. */\n async measureWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n ): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = await measureCodeNativeAsync(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n return toCrossFileDuplicationFileData(\n collectCrossFileDataNative(code, language.name, options.duplication?.minTokens)\n );\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\nfunction assembleMetricsWithCrossFileData(\n payload: NativeMetricsPayload,\n includeSyntaxTree: boolean\n): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n if (!payload.crossFileData) {\n throw new Error('The native addon returned no cross-file duplication data');\n }\n return {\n metrics: assembleNativeMetrics(payload, includeSyntaxTree),\n crossFileData: toCrossFileDuplicationFileData(payload.crossFileData),\n };\n}\n\nfunction toCrossFileDuplicationFileData(payload: NativeCrossFileDataPayload): CrossFileDuplicationFileData {\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n nearMissBlocks: payload.nearMissBlocks,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n difficulty,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring TreeMeasurer.measureWithCrossFileDataAsync for the default measurer. */\nexport function measureCodeWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n return defaultMeasurer.measureWithCrossFileDataAsync(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.cjs","names":["createLanguageRegistry","measureCodeNative","measureCodeNativeAsync","collectCrossFileDataNative","collectFunctionTokenSequencesNative"],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n measureCodeNativeAsync,\n type NativeCrossFileDataPayload,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Measures a file and collects its cross-file clone-detection contribution from one parse. */\n measureWithCrossFileData(\n code: string,\n options: MeasureOptions\n ): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** measureWithCrossFileData on the native addon's worker threads, for measuring files in parallel. */\n async measureWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n ): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = await measureCodeNativeAsync(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n return toCrossFileDuplicationFileData(\n collectCrossFileDataNative(code, language.name, options.duplication?.minTokens)\n );\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\nfunction assembleMetricsWithCrossFileData(\n payload: NativeMetricsPayload,\n includeSyntaxTree: boolean\n): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n if (!payload.crossFileData) {\n throw new Error('The native addon returned no cross-file duplication data');\n }\n return {\n metrics: assembleNativeMetrics(payload, includeSyntaxTree),\n crossFileData: toCrossFileDuplicationFileData(payload.crossFileData),\n };\n}\n\nfunction toCrossFileDuplicationFileData(payload: NativeCrossFileDataPayload): CrossFileDuplicationFileData {\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n nearMissBlocks: payload.nearMissBlocks,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n codeLineNumbers: payload.codeLineNumbers,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n containerName: fn.containerName,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n cognitiveBlocks: fn.cognitiveBlocks ?? [],\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n difficulty,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring TreeMeasurer.measureWithCrossFileDataAsync for the default measurer. */\nexport function measureCodeWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n return defaultMeasurer.measureWithCrossFileDataAsync(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"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"}
package/dist/metrics.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createLanguageRegistry as e}from"./languages.js";import{collectCrossFileDataNative as t,collectFunctionTokenSequencesNative as n,measureCodeNative as r,measureCodeNativeAsync as i}from"./nativeMetrics.js";var a=class{registry=e();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return c(r(e,n.name,i,t.duplication),i)}measureWithCrossFileData(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return o(r(e,n.name,i,t.duplication,!0),i)}async measureWithCrossFileDataAsync(e,t){let n=this.resolveLanguage(t.language),r=t.includeSyntaxTree??!1;return o(await i(e,n.name,r,t.duplication,!0),r)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language);return s(t(e,r.name,n.duplication?.minTokens))}collectFunctionTokenSequences(e,t){let r=this.resolveLanguage(t.language);return n(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function o(e,t){if(!e.crossFileData)throw Error(`The native addon returned no cross-file duplication data`);return{metrics:c(e,t),crossFileData:s(e.crossFileData)}}function s(e){return{candidates:e.candidates,tokens:e.tokens,containerStatements:e.containerStatements,nearMissBlocks:e.nearMissBlocks,codeLineNumbers:new Set(e.codeLineNumbers)}}function c(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,functions:e.functions.map(e=>({name:e.name,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:l(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:l(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function l(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a),c=n===0?0:t/2*(i/n);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,difficulty:c,effort:c*s}}const u=new a;function d(e,t){return u.measure(e,t)}function f(e,t){return u.measureWithCrossFileDataAsync(e,t)}function p(e,t){return u.collectDuplicationCandidates(e,t)}function m(e,t){return u.collectFunctionTokenSequences(e,t)}function h(e,t){return u.collectCrossFileDuplicationFileData(e,t)}export{a as TreeMeasurer,h as collectCrossFileDuplicationFileData,p as collectDuplicationCandidates,m as collectFunctionTokenSequences,u as defaultMeasurer,d as measureCode,f as measureCodeWithCrossFileDataAsync};
1
+ import{createLanguageRegistry as e}from"./languages.js";import{collectCrossFileDataNative as t,collectFunctionTokenSequencesNative as n,measureCodeNative as r,measureCodeNativeAsync as i}from"./nativeMetrics.js";var a=class{registry=e();getSupportedLanguages(){return[...new Set([...this.registry.values()].map(e=>e.name))]}measure(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return c(r(e,n.name,i,t.duplication),i)}measureWithCrossFileData(e,t){let n=this.resolveLanguage(t.language),i=t.includeSyntaxTree??!1;return o(r(e,n.name,i,t.duplication,!0),i)}async measureWithCrossFileDataAsync(e,t){let n=this.resolveLanguage(t.language),r=t.includeSyntaxTree??!1;return o(await i(e,n.name,r,t.duplication,!0),r)}collectDuplicationCandidates(e,t){return this.collectCrossFileDuplicationFileData(e,t).candidates}collectCrossFileDuplicationFileData(e,n){let r=this.resolveLanguage(n.language);return s(t(e,r.name,n.duplication?.minTokens))}collectFunctionTokenSequences(e,t){let r=this.resolveLanguage(t.language);return n(e,r.name)}resolveLanguage(e){let t=this.registry.get(e);if(!t)throw Error(`Unsupported language: ${e}`);return t}};function o(e,t){if(!e.crossFileData)throw Error(`The native addon returned no cross-file duplication data`);return{metrics:c(e,t),crossFileData:s(e.crossFileData)}}function s(e){return{candidates:e.candidates,tokens:e.tokens,containerStatements:e.containerStatements,nearMissBlocks:e.nearMissBlocks,codeLineNumbers:new Set(e.codeLineNumbers)}}function c(e,t){return{language:e.language,bytes:e.bytes,lines:e.lines,codeLineNumbers:e.codeLineNumbers,functions:e.functions.map(e=>({name:e.name,containerName:e.containerName,nodeType:e.nodeType,startLine:e.startLine,startColumn:e.startColumn,endLine:e.endLine,endColumn:e.endColumn,cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,cognitiveBlocks:e.cognitiveBlocks??[],nestingDepth:e.nestingDepth,ncss:e.ncss,parameterCount:e.parameterCount,halstead:l(e.halsteadCounts),depDegree:e.depDegree})),cyclomaticComplexity:e.cyclomaticComplexity,cognitiveComplexity:e.cognitiveComplexity,maxCognitiveComplexity:e.maxCognitiveComplexity,nestingDepth:e.nestingDepth,ncssCount:e.ncssCount,duplication:e.duplication,halstead:l(e.halsteadCounts),syntaxTree:t?e.syntaxTree:void 0}}function l(e){let{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i}=e,a=t+n,o=r+i,s=a===0?0:o*Math.log2(a),c=n===0?0:t/2*(i/n);return{distinctOperators:t,distinctOperands:n,totalOperators:r,totalOperands:i,vocabulary:a,length:o,volume:s,difficulty:c,effort:c*s}}const u=new a;function d(e,t){return u.measure(e,t)}function f(e,t){return u.measureWithCrossFileDataAsync(e,t)}function p(e,t){return u.collectDuplicationCandidates(e,t)}function m(e,t){return u.collectFunctionTokenSequences(e,t)}function h(e,t){return u.collectCrossFileDuplicationFileData(e,t)}export{a as TreeMeasurer,h as collectCrossFileDuplicationFileData,p as collectDuplicationCandidates,m as collectFunctionTokenSequences,u as defaultMeasurer,d as measureCode,f as measureCodeWithCrossFileDataAsync};
2
2
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n measureCodeNativeAsync,\n type NativeCrossFileDataPayload,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Measures a file and collects its cross-file clone-detection contribution from one parse. */\n measureWithCrossFileData(\n code: string,\n options: MeasureOptions\n ): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** measureWithCrossFileData on the native addon's worker threads, for measuring files in parallel. */\n async measureWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n ): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = await measureCodeNativeAsync(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n return toCrossFileDuplicationFileData(\n collectCrossFileDataNative(code, language.name, options.duplication?.minTokens)\n );\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\nfunction assembleMetricsWithCrossFileData(\n payload: NativeMetricsPayload,\n includeSyntaxTree: boolean\n): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n if (!payload.crossFileData) {\n throw new Error('The native addon returned no cross-file duplication data');\n }\n return {\n metrics: assembleNativeMetrics(payload, includeSyntaxTree),\n crossFileData: toCrossFileDuplicationFileData(payload.crossFileData),\n };\n}\n\nfunction toCrossFileDuplicationFileData(payload: NativeCrossFileDataPayload): CrossFileDuplicationFileData {\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n nearMissBlocks: payload.nearMissBlocks,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n difficulty,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring TreeMeasurer.measureWithCrossFileDataAsync for the default measurer. */\nexport function measureCodeWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n return defaultMeasurer.measureWithCrossFileDataAsync(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import type { CrossFileDuplicateCandidate, CrossFileDuplicationFileData } from './duplication.js';\nimport { createLanguageRegistry } from './languages.js';\nimport {\n collectCrossFileDataNative,\n collectFunctionTokenSequencesNative,\n measureCodeNative,\n measureCodeNativeAsync,\n type NativeCrossFileDataPayload,\n type NativeHalsteadCounts,\n type NativeMetricsPayload,\n} from './nativeMetrics.js';\nimport type { CodeMetrics, HalsteadMetrics, LanguageDefinition, LanguageName, MeasureOptions } from './types.js';\n\n/**\n * Measures code metrics for the built-in languages. Parsing and metric passes run in the bundled\n * Rust addon (tree-sitter); this class resolves language aliases, crosses the N-API boundary, and\n * derives the Halstead float metrics from the natively measured counts.\n */\nexport class TreeMeasurer {\n private readonly registry = createLanguageRegistry();\n\n getSupportedLanguages(): LanguageName[] {\n return [...new Set([...this.registry.values()].map((language) => language.name))];\n }\n\n measure(code: string, options: MeasureOptions): CodeMetrics {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication);\n return assembleNativeMetrics(payload, includeSyntaxTree);\n }\n\n /** Measures a file and collects its cross-file clone-detection contribution from one parse. */\n measureWithCrossFileData(\n code: string,\n options: MeasureOptions\n ): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = measureCodeNative(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** measureWithCrossFileData on the native addon's worker threads, for measuring files in parallel. */\n async measureWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n ): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n const language = this.resolveLanguage(options.language);\n const includeSyntaxTree = options.includeSyntaxTree ?? false;\n const payload = await measureCodeNativeAsync(code, language.name, includeSyntaxTree, options.duplication, true);\n return assembleMetricsWithCrossFileData(payload, includeSyntaxTree);\n }\n\n /** Collects one file's duplicate candidates for cross-file clone detection. */\n collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return this.collectCrossFileDuplicationFileData(code, options).candidates;\n }\n\n /**\n * Collects one file's duplicate candidates, normalized tokens, and statement structure for\n * cross-file clone detection with measureCrossFileDuplication.\n */\n collectCrossFileDuplicationFileData(code: string, options: MeasureOptions): CrossFileDuplicationFileData {\n const language = this.resolveLanguage(options.language);\n return toCrossFileDuplicationFileData(\n collectCrossFileDataNative(code, language.name, options.duplication?.minTokens)\n );\n }\n\n /**\n * Normalized token hash sequences of every function, index-parallel to the functions array of\n * measure(): identifiers are anonymized by first occurrence within the function, literals by\n * kind, and keywords/operators kept verbatim.\n */\n collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n const language = this.resolveLanguage(options.language);\n return collectFunctionTokenSequencesNative(code, language.name);\n }\n\n private resolveLanguage(name: LanguageName): LanguageDefinition {\n const language = this.registry.get(name);\n if (!language) {\n throw new Error(`Unsupported language: ${name}`);\n }\n return language;\n }\n}\n\nfunction assembleMetricsWithCrossFileData(\n payload: NativeMetricsPayload,\n includeSyntaxTree: boolean\n): { metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData } {\n if (!payload.crossFileData) {\n throw new Error('The native addon returned no cross-file duplication data');\n }\n return {\n metrics: assembleNativeMetrics(payload, includeSyntaxTree),\n crossFileData: toCrossFileDuplicationFileData(payload.crossFileData),\n };\n}\n\nfunction toCrossFileDuplicationFileData(payload: NativeCrossFileDataPayload): CrossFileDuplicationFileData {\n return {\n candidates: payload.candidates,\n tokens: payload.tokens,\n containerStatements: payload.containerStatements,\n nearMissBlocks: payload.nearMissBlocks,\n // Lets cross-file line coverage count only code lines, like within-file coverage.\n codeLineNumbers: new Set(payload.codeLineNumbers),\n };\n}\n\n/**\n * Completes a native measurement into CodeMetrics. The object is rebuilt field by field (rather\n * than spread from the parsed JSON) so the result has a stable shape, including\n * explicitly-undefined optional keys.\n */\nfunction assembleNativeMetrics(payload: NativeMetricsPayload, includeSyntaxTree: boolean): CodeMetrics {\n return {\n language: payload.language,\n bytes: payload.bytes,\n lines: payload.lines,\n codeLineNumbers: payload.codeLineNumbers,\n functions: payload.functions.map((fn) => ({\n name: fn.name,\n containerName: fn.containerName,\n nodeType: fn.nodeType,\n startLine: fn.startLine,\n startColumn: fn.startColumn,\n endLine: fn.endLine,\n endColumn: fn.endColumn,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n cognitiveBlocks: fn.cognitiveBlocks ?? [],\n nestingDepth: fn.nestingDepth,\n ncss: fn.ncss,\n parameterCount: fn.parameterCount,\n halstead: deriveHalsteadMetrics(fn.halsteadCounts),\n depDegree: fn.depDegree,\n })),\n cyclomaticComplexity: payload.cyclomaticComplexity,\n cognitiveComplexity: payload.cognitiveComplexity,\n maxCognitiveComplexity: payload.maxCognitiveComplexity,\n nestingDepth: payload.nestingDepth,\n ncssCount: payload.ncssCount,\n duplication: payload.duplication,\n halstead: deriveHalsteadMetrics(payload.halsteadCounts),\n syntaxTree: includeSyntaxTree ? payload.syntaxTree : undefined,\n };\n}\n\nfunction deriveHalsteadMetrics(counts: NativeHalsteadCounts): HalsteadMetrics {\n const { distinctOperators, distinctOperands, totalOperators, totalOperands } = counts;\n const vocabulary = distinctOperators + distinctOperands;\n const length = totalOperators + totalOperands;\n const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);\n const difficulty = distinctOperands === 0 ? 0 : (distinctOperators / 2) * (totalOperands / distinctOperands);\n const effort = difficulty * volume;\n\n return {\n distinctOperators,\n distinctOperands,\n totalOperators,\n totalOperands,\n vocabulary,\n length,\n volume,\n difficulty,\n effort,\n };\n}\n\nexport const defaultMeasurer = new TreeMeasurer();\n\nexport function measureCode(code: string, options: MeasureOptions): CodeMetrics {\n return defaultMeasurer.measure(code, options);\n}\n\n/** Standalone helper mirroring TreeMeasurer.measureWithCrossFileDataAsync for the default measurer. */\nexport function measureCodeWithCrossFileDataAsync(\n code: string,\n options: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData: CrossFileDuplicationFileData }> {\n return defaultMeasurer.measureWithCrossFileDataAsync(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectDuplicationCandidates(code: string, options: MeasureOptions): CrossFileDuplicateCandidate[] {\n return defaultMeasurer.collectDuplicationCandidates(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectFunctionTokenSequences(code: string, options: MeasureOptions): Int32Array[] {\n return defaultMeasurer.collectFunctionTokenSequences(code, options);\n}\n\n/** Standalone helper mirroring measureCode for the default measurer. */\nexport function collectCrossFileDuplicationFileData(\n code: string,\n options: MeasureOptions\n): CrossFileDuplicationFileData {\n return defaultMeasurer.collectCrossFileDuplicationFileData(code, options);\n}\n"],"mappings":"oNAkBA,IAAa,EAAb,KAA0B,CACxB,SAA4B,EAAuB,EAEnD,uBAAwC,CACtC,MAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,IAAK,GAAa,EAAS,IAAI,CAAC,CAAC,CAClF,CAEA,QAAQ,EAAc,EAAsC,CAC1D,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADS,EAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,WAC/C,EAAG,CAAiB,CACzD,CAGA,yBACE,EACA,EACuE,CACvE,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EADS,EAAkB,EAAM,EAAS,KAAM,EAAmB,EAAQ,YAAa,EACjD,EAAG,CAAiB,CACpE,CAGA,MAAM,8BACJ,EACA,EACgF,CAChF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EAChD,EAAoB,EAAQ,mBAAqB,GAEvD,OAAO,EAAiC,MADlB,EAAuB,EAAM,EAAS,KAAM,EAAmB,EAAQ,YAAa,EAAI,EAC7D,CAAiB,CACpE,CAGA,6BAA6B,EAAc,EAAwD,CACjG,OAAO,KAAK,oCAAoC,EAAM,CAAO,CAAC,CAAC,UACjE,CAMA,oCAAoC,EAAc,EAAuD,CACvG,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAO,EACL,EAA2B,EAAM,EAAS,KAAM,EAAQ,aAAa,SAAS,CAChF,CACF,CAOA,8BAA8B,EAAc,EAAuC,CACjF,IAAM,EAAW,KAAK,gBAAgB,EAAQ,QAAQ,EACtD,OAAO,EAAoC,EAAM,EAAS,IAAI,CAChE,CAEA,gBAAwB,EAAwC,CAC9D,IAAM,EAAW,KAAK,SAAS,IAAI,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MAAM,yBAAyB,GAAM,EAEjD,OAAO,CACT,CACF,EAEA,SAAS,EACP,EACA,EACuE,CACvE,GAAI,CAAC,EAAQ,cACX,MAAU,MAAM,0DAA0D,EAE5E,MAAO,CACL,QAAS,EAAsB,EAAS,CAAiB,EACzD,cAAe,EAA+B,EAAQ,aAAa,CACrE,CACF,CAEA,SAAS,EAA+B,EAAmE,CACzG,MAAO,CACL,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,oBAAqB,EAAQ,oBAC7B,eAAgB,EAAQ,eAExB,gBAAiB,IAAI,IAAI,EAAQ,eAAe,CAClD,CACF,CAOA,SAAS,EAAsB,EAA+B,EAAyC,CACrG,MAAO,CACL,SAAU,EAAQ,SAClB,MAAO,EAAQ,MACf,MAAO,EAAQ,MACf,gBAAiB,EAAQ,gBACzB,UAAW,EAAQ,UAAU,IAAK,IAAQ,CACxC,KAAM,EAAG,KACT,cAAe,EAAG,cAClB,SAAU,EAAG,SACb,UAAW,EAAG,UACd,YAAa,EAAG,YAChB,QAAS,EAAG,QACZ,UAAW,EAAG,UACd,qBAAsB,EAAG,qBACzB,oBAAqB,EAAG,oBACxB,gBAAiB,EAAG,iBAAmB,CAAC,EACxC,aAAc,EAAG,aACjB,KAAM,EAAG,KACT,eAAgB,EAAG,eACnB,SAAU,EAAsB,EAAG,cAAc,EACjD,UAAW,EAAG,SAChB,EAAE,EACF,qBAAsB,EAAQ,qBAC9B,oBAAqB,EAAQ,oBAC7B,uBAAwB,EAAQ,uBAChC,aAAc,EAAQ,aACtB,UAAW,EAAQ,UACnB,YAAa,EAAQ,YACrB,SAAU,EAAsB,EAAQ,cAAc,EACtD,WAAY,EAAoB,EAAQ,WAAa,IAAA,EACvD,CACF,CAEA,SAAS,EAAsB,EAA+C,CAC5E,GAAM,CAAE,oBAAmB,mBAAkB,iBAAgB,iBAAkB,EACzE,EAAa,EAAoB,EACjC,EAAS,EAAiB,EAC1B,EAAS,IAAe,EAAI,EAAI,EAAS,KAAK,KAAK,CAAU,EAC7D,EAAa,IAAqB,EAAI,EAAK,EAAoB,GAAM,EAAgB,GAG3F,MAAO,CACL,oBACA,mBACA,iBACA,gBACA,aACA,SACA,SACA,aACA,OAXa,EAAa,CAY5B,CACF,CAEA,MAAa,EAAkB,IAAI,EAEnC,SAAgB,EAAY,EAAc,EAAsC,CAC9E,OAAO,EAAgB,QAAQ,EAAM,CAAO,CAC9C,CAGA,SAAgB,EACd,EACA,EACgF,CAChF,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EAA6B,EAAc,EAAwD,CACjH,OAAO,EAAgB,6BAA6B,EAAM,CAAO,CACnE,CAGA,SAAgB,EAA8B,EAAc,EAAuC,CACjG,OAAO,EAAgB,8BAA8B,EAAM,CAAO,CACpE,CAGA,SAAgB,EACd,EACA,EAC8B,CAC9B,OAAO,EAAgB,oCAAoC,EAAM,CAAO,CAC1E"}
@@ -1,4 +1,4 @@
1
1
  "use strict";let e=require("@willbooster/shared-lib");function t(e,t,n,i,a=!1){return JSON.parse(f().measureCodeNative(...r(e,t,n,i,a)))}function n(e,t,n,i,a=!1){return f().measureCodeNativeAsync(...r(e,t,n,i,a)).then(e=>JSON.parse(e))}function r(e,t,n,r,i){return[o(e),t,n,s(r?.minTokens),s(r?.maxGapTokens),s(r?.minSimilarityPercent),i]}function i(e,t,n){return JSON.parse(f().collectCrossFileDataNative(o(e),t,s(n)))}function a(e,t){return JSON.parse(f().collectFunctionTokenSequencesNative(o(e),t)).map(e=>Int32Array.from(e))}function o(e){return e.isWellFormed()?e:e.toWellFormed()}function s(t){return t===void 0||Number.isNaN(t)?void 0:(0,e.clamp)(Math.trunc(t),0,4294967295)}var c=class extends Error{};let l,u;function d(e){l=e}function f(){if(l)return l;if(u)throw u;let t=process.getBuiltinModule(`node:module`).createRequire(require("url").pathToFileURL(__filename).href),n=[`code-gauge-${p()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=t(i)}catch(t){r.push(` ${i}: ${(0,e.getErrorMessage)(t).split(`
2
- `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
2
+ `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==10){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 10; rebuild the addon with \`bun run build-native\``);continue}return l=n,n}throw u=new c(`The code-gauge native addon is not available for ${p()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
3
3
  `)}`),u}function p(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}exports.NativeAddonError=c,exports.collectCrossFileDataNative=i,exports.collectFunctionTokenSequencesNative=a,exports.measureCodeNative=t,exports.measureCodeNativeAsync=n,exports.setNativeBinding=d;
4
4
  //# sourceMappingURL=nativeMetrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 8;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAmEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,EAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,gFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
1
+ {"version":3,"file":"nativeMetrics.cjs","names":["clamp","getErrorMessage"],"sources":["../src/nativeMetrics.ts"],"sourcesContent":["import { clamp, getErrorMessage } from '@willbooster/shared-lib';\nimport type { CrossFileDuplicateCandidate, Token, TokenRange } from './duplication.js';\nimport type { CodeMetrics, DuplicationOptions, FunctionMetrics } from './types.js';\n\n/**\n * Halstead counts measured natively; the derived float metrics (volume, effort, ...) are\n * computed in TypeScript because they involve transcendental functions (log2) whose last-bit\n * results can differ between V8 and Rust's libm, and results must not depend on the Rust side's\n * libm build.\n */\nexport interface NativeHalsteadCounts {\n distinctOperators: number;\n distinctOperands: number;\n totalOperators: number;\n totalOperands: number;\n}\n\nexport interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead' | 'cognitiveBlocks'> {\n /** Absent when the function has none. */\n cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];\n halsteadCounts: NativeHalsteadCounts;\n}\n\nexport interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {\n functions: NativeFunctionMetricsPayload[];\n halsteadCounts: NativeHalsteadCounts;\n syntaxTree?: string;\n crossFileData?: NativeCrossFileDataPayload;\n}\n\n/** One file's cross-file clone-detection contribution as serialized by the native addon. */\nexport interface NativeCrossFileDataPayload {\n candidates: CrossFileDuplicateCandidate[];\n tokens: Token[];\n containerStatements: TokenRange[][];\n nearMissBlocks: TokenRange[];\n /** 1-based lines that are neither blank nor comment-only, sorted ascending. */\n codeLineNumbers: number[];\n}\n\ntype NativeMeasureArguments = [\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n minTokens: number | undefined,\n maxGapTokens: number | undefined,\n minSimilarityPercent: number | undefined,\n includeCrossFileData: boolean,\n];\n\nexport interface NativeBinding {\n measureCodeNative(...args: NativeMeasureArguments): string;\n measureCodeNativeAsync(...args: NativeMeasureArguments): Promise<string>;\n collectCrossFileDataNative(code: string, language: string, minTokens?: number): string;\n collectFunctionTokenSequencesNative(code: string, language: string): string;\n payloadVersion?(): number;\n}\n\n/**\n * Must equal `payload_version` in native/src/lib.rs. A previously built addon survives a\n * `git pull` untouched, so without this handshake it would silently return payloads missing\n * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.\n */\nexport const expectedPayloadVersion = 10;\n\n/**\n * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;\n * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.\n */\nexport function measureCodeNative(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): NativeMetricsPayload {\n return JSON.parse(\n loadBinding().measureCodeNative(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n ) as NativeMetricsPayload;\n}\n\n/**\n * measureCodeNative on the addon's worker threads, so several files can be measured in parallel.\n * A missing addon still throws synchronously, like measureCodeNative.\n */\nexport function measureCodeNativeAsync(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication?: DuplicationOptions,\n includeCrossFileData = false\n): Promise<NativeMetricsPayload> {\n return loadBinding()\n .measureCodeNativeAsync(\n ...toNativeMeasureArguments(code, language, includeSyntaxTree, duplication, includeCrossFileData)\n )\n .then((json) => JSON.parse(json) as NativeMetricsPayload);\n}\n\n/** The binding arguments shared by the sync and async measurements, so both measure alike. */\nfunction toNativeMeasureArguments(\n code: string,\n language: string,\n includeSyntaxTree: boolean,\n duplication: DuplicationOptions | undefined,\n includeCrossFileData: boolean\n): NativeMeasureArguments {\n return [\n toWellFormed(code),\n language,\n includeSyntaxTree,\n clampToU32(duplication?.minTokens),\n clampToU32(duplication?.maxGapTokens),\n clampToU32(duplication?.minSimilarityPercent),\n includeCrossFileData,\n ];\n}\n\n/** Collects one file's cross-file clone-detection contribution via the native addon. */\nexport function collectCrossFileDataNative(\n code: string,\n language: string,\n minTokens?: number\n): NativeCrossFileDataPayload {\n return JSON.parse(\n loadBinding().collectCrossFileDataNative(toWellFormed(code), language, clampToU32(minTokens))\n ) as NativeCrossFileDataPayload;\n}\n\n/** Collects normalized token hash sequences of every function via the native addon. */\nexport function collectFunctionTokenSequencesNative(code: string, language: string): Int32Array[] {\n const sequences = JSON.parse(\n loadBinding().collectFunctionTokenSequencesNative(toWellFormed(code), language)\n ) as number[][];\n return sequences.map((sequence) => Int32Array.from(sequence));\n}\n\n/**\n * Lone surrogates cannot cross the N-API boundary losslessly, so ill-formed strings (invalid\n * UTF-16 occasionally present in real-world files) are measured with U+FFFD replacements — the\n * same code units V8's own UTF-8 conversion would substitute.\n */\nfunction toWellFormed(code: string): string {\n return code.isWellFormed() ? code : code.toWellFormed();\n}\n\n/**\n * The duplication settings cross the boundary as u32, whose JavaScript conversion wraps modulo\n * 2^32 (2 ** 32 would become 0 and match everything). The public API accepts any safe integer, so\n * out-of-range values clamp to [0, u32::MAX] — no source can hold 2^32 tokens, so a clamped\n * threshold behaves identically to the requested one. NaN (e.g. `Number(unsetEnvVariable)`)\n * survives clamping arithmetic and would also convert to 0, so it is treated as an absent setting\n * instead.\n */\nfunction clampToU32(value: number | undefined): number | undefined {\n return value === undefined || Number.isNaN(value) ? undefined : clamp(Math.trunc(value), 0, 0xFF_FF_FF_FF);\n}\n\n/**\n * Raised when no usable native addon can be loaded. Every measurement fails identically until the\n * addon is built, so callers measuring many files (the CLI scan) treat it as fatal for the whole\n * run instead of recording one \"skipped\" entry per file.\n */\nexport class NativeAddonError extends Error {}\n\nlet cachedBinding: NativeBinding | undefined;\nlet cachedFailure: NativeAddonError | undefined;\n\n/** Replaces the N-API addon, e.g. with the WebAssembly build on runtimes without N-API. */\nexport function setNativeBinding(binding: NativeBinding): void {\n cachedBinding = binding;\n}\n\nfunction loadBinding(): NativeBinding {\n if (cachedBinding) {\n return cachedBinding;\n }\n // The failure is memoized too: resolution (including platformTriplet's diagnostic-report call\n // on Linux) would otherwise repeat for every measured file of an already-failing run.\n if (cachedFailure) {\n throw cachedFailure;\n }\n // Resolved relative to this file, so both src/ (tests) and dist/ (build) find the addon.\n // node:module is loaded lazily so that runtimes without it can bundle this module.\n const requireNative = process.getBuiltinModule('node:module').createRequire(import.meta.url);\n const specifiers = [\n // A prebuilt platform package, when published for this platform.\n `code-gauge-${platformTriplet()}`,\n // A locally built addon (`bun run build-native`).\n '../native/code-gauge.node',\n ];\n const failures: string[] = [];\n for (const specifier of specifiers) {\n let binding: NativeBinding;\n try {\n binding = requireNative(specifier) as NativeBinding;\n } catch (error) {\n failures.push(` ${specifier}: ${getErrorMessage(error).split('\\n')[0]}`);\n continue;\n }\n const version = binding.payloadVersion?.();\n if (version !== expectedPayloadVersion) {\n failures.push(\n ` ${specifier}: payload version ${version ?? 'unknown'} does not match the expected ` +\n `${expectedPayloadVersion}; rebuild the addon with \\`bun run build-native\\``\n );\n continue;\n }\n cachedBinding = binding;\n return binding;\n }\n cachedFailure = new NativeAddonError(\n `The code-gauge native addon is not available for ${platformTriplet()}. Build it with ` +\n '`node scripts/buildNative.mjs` in the code-gauge package directory (requires a Rust ' +\n `toolchain).\\n${failures.join('\\n')}`\n );\n throw cachedFailure;\n}\n\n/**\n * The platform-package suffix in the napi-rs naming convention: Linux targets are qualified by\n * libc ABI (`linux-x64-gnu` / `linux-x64-musl`) because a glibc-linked addon cannot load on\n * Alpine/musl, and Windows by toolchain ABI (`win32-x64-msvc`), matching what napi-rs tooling\n * generates. Must match scripts/installNative.mjs and the build-native workflow's target list.\n */\nfunction platformTriplet(): string {\n const base = `${process.platform}-${process.arch}`;\n if (process.platform === 'win32') {\n return `${base}-msvc`;\n }\n if (process.platform !== 'linux') {\n return base;\n }\n // Musl builds of Node report no glibc runtime version; see napi-rs's isMusl detection.\n const report = process.report?.getReport() as { header?: { glibcVersionRuntime?: string } } | undefined;\n return report?.header?.glibcVersionRuntime ? `${base}-gnu` : `${base}-musl`;\n}\n"],"mappings":"sDAqEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACD,CACtB,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,kBACZ,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CACF,CACF,CAMA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAAuB,GACQ,CAC/B,OAAO,EAAY,CAAC,CACjB,uBACC,GAAG,EAAyB,EAAM,EAAU,EAAmB,EAAa,CAAoB,CAClG,CAAC,CACA,KAAM,GAAS,KAAK,MAAM,CAAI,CAAyB,CAC5D,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACwB,CACxB,MAAO,CACL,EAAa,CAAI,EACjB,EACA,EACA,EAAW,GAAa,SAAS,EACjC,EAAW,GAAa,YAAY,EACpC,EAAW,GAAa,oBAAoB,EAC5C,CACF,CACF,CAGA,SAAgB,EACd,EACA,EACA,EAC4B,CAC5B,OAAO,KAAK,MACV,EAAY,CAAC,CAAC,2BAA2B,EAAa,CAAI,EAAG,EAAU,EAAW,CAAS,CAAC,CAC9F,CACF,CAGA,SAAgB,EAAoC,EAAc,EAAgC,CAIhG,OAHkB,KAAK,MACrB,EAAY,CAAC,CAAC,oCAAoC,EAAa,CAAI,EAAG,CAAQ,CAEjE,CAAC,CAAC,IAAK,GAAa,WAAW,KAAK,CAAQ,CAAC,CAC9D,CAOA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,aAAa,EAAI,EAAO,EAAK,aAAa,CACxD,CAUA,SAAS,EAAW,EAA+C,CACjE,OAAO,IAAU,IAAA,IAAa,OAAO,MAAM,CAAK,EAAI,IAAA,IAAA,EAAYA,EAAAA,MAAAA,CAAM,KAAK,MAAM,CAAK,EAAG,EAAG,UAAa,CAC3G,CAOA,IAAa,EAAb,cAAsC,KAAM,CAAC,EAE7C,IAAI,EACA,EAGJ,SAAgB,EAAiB,EAA8B,CAC7D,EAAgB,CAClB,CAEA,SAAS,GAA6B,CACpC,GAAI,EACF,OAAO,EAIT,GAAI,EACF,MAAM,EAIR,IAAM,EAAgB,QAAQ,iBAAiB,aAAa,CAAC,CAAC,cAAA,QAAA,KAAA,CAAA,CAAA,cAAA,UAAA,CAAA,CAAA,IAA6B,EACrF,EAAa,CAEjB,cAAc,EAAgB,IAE9B,2BACF,EACM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAa,EAAY,CAClC,IAAI,EACJ,GAAI,CACF,EAAU,EAAc,CAAS,CACnC,OAAS,EAAO,CACd,EAAS,KAAK,KAAK,EAAU,KAAA,EAAIC,EAAAA,gBAAAA,CAAgB,CAAK,CAAC,CAAC,MAAM;CAAI,CAAC,CAAC,IAAI,EACxE,QACF,CACA,IAAM,EAAU,EAAQ,iBAAiB,EACzC,GAAI,IAAA,GAAoC,CACtC,EAAS,KACP,KAAK,EAAU,oBAAoB,GAAW,UAAU,iFAE1D,EACA,QACF,CAEA,MADA,GAAgB,EACT,CACT,CAMA,KALA,GAAgB,IAAI,EAClB,oDAAoD,EAAgB,EAAE,qHAEpD,EAAS,KAAK;CAAI,GACtC,EACM,CACR,CAQA,SAAS,GAA0B,CACjC,IAAM,EAAO,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAS5C,OARI,QAAQ,WAAa,QAChB,GAAG,EAAK,OAEb,QAAQ,WAAa,SAIV,QAAQ,QAAQ,UAAU,EAAA,EAC1B,QAAQ,oBAAsB,GAAG,EAAK,MAAQ,GAAG,EAAK,OAJ5D,CAKX"}
@@ -12,7 +12,9 @@ export interface NativeHalsteadCounts {
12
12
  totalOperators: number;
13
13
  totalOperands: number;
14
14
  }
15
- export interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead'> {
15
+ export interface NativeFunctionMetricsPayload extends Omit<FunctionMetrics, 'halstead' | 'cognitiveBlocks'> {
16
+ /** Absent when the function has none. */
17
+ cognitiveBlocks?: FunctionMetrics['cognitiveBlocks'];
16
18
  halsteadCounts: NativeHalsteadCounts;
17
19
  }
18
20
  export interface NativeMetricsPayload extends Omit<CodeMetrics, 'halstead' | 'functions' | 'syntaxTree'> {
@@ -51,7 +53,7 @@ export interface NativeBinding {
51
53
  * `git pull` untouched, so without this handshake it would silently return payloads missing
52
54
  * newer fields, or lack newer binding functions, instead of failing with a clear rebuild message.
53
55
  */
54
- export declare const expectedPayloadVersion = 8;
56
+ export declare const expectedPayloadVersion = 10;
55
57
  /**
56
58
  * Measures one file via the native addon, returning the raw payload for assembly in metrics.ts;
57
59
  * with `includeCrossFileData`, the payload also carries the file's cross-file contribution.
@@ -1,4 +1,4 @@
1
1
  import{clamp as e,getErrorMessage as t}from"@willbooster/shared-lib";function n(e,t,n,r,a=!1){return JSON.parse(p().measureCodeNative(...i(e,t,n,r,a)))}function r(e,t,n,r,a=!1){return p().measureCodeNativeAsync(...i(e,t,n,r,a)).then(e=>JSON.parse(e))}function i(e,t,n,r,i){return[s(e),t,n,c(r?.minTokens),c(r?.maxGapTokens),c(r?.minSimilarityPercent),i]}function a(e,t,n){return JSON.parse(p().collectCrossFileDataNative(s(e),t,c(n)))}function o(e,t){return JSON.parse(p().collectFunctionTokenSequencesNative(s(e),t)).map(e=>Int32Array.from(e))}function s(e){return e.isWellFormed()?e:e.toWellFormed()}function c(t){return t===void 0||Number.isNaN(t)?void 0:e(Math.trunc(t),0,4294967295)}var l=class extends Error{};let u,d;function f(e){u=e}function p(){if(u)return u;if(d)throw d;let e=process.getBuiltinModule(`node:module`).createRequire(import.meta.url),n=[`code-gauge-${m()}`,`../native/code-gauge.node`],r=[];for(let i of n){let n;try{n=e(i)}catch(e){r.push(` ${i}: ${t(e).split(`
2
- `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==8){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 8; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
2
+ `)[0]}`);continue}let a=n.payloadVersion?.();if(a!==10){r.push(` ${i}: payload version ${a??`unknown`} does not match the expected 10; rebuild the addon with \`bun run build-native\``);continue}return u=n,n}throw d=new l(`The code-gauge native addon is not available for ${m()}. Build it with \`node scripts/buildNative.mjs\` in the code-gauge package directory (requires a Rust toolchain).\n${r.join(`
3
3
  `)}`),d}function m(){let e=`${process.platform}-${process.arch}`;return process.platform===`win32`?`${e}-msvc`:process.platform===`linux`?(process.report?.getReport())?.header?.glibcVersionRuntime?`${e}-gnu`:`${e}-musl`:e}export{l as NativeAddonError,a as collectCrossFileDataNative,o as collectFunctionTokenSequencesNative,n as measureCodeNative,r as measureCodeNativeAsync,f as setNativeBinding};
4
4
  //# sourceMappingURL=nativeMetrics.js.map