code-gauge 4.7.1 → 5.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 (80) hide show
  1. package/README.md +35 -10
  2. package/THIRD-PARTY-NOTICES.txt +595 -29
  3. package/dist/cli.cjs +3 -3
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +3 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cliConfig.cjs +1 -1
  8. package/dist/cliConfig.cjs.map +1 -1
  9. package/dist/cliConfig.d.ts +13 -3
  10. package/dist/cliConfig.js +1 -1
  11. package/dist/cliConfig.js.map +1 -1
  12. package/dist/crossFileDuplication.cjs +1 -1
  13. package/dist/crossFileDuplication.cjs.map +1 -1
  14. package/dist/crossFileDuplication.js +1 -1
  15. package/dist/crossFileDuplication.js.map +1 -1
  16. package/dist/crossFileNearMiss.cjs +1 -1
  17. package/dist/crossFileNearMiss.cjs.map +1 -1
  18. package/dist/crossFileNearMiss.js +1 -1
  19. package/dist/crossFileNearMiss.js.map +1 -1
  20. package/dist/diffCommand.cjs +3 -3
  21. package/dist/diffCommand.cjs.map +1 -1
  22. package/dist/diffCommand.js +3 -3
  23. package/dist/diffCommand.js.map +1 -1
  24. package/dist/duplicateSelection.cjs +1 -1
  25. package/dist/duplicateSelection.cjs.map +1 -1
  26. package/dist/duplicateSelection.js +1 -1
  27. package/dist/duplicateSelection.js.map +1 -1
  28. package/dist/duplication.cjs +1 -1
  29. package/dist/duplication.cjs.map +1 -1
  30. package/dist/duplication.js +1 -1
  31. package/dist/duplication.js.map +1 -1
  32. package/dist/exclusion.cjs +2 -0
  33. package/dist/exclusion.cjs.map +1 -0
  34. package/dist/exclusion.d.ts +30 -0
  35. package/dist/exclusion.js +2 -0
  36. package/dist/exclusion.js.map +1 -0
  37. package/dist/git.cjs +1 -1
  38. package/dist/git.cjs.map +1 -1
  39. package/dist/git.d.ts +16 -0
  40. package/dist/git.js +1 -1
  41. package/dist/git.js.map +1 -1
  42. package/dist/index.cjs +1 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +1 -1
  45. package/dist/languages.cjs +1 -1
  46. package/dist/languages.cjs.map +1 -1
  47. package/dist/languages.js +1 -1
  48. package/dist/languages.js.map +1 -1
  49. package/dist/newDuplication.cjs +2 -0
  50. package/dist/newDuplication.cjs.map +1 -0
  51. package/dist/newDuplication.d.ts +40 -0
  52. package/dist/newDuplication.js +2 -0
  53. package/dist/newDuplication.js.map +1 -0
  54. package/dist/regressionGate.cjs +1 -1
  55. package/dist/regressionGate.cjs.map +1 -1
  56. package/dist/regressionGate.d.ts +8 -6
  57. package/dist/regressionGate.js +1 -1
  58. package/dist/regressionGate.js.map +1 -1
  59. package/dist/scan.cjs +1 -1
  60. package/dist/scan.cjs.map +1 -1
  61. package/dist/scan.d.ts +12 -3
  62. package/dist/scan.js +1 -1
  63. package/dist/scan.js.map +1 -1
  64. package/dist/worker.cjs +1 -1
  65. package/dist/worker.cjs.map +1 -1
  66. package/dist/worker.js +1 -1
  67. package/dist/worker.js.map +1 -1
  68. package/native/Cargo.lock +47 -15
  69. package/native/Cargo.toml +9 -9
  70. package/native/code-gauge.wasm +0 -0
  71. package/native/src/complexity.rs +26 -33
  72. package/native/src/dep_degree.rs +43 -32
  73. package/native/src/duplication.rs +158 -40
  74. package/native/src/functions.rs +20 -62
  75. package/native/src/languages.rs +28 -7
  76. package/native/src/measure.rs +16 -27
  77. package/native/src/ncss.rs +32 -23
  78. package/native/src/tree_index.rs +9 -0
  79. package/native/src/util.rs +12 -57
  80. package/package.json +10 -10
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- "use strict";const e=require("./cliConfig.cjs"),t=require("./scan.cjs"),n=require("./diffCommand.cjs");let r=require("commander");a().catch(e=>{t.writeStderr(`Error: ${t.formatError(e)}\n`),process.exitCode=1});function i(t){return t.option(`--config <path>`,`config file to use instead of the auto-detected ${e.configFileName}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,g).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,h).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)`,m).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}async function a(){let a=i(new r.Command().name(`code-gauge`).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)`,g).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`);a.action(async(n,r)=>{let i=t.resolveTarget(n),a=await e.loadConfig(r.config,await t.configSearchDirectory(i)),s=e.resolveOptions(r,a),c=await t.scanTarget(i,s);t.addCrossFileDuplication(c,s);let l=o(c,s.top);s.json?u(c,l,s):d(i,c,l,s),(c.fatalError||s.failOnError&&c.errors.length>0)&&(process.exitCode=1)}),i(a.command(`diff`).description(`Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)`).argument(`[target]`,`directory whose changed files are gated`,`.`).requiredOption(`--base <ref>`,`base git ref; changes are measured against its merge-base with HEAD`)).option(`--full`,`also print the passing gate values of every checked function and file`).action(async(e,t,r)=>{await n.runDiffCommand(e,r.optsWithGlobals())}),await a.parseAsync()}function o(e,n){let r=e.files.map(({file:n,metrics:r})=>{let i=t.formatPath(n,e.displayRoot),a=t.collectDuplicatedLineNumbers(r,e.crossFileDuplication,i);return{file:i,worstFunction:l(r.functions),duplicatedLineCount:a.size,duplicatedLineRatio:r.lines.code===0?0:a.size/r.lines.code,crossFilePartners:[],ncss:r.ncssCount,codeLines:r.lines.code}}),i=c(r.map(e=>e.worstFunction?.cognitiveComplexity??0)),a=c(r.map(e=>e.duplicatedLineCount)),o=c(r.map(e=>e.ncss)),u=r.map(e=>({...e,score:i(e.worstFunction?.cognitiveComplexity??0)+a(e.duplicatedLineCount)+o(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return s(u.slice(0,n),e.crossFileDuplication),u}function s(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,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 c(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 l(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 u(e,n,r){let i=n.slice(0,r.top);t.writeStdout(JSON.stringify({summary:p(e.files),totalRankedFiles:n.length,truncated:i.length<n.length,files:i,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
- `)}function d(e,n,r,i){if(n.fatalError){t.writeStderr(`Error: ${n.fatalError}\n`);return}let a=p(n.files);if(t.writeStdout(`Measured ${a.fileCount} files under ${e} (code LOC ${a.linesOfCode}, NCSS ${a.ncssCount}, functions ${a.functionCount})\n`),r.length===0)t.writeStdout(`No measurable files found.
4
- `);else{let e=r.slice(0,i.top),n=r.length>e.length?` of ${r.length}`:``;t.writeStdout(`\nRefactoring candidates (top ${e.length}${n}):\n`);for(let[n,r]of e.entries())t.writeStdout(`${n+1}. ${f(r)}\n`)}if(n.warnings.length>0){t.writeStderr(`\nDegraded ${n.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of n.warnings.slice(0,10))t.writeStderr(`- ${e}\n`);n.warnings.length>10&&t.writeStderr(`- ... ${n.warnings.length-10} more\n`)}if(n.errors.length>0){t.writeStderr(`\nSkipped ${n.errors.length} files or directories:\n`);for(let e of n.errors.slice(0,10))t.writeStderr(`- ${e}\n`);n.errors.length>10&&t.writeStderr(`- ... ${n.errors.length-10} more\n`)}}function f(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 m(e){let t=g(e);if(t>100)throw new r.InvalidArgumentError(`Expected an integer between 1 and 100.`);return t}function h(e){if(!/^\d+$/u.test(e))throw new r.InvalidArgumentError(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new r.InvalidArgumentError(`Expected a non-negative integer.`);return t}function g(e){if(!/^[1-9]\d*$/u.test(e))throw new r.InvalidArgumentError(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new r.InvalidArgumentError(`Expected a positive integer.`);return t}
2
+ "use strict";const e=require("./cliConfig.cjs"),t=require("./exclusion.cjs"),n=require("./scan.cjs"),r=require("./diffCommand.cjs");let i=require("commander");o().catch(e=>{n.writeStderr(`Error: ${n.formatError(e)}\n`),process.exitCode=1});function a(t){return t.option(`--config <path>`,`config file to use instead of the auto-detected ${e.configFileName}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,_).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,g).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)`,h).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}async function o(){let o=a(new i.Command().name(`code-gauge`).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)`,_).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`);o.action(async(r,i)=>{let a=n.resolveTarget(r),o=await e.loadConfig(i.config,await n.configSearchDirectory(a)),c=e.resolveOptions(i,o),l=await n.configSearchDirectory(a),u=await n.scanTarget(a,{...c,loadExclusion:e=>t.loadExclusion(l,c.exclude,e)});n.addCrossFileDuplication(u,c);let p=s(u,c.top);c.json?d(u,p,c):f(a,u,p,c),(u.fatalError||c.failOnError&&u.errors.length>0)&&(process.exitCode=1)}),a(o.command(`diff`).description(`Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)`).argument(`[target]`,`directory whose changed files are gated`,`.`).requiredOption(`--base <ref>`,`base git ref; changes are measured against its merge-base with HEAD`)).option(`--full`,`also print the passing gate values of every checked function and file`).action(async(e,t,n)=>{await r.runDiffCommand(e,n.optsWithGlobals())}),await o.parseAsync()}function s(e,t){let r=e.files.map(({file:t,metrics:r})=>{let i=n.formatPath(t,e.displayRoot),a=n.collectDuplicatedLineNumbers(r,e.crossFileDuplication,i);return{file:i,worstFunction:u(r.functions),duplicatedLineCount:a.size,duplicatedLineRatio:r.lines.code===0?0:a.size/r.lines.code,crossFilePartners:[],ncss:r.ncssCount,codeLines:r.lines.code}}),i=l(r.map(e=>e.worstFunction?.cognitiveComplexity??0)),a=l(r.map(e=>e.duplicatedLineCount)),o=l(r.map(e=>e.ncss)),s=r.map(e=>({...e,score:i(e.worstFunction?.cognitiveComplexity??0)+a(e.duplicatedLineCount)+o(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return c(s.slice(0,t),e.crossFileDuplication),s}function c(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 l(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 u(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 d(e,t,r){let i=t.slice(0,r.top);n.writeStdout(JSON.stringify({summary:m(e.files),totalRankedFiles:t.length,truncated:i.length<t.length,files:i,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
+ `)}function f(e,t,r,i){if(t.fatalError){n.writeStderr(`Error: ${t.fatalError}\n`);return}let a=m(t.files);if(n.writeStdout(`Measured ${a.fileCount} files under ${e} (code LOC ${a.linesOfCode}, NCSS ${a.ncssCount}, functions ${a.functionCount})\n`),r.length===0)n.writeStdout(`No measurable files found.
4
+ `);else{let e=r.slice(0,i.top),t=r.length>e.length?` of ${r.length}`:``;n.writeStdout(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,r]of e.entries())n.writeStdout(`${t+1}. ${p(r)}\n`)}if(t.warnings.length>0){n.writeStderr(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))n.writeStderr(`- ${e}\n`);t.warnings.length>10&&n.writeStderr(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){n.writeStderr(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))n.writeStderr(`- ${e}\n`);t.errors.length>10&&n.writeStderr(`- ... ${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 m(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 h(e){let t=_(e);if(t>100)throw new i.InvalidArgumentError(`Expected an integer between 1 and 100.`);return t}function g(e){if(!/^\d+$/u.test(e))throw new i.InvalidArgumentError(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new i.InvalidArgumentError(`Expected a non-negative integer.`);return t}function _(e){if(!/^[1-9]\d*$/u.test(e))throw new i.InvalidArgumentError(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new i.InvalidArgumentError(`Expected a positive integer.`);return t}
5
5
  //# sourceMappingURL=cli.cjs.map
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["formatError","configFileName","Command","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","scanTarget","runDiffCommand","formatPath","collectDuplicatedLineNumbers","InvalidArgumentError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command, InvalidArgumentError } from 'commander';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runDiffCommand, type DiffCliOptions } from './diffCommand.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.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: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\n/** Registers the options shared by the ranking command and the diff gate. */\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 parsePositiveInteger\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\nasync function main(): Promise<void> {\n const program = addSharedOptions(\n new Command()\n .name('code-gauge')\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)', parsePositiveInteger)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, options);\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\n addSharedOptions(\n program\n .command('diff')\n .description(\n 'Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)'\n )\n .argument('[target]', 'directory whose changed files are gated', '.')\n .requiredOption('--base <ref>', 'base git ref; changes are measured against its merge-base with HEAD')\n )\n .option('--full', 'also print the passing gate values of every checked function and file')\n .action(async (target: string, _cliOptions: DiffCliOptions, command: Command) => {\n // Options sharing a name with a root option (--json, --config, ...) land in the root's\n // option store, so the merged view is required to see them.\n await runDiffCommand(target, command.optsWithGlobals() as DiffCliOptions);\n });\n\n await program.parseAsync();\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 parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(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 parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\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":["formatError","configFileName","Command","resolveTarget","loadConfig","configSearchDirectory","resolveOptions","scanTarget","loadExclusion","runDiffCommand","formatPath","collectDuplicatedLineNumbers","InvalidArgumentError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command, InvalidArgumentError } from 'commander';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runDiffCommand, type DiffCliOptions } from './diffCommand.js';\nimport { loadExclusion } from './exclusion.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.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: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\n/** Registers the options shared by the ranking command and the diff gate. */\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 parsePositiveInteger\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\nasync function main(): Promise<void> {\n const program = addSharedOptions(\n new Command()\n .name('code-gauge')\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)', parsePositiveInteger)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const searchDirectory = await configSearchDirectory(resolvedTarget);\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\n addSharedOptions(\n program\n .command('diff')\n .description(\n 'Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)'\n )\n .argument('[target]', 'directory whose changed files are gated', '.')\n .requiredOption('--base <ref>', 'base git ref; changes are measured against its merge-base with HEAD')\n )\n .option('--full', 'also print the passing gate values of every checked function and file')\n .action(async (target: string, _cliOptions: DiffCliOptions, command: Command) => {\n // Options sharing a name with a root option (--json, --config, ...) land in the root's\n // option store, so the merged view is required to see them.\n await runDiffCommand(target, command.optsWithGlobals() as DiffCliOptions);\n });\n\n await program.parseAsync();\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 parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(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 parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\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{configFileName as e,loadConfig as t,resolveOptions as n}from"./cliConfig.js";import{addCrossFileDuplication as r,collectDuplicatedLineNumbers as i,configSearchDirectory as a,formatError as o,formatPath as s,resolveTarget as c,scanTarget as l,writeStderr as u,writeStdout as d}from"./scan.js";import{runDiffCommand as f}from"./diffCommand.js";import{Command as p,InvalidArgumentError as m}from"commander";g().catch(e=>{u(`Error: ${o(e)}\n`),process.exitCode=1});function h(t){return t.option(`--config <path>`,`config file to use instead of the auto-detected ${e}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,D).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,E).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)`,T).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}async function g(){let e=h(new p().name(`code-gauge`).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)`,D).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`);e.action(async(e,i)=>{let o=c(e),s=await t(i.config,await a(o)),u=n(i,s),d=await l(o,u);r(d,u);let f=_(d,u.top);u.json?x(d,f,u):S(o,d,f,u),(d.fatalError||u.failOnError&&d.errors.length>0)&&(process.exitCode=1)}),h(e.command(`diff`).description(`Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)`).argument(`[target]`,`directory whose changed files are gated`,`.`).requiredOption(`--base <ref>`,`base git ref; changes are measured against its merge-base with HEAD`)).option(`--full`,`also print the passing gate values of every checked function and file`).action(async(e,t,n)=>{await f(e,n.optsWithGlobals())}),await e.parseAsync()}function _(e,t){let n=e.files.map(({file:t,metrics:n})=>{let r=s(t,e.displayRoot),a=i(n,e.crossFileDuplication,r);return{file:r,worstFunction:b(n.functions),duplicatedLineCount:a.size,duplicatedLineRatio:n.lines.code===0?0:a.size/n.lines.code,crossFilePartners:[],ncss:n.ncssCount,codeLines:n.lines.code}}),r=y(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),a=y(n.map(e=>e.duplicatedLineCount)),o=y(n.map(e=>e.ncss)),c=n.map(e=>({...e,score:r(e.worstFunction?.cognitiveComplexity??0)+a(e.duplicatedLineCount)+o(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return v(c.slice(0,t),e.crossFileDuplication),c}function v(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,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 y(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 b(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 x(e,t,n){let r=t.slice(0,n.top);d(JSON.stringify({summary:w(e.files),totalRankedFiles:t.length,truncated:r.length<t.length,files:r,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
- `)}function S(e,t,n,r){if(t.fatalError){u(`Error: ${t.fatalError}\n`);return}let i=w(t.files);if(d(`Measured ${i.fileCount} files under ${e} (code LOC ${i.linesOfCode}, NCSS ${i.ncssCount}, functions ${i.functionCount})\n`),n.length===0)d(`No measurable files found.
4
- `);else{let e=n.slice(0,r.top),t=n.length>e.length?` of ${n.length}`:``;d(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())d(`${t+1}. ${C(n)}\n`)}if(t.warnings.length>0){u(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))u(`- ${e}\n`);t.warnings.length>10&&u(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){u(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))u(`- ${e}\n`);t.errors.length>10&&u(`- ... ${t.errors.length-10} more\n`)}}function C(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 w(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 T(e){let t=D(e);if(t>100)throw new m(`Expected an integer between 1 and 100.`);return t}function E(e){if(!/^\d+$/u.test(e))throw new m(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new m(`Expected a non-negative integer.`);return t}function D(e){if(!/^[1-9]\d*$/u.test(e))throw new m(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new m(`Expected a positive integer.`);return t}
2
+ import{configFileName as e,loadConfig as t,resolveOptions as n}from"./cliConfig.js";import{loadExclusion as r}from"./exclusion.js";import{addCrossFileDuplication as i,collectDuplicatedLineNumbers as a,configSearchDirectory as o,formatError as s,formatPath as c,resolveTarget as l,scanTarget as u,writeStderr as d,writeStdout as f}from"./scan.js";import{runDiffCommand as p}from"./diffCommand.js";import{Command as m,InvalidArgumentError as h}from"commander";_().catch(e=>{d(`Error: ${s(e)}\n`),process.exitCode=1});function g(t){return t.option(`--config <path>`,`config file to use instead of the auto-detected ${e}`).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,O).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,D).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)`,E).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`)}async function _(){let e=g(new m().name(`code-gauge`).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)`,O).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`);e.action(async(e,a)=>{let s=l(e),c=await t(a.config,await o(s)),d=n(a,c),f=await o(s),p=await u(s,{...d,loadExclusion:e=>r(f,d.exclude,e)});i(p,d);let m=v(p,d.top);d.json?S(p,m,d):C(s,p,m,d),(p.fatalError||d.failOnError&&p.errors.length>0)&&(process.exitCode=1)}),g(e.command(`diff`).description(`Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)`).argument(`[target]`,`directory whose changed files are gated`,`.`).requiredOption(`--base <ref>`,`base git ref; changes are measured against its merge-base with HEAD`)).option(`--full`,`also print the passing gate values of every checked function and file`).action(async(e,t,n)=>{await p(e,n.optsWithGlobals())}),await e.parseAsync()}function v(e,t){let n=e.files.map(({file:t,metrics:n})=>{let r=c(t,e.displayRoot),i=a(n,e.crossFileDuplication,r);return{file:r,worstFunction:x(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=b(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),i=b(n.map(e=>e.duplicatedLineCount)),o=b(n.map(e=>e.ncss)),s=n.map(e=>({...e,score:r(e.worstFunction?.cognitiveComplexity??0)+i(e.duplicatedLineCount)+o(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return y(s.slice(0,t),e.crossFileDuplication),s}function y(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 b(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 x(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 S(e,t,n){let r=t.slice(0,n.top);f(JSON.stringify({summary:T(e.files),totalRankedFiles:t.length,truncated:r.length<t.length,files:r,errors:e.errors,warnings:e.warnings},void 0,2)+`
3
+ `)}function C(e,t,n,r){if(t.fatalError){d(`Error: ${t.fatalError}\n`);return}let i=T(t.files);if(f(`Measured ${i.fileCount} files under ${e} (code LOC ${i.linesOfCode}, NCSS ${i.ncssCount}, functions ${i.functionCount})\n`),n.length===0)f(`No measurable files found.
4
+ `);else{let e=n.slice(0,r.top),t=n.length>e.length?` of ${n.length}`:``;f(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())f(`${t+1}. ${w(n)}\n`)}if(t.warnings.length>0){d(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))d(`- ${e}\n`);t.warnings.length>10&&d(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){d(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))d(`- ${e}\n`);t.errors.length>10&&d(`- ... ${t.errors.length-10} more\n`)}}function w(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 T(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 E(e){let t=O(e);if(t>100)throw new h(`Expected an integer between 1 and 100.`);return t}function D(e){if(!/^\d+$/u.test(e))throw new h(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new h(`Expected a non-negative integer.`);return t}function O(e){if(!/^[1-9]\d*$/u.test(e))throw new h(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new h(`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 { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runDiffCommand, type DiffCliOptions } from './diffCommand.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.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: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\n/** Registers the options shared by the ranking command and the diff gate. */\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 parsePositiveInteger\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\nasync function main(): Promise<void> {\n const program = addSharedOptions(\n new Command()\n .name('code-gauge')\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)', parsePositiveInteger)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, options);\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\n addSharedOptions(\n program\n .command('diff')\n .description(\n 'Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)'\n )\n .argument('[target]', 'directory whose changed files are gated', '.')\n .requiredOption('--base <ref>', 'base git ref; changes are measured against its merge-base with HEAD')\n )\n .option('--full', 'also print the passing gate values of every checked function and file')\n .action(async (target: string, _cliOptions: DiffCliOptions, command: Command) => {\n // Options sharing a name with a root option (--json, --config, ...) land in the root's\n // option store, so the merged view is required to see them.\n await runDiffCommand(target, command.optsWithGlobals() as DiffCliOptions);\n });\n\n await program.parseAsync();\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 parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(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 parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\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 { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport type { CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport { runDiffCommand, type DiffCliOptions } from './diffCommand.js';\nimport { loadExclusion } from './exclusion.js';\nimport {\n addCrossFileDuplication,\n collectDuplicatedLineNumbers,\n configSearchDirectory,\n formatError,\n formatPath,\n resolveTarget,\n scanTarget,\n writeStderr,\n writeStdout,\n type FileMetrics,\n type ScanResult,\n} from './scan.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: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\n/** Registers the options shared by the ranking command and the diff gate. */\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 parsePositiveInteger\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\nasync function main(): Promise<void> {\n const program = addSharedOptions(\n new Command()\n .name('code-gauge')\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)', parsePositiveInteger)\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const searchDirectory = await configSearchDirectory(resolvedTarget);\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\n addSharedOptions(\n program\n .command('diff')\n .description(\n 'Gate the working tree against a base ref: report only metric regressions in changed files (exit 1 on violations)'\n )\n .argument('[target]', 'directory whose changed files are gated', '.')\n .requiredOption('--base <ref>', 'base git ref; changes are measured against its merge-base with HEAD')\n )\n .option('--full', 'also print the passing gate values of every checked function and file')\n .action(async (target: string, _cliOptions: DiffCliOptions, command: Command) => {\n // Options sharing a name with a root option (--json, --config, ...) land in the root's\n // option store, so the merged view is required to see them.\n await runDiffCommand(target, command.optsWithGlobals() as DiffCliOptions);\n });\n\n await program.parseAsync();\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 parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(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 parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./duplication.cjs"),n=require("./regressionGate.cjs");let r=require("node:fs/promises"),i=require("node:path");i=e.__toESM(i,1);const a=`code-gauge.config.json`;function o(e,n){return{duplication:{minTokens:e.duplicationMinTokens??n.duplication?.minTokens??t.defaultDuplicationOptions.minTokens,maxGapTokens:e.duplicationMaxGapTokens??n.duplication?.maxGapTokens??t.defaultDuplicationOptions.maxGapTokens,minSimilarityPercent:e.duplicationMinSimilarityPercent??n.duplication?.minSimilarityPercent??t.defaultDuplicationOptions.minSimilarityPercent},top:e.top??n.rank?.top??10,includeTests:e.includeTests??n.includeTests??!1,failOnError:e.failOnError??n.failOnError??!1,json:e.json??!1}}function s(e){return{newFunction:{...n.defaultGateOptions.newFunction,...e.gate?.newFunction},tolerance:{...n.defaultGateOptions.tolerance,...e.gate?.tolerance},matchSimilarityPercent:e.gate?.matchSimilarityPercent??n.defaultGateOptions.matchSimilarityPercent}}async function c(e,t){let n=e??await l(t);if(!n)return{};let i;try{i=await(0,r.readFile)(n,`utf8`)}catch(t){if(e)throw Error(`Cannot read config file "${n}": ${x(t)}`);return{}}let a;try{a=JSON.parse(i)}catch(e){throw Error(`Invalid JSON in config file "${n}": ${x(e)}`)}return d(a,n)}async function l(e){let t=e;for(;;){let e=i.default.join(t,a);if(await u(e))return e;let n=i.default.dirname(t);if(n===t)return;t=n}}async function u(e){try{return(await(0,r.stat)(e)).isFile()}catch{return!1}}function d(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r=new Set([`duplication`,`rank`,`gate`,`includeTests`,`failOnError`]);for(let e of Object.keys(n))if(!r.has(e))throw Error(`Config file "${t}": unknown setting "${e}" (expected ${[...r].join(`, `)}).`);let i={};n.duplication!==void 0&&(i.duplication=h(n.duplication,t)),n.rank!==void 0&&(i.rank=f(n.rank,t)),n.gate!==void 0&&(i.gate=p(n.gate,t));for(let e of[`includeTests`,`failOnError`])n[e]!==void 0&&(i[e]=b(n[e],e,t));return i}function f(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "rank" must be an object.`);let n={};for(let[r,i]of Object.entries(e)){if(r!==`top`)throw Error(`Config file "${t}": unknown setting "${r}" in "rank" (expected top).`);n.top=v(i,`rank.top`,t)}return n}function p(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "gate" must be an object.`);let r={};for(let[i,a]of Object.entries(e))if(i===`newFunction`)r.newFunction=m(a,`gate.newFunction`,Object.keys(n.defaultGateOptions.newFunction),t,_);else if(i===`tolerance`)r.tolerance=m(a,`gate.tolerance`,Object.keys(n.defaultGateOptions.tolerance),t,g);else if(i===`matchSimilarityPercent`){let e=v(a,`gate.matchSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "gate.matchSimilarityPercent" must be between 1 and 100.`);r.matchSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${i}" in "gate" (expected newFunction, tolerance, or matchSimilarityPercent).`);return r}function m(e,t,n,r,i){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${r}": "${t}" must be an object.`);let a={};for(let[o,s]of Object.entries(e)){if(!n.includes(o))throw Error(`Config file "${r}": unknown setting "${o}" in "${t}" (expected ${n.join(`, `)}).`);a[o]=i(s,`${t}.${o}`,r)}return a}function h(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "duplication" must be an object.`);let n={};for(let[r,i]of Object.entries(e))if(r===`minTokens`)n.minTokens=v(i,`duplication.minTokens`,t);else if(r===`maxGapTokens`)n.maxGapTokens=_(i,`duplication.maxGapTokens`,t);else if(r===`minSimilarityPercent`){let e=v(i,`duplication.minSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "duplication.minSimilarityPercent" must be between 1 and 100.`);n.minSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${r}" in "duplication" (expected minTokens, maxGapTokens, or minSimilarityPercent).`);return n}function g(e,t,n){return y(e,t,n,Number.isFinite,`a non-negative number`)}function _(e,t,n){return y(e,t,n,Number.isSafeInteger,`a non-negative integer`)}function v(e,t,n){let r=y(e,t,n,Number.isSafeInteger,`a positive integer`);if(r<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return r}function y(e,t,n,r,i){if(typeof e!=`number`||!r(e)||e<0)throw Error(`Config file "${n}": "${t}" must be ${i}.`);return e}function b(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function x(e){return e instanceof Error?e.message:String(e)}exports.configFileName=a,exports.loadConfig=c,exports.resolveGateOptions=s,exports.resolveOptions=o;
1
+ "use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./duplication.cjs"),n=require("./regressionGate.cjs");let r=require("node:fs/promises"),i=require("node:path");i=e.__toESM(i,1);const a=`code-gauge.config.json`;function o(e,{config:n,directory:r}){return{duplication:{minTokens:e.duplicationMinTokens??n.duplication?.minTokens??t.defaultDuplicationOptions.minTokens,maxGapTokens:e.duplicationMaxGapTokens??n.duplication?.maxGapTokens??t.defaultDuplicationOptions.maxGapTokens,minSimilarityPercent:e.duplicationMinSimilarityPercent??n.duplication?.minSimilarityPercent??t.defaultDuplicationOptions.minSimilarityPercent},exclude:{patterns:n.exclude??[],root:r},top:e.top??n.rank?.top??10,includeTests:e.includeTests??n.includeTests??!1,failOnError:e.failOnError??n.failOnError??!1,json:e.json??!1}}function s({config:e}){return{newFunction:{...n.defaultGateOptions.newFunction,...e.gate?.newFunction},tolerance:{...n.defaultGateOptions.tolerance,...e.gate?.tolerance},matchSimilarityPercent:e.gate?.matchSimilarityPercent??n.defaultGateOptions.matchSimilarityPercent}}async function c(e,t){let n=e??await l(t);if(!n)return{config:{},directory:t};let a;try{a=await(0,r.readFile)(n,`utf8`)}catch(r){if(e)throw Error(`Cannot read config file "${n}": ${S(r)}`);return{config:{},directory:t}}let o;try{o=JSON.parse(a)}catch(e){throw Error(`Invalid JSON in config file "${n}": ${S(e)}`)}return{config:d(o,n),directory:i.default.dirname(i.default.resolve(n))}}async function l(e){let t=e;for(;;){let e=i.default.join(t,a);if(await u(e))return e;let n=i.default.dirname(t);if(n===t)return;t=n}}async function u(e){try{return(await(0,r.stat)(e)).isFile()}catch{return!1}}function d(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r=/* @__PURE__ */ new Set([`duplication`,`rank`,`gate`,`exclude`,`includeTests`,`failOnError`]);for(let e of Object.keys(n))if(!r.has(e))throw Error(`Config file "${t}": unknown setting "${e}" (expected ${[...r].join(`, `)}).`);let i={};n.duplication!==void 0&&(i.duplication=g(n.duplication,t)),n.rank!==void 0&&(i.rank=p(n.rank,t)),n.gate!==void 0&&(i.gate=m(n.gate,t)),n.exclude!==void 0&&(i.exclude=f(n.exclude,t));for(let e of[`includeTests`,`failOnError`])n[e]!==void 0&&(i[e]=x(n[e],e,t));return i}function f(e,t){if(!Array.isArray(e)||!e.every(e=>typeof e==`string`&&e!==``))throw TypeError(`Config file "${t}": "exclude" must be an array of non-empty glob patterns.`);return e}function p(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "rank" must be an object.`);let n={};for(let[r,i]of Object.entries(e)){if(r!==`top`)throw Error(`Config file "${t}": unknown setting "${r}" in "rank" (expected top).`);n.top=y(i,`rank.top`,t)}return n}function m(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "gate" must be an object.`);let r={};for(let[i,a]of Object.entries(e))if(i===`newFunction`)r.newFunction=h(a,`gate.newFunction`,Object.keys(n.defaultGateOptions.newFunction),t,v);else if(i===`tolerance`)r.tolerance=h(a,`gate.tolerance`,Object.keys(n.defaultGateOptions.tolerance),t,_);else if(i===`matchSimilarityPercent`){let e=y(a,`gate.matchSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "gate.matchSimilarityPercent" must be between 1 and 100.`);r.matchSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${i}" in "gate" (expected newFunction, tolerance, or matchSimilarityPercent).`);return r}function h(e,t,n,r,i){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${r}": "${t}" must be an object.`);let a={};for(let[o,s]of Object.entries(e)){if(!n.includes(o))throw Error(`Config file "${r}": unknown setting "${o}" in "${t}" (expected ${n.join(`, `)}).`);a[o]=i(s,`${t}.${o}`,r)}return a}function g(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "duplication" must be an object.`);let n={};for(let[r,i]of Object.entries(e))if(r===`minTokens`)n.minTokens=y(i,`duplication.minTokens`,t);else if(r===`maxGapTokens`)n.maxGapTokens=v(i,`duplication.maxGapTokens`,t);else if(r===`minSimilarityPercent`){let e=y(i,`duplication.minSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "duplication.minSimilarityPercent" must be between 1 and 100.`);n.minSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${r}" in "duplication" (expected minTokens, maxGapTokens, or minSimilarityPercent).`);return n}function _(e,t,n){return b(e,t,n,Number.isFinite,`a non-negative number`)}function v(e,t,n){return b(e,t,n,Number.isSafeInteger,`a non-negative integer`)}function y(e,t,n){let r=b(e,t,n,Number.isSafeInteger,`a positive integer`);if(r<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return r}function b(e,t,n,r,i){if(typeof e!=`number`||!r(e)||e<0)throw Error(`Config file "${n}": "${t}" must be ${i}.`);return e}function x(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function S(e){return e instanceof Error?e.message:String(e)}exports.configFileName=a,exports.loadConfig=c,exports.resolveGateOptions=s,exports.resolveOptions=o;
2
2
  //# sourceMappingURL=cliConfig.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"cliConfig.cjs","names":["defaultDuplicationOptions","defaultGateOptions","readFile","path","stat"],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport {\n defaultGateOptions,\n type GateOptions,\n type GateTolerances,\n type NewFunctionThresholds,\n} from './regressionGate.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */\nexport interface GateConfig {\n newFunction?: Partial<NewFunctionThresholds>;\n tolerance?: Partial<GateTolerances>;\n matchSimilarityPercent?: number;\n}\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n /** Regression-gate settings for `code-gauge diff`. */\n gate?: GateConfig;\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */\nexport function resolveGateOptions(config: CodeGaugeConfig): GateOptions {\n return {\n newFunction: { ...defaultGateOptions.newFunction, ...config.gate?.newFunction },\n tolerance: { ...defaultGateOptions.tolerance, ...config.gate?.tolerance },\n matchSimilarityPercent: config.gate?.matchSimilarityPercent ?? defaultGateOptions.matchSimilarityPercent,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'gate', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n if (raw.gate !== undefined) {\n config.gate = validateGateObject(raw.gate, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateGateObject(value: unknown, configFile: string): GateConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"gate\" must be an object.`);\n }\n const gate: GateConfig = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'newFunction') {\n // Zero is a meaningful upper bound (branch-free or unnested new functions), so the limits\n // are validated as non-negative rather than positive.\n gate.newFunction = validateGateNumberObject(\n setting,\n 'gate.newFunction',\n Object.keys(defaultGateOptions.newFunction),\n configFile,\n requireNonNegativeInteger\n ) as Partial<NewFunctionThresholds>;\n } else if (key === 'tolerance') {\n gate.tolerance = validateGateNumberObject(\n setting,\n 'gate.tolerance',\n Object.keys(defaultGateOptions.tolerance),\n configFile,\n requireNonNegativeNumber\n ) as Partial<GateTolerances>;\n } else if (key === 'matchSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'gate.matchSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"gate.matchSimilarityPercent\" must be between 1 and 100.`);\n }\n gate.matchSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"gate\" (expected newFunction, tolerance, or matchSimilarityPercent).`\n );\n }\n }\n return gate;\n}\n\nfunction validateGateNumberObject(\n value: unknown,\n settingName: string,\n knownKeys: string[],\n configFile: string,\n requireNumber: (value: unknown, key: string, configFile: string) => number\n): Record<string, number> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n const validated: Record<string, number> = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${knownKeys.join(', ')}).`\n );\n }\n validated[key] = requireNumber(setting, `${settingName}.${key}`, configFile);\n }\n return validated;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\n/** Tolerances may be fractional (e.g. Halstead volume), so only finiteness and sign are checked. */\nfunction requireNonNegativeNumber(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isFinite, 'a non-negative number');\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isSafeInteger, 'a non-negative integer');\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n const parsed = requireNumber(value, key, configFile, Number.isSafeInteger, 'a positive integer');\n if (parsed < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return parsed;\n}\n\n/** Shared core of the numeric validators: the right kind of number, and never negative. */\nfunction requireNumber(\n value: unknown,\n key: string,\n configFile: string,\n isValidKind: (value: unknown) => boolean,\n description: string\n): number {\n if (typeof value !== 'number' || !isValidKind(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be ${description}.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cliConfig.cjs","names":["defaultDuplicationOptions","defaultGateOptions","readFile","path","stat"],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport type { ExcludePatterns } from './exclusion.js';\nimport {\n defaultGateOptions,\n type GateOptions,\n type GateTolerances,\n type NewFunctionThresholds,\n} from './regressionGate.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */\nexport interface GateConfig {\n newFunction?: Partial<NewFunctionThresholds>;\n tolerance?: Partial<GateTolerances>;\n matchSimilarityPercent?: number;\n}\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n /** Regression-gate settings for `code-gauge diff`. */\n gate?: GateConfig;\n /** Glob patterns, relative to the config file's directory, of files left out of every command. */\n exclude?: string[];\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** A parsed configuration file and the directory its relative settings resolve against. */\nexport interface LoadedConfig {\n config: CodeGaugeConfig;\n /** The config file's directory; the search start directory when no config file was found. */\n directory: string;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n exclude: ExcludePatterns;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, { config, directory }: LoadedConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n exclude: { patterns: config.exclude ?? [], root: directory },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */\nexport function resolveGateOptions({ config }: LoadedConfig): GateOptions {\n return {\n newFunction: { ...defaultGateOptions.newFunction, ...config.gate?.newFunction },\n tolerance: { ...defaultGateOptions.tolerance, ...config.gate?.tolerance },\n matchSimilarityPercent: config.gate?.matchSimilarityPercent ?? defaultGateOptions.matchSimilarityPercent,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<LoadedConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return { config: {}, directory: targetDirectory };\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return { config: {}, directory: targetDirectory };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return { config: validateConfig(parsed, configFile), directory: path.dirname(path.resolve(configFile)) };\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'gate', 'exclude', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n if (raw.gate !== undefined) {\n config.gate = validateGateObject(raw.gate, configFile);\n }\n\n if (raw.exclude !== undefined) {\n config.exclude = validateExcludePatterns(raw.exclude, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateExcludePatterns(value: unknown, configFile: string): string[] {\n if (!Array.isArray(value) || !value.every((pattern) => typeof pattern === 'string' && pattern !== '')) {\n throw new TypeError(`Config file \"${configFile}\": \"exclude\" must be an array of non-empty glob patterns.`);\n }\n return value as string[];\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateGateObject(value: unknown, configFile: string): GateConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"gate\" must be an object.`);\n }\n const gate: GateConfig = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'newFunction') {\n // Zero is a meaningful upper bound (branch-free or unnested new functions), so the limits\n // are validated as non-negative rather than positive.\n gate.newFunction = validateGateNumberObject(\n setting,\n 'gate.newFunction',\n Object.keys(defaultGateOptions.newFunction),\n configFile,\n requireNonNegativeInteger\n ) as Partial<NewFunctionThresholds>;\n } else if (key === 'tolerance') {\n gate.tolerance = validateGateNumberObject(\n setting,\n 'gate.tolerance',\n Object.keys(defaultGateOptions.tolerance),\n configFile,\n requireNonNegativeNumber\n ) as Partial<GateTolerances>;\n } else if (key === 'matchSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'gate.matchSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"gate.matchSimilarityPercent\" must be between 1 and 100.`);\n }\n gate.matchSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"gate\" (expected newFunction, tolerance, or matchSimilarityPercent).`\n );\n }\n }\n return gate;\n}\n\nfunction validateGateNumberObject(\n value: unknown,\n settingName: string,\n knownKeys: string[],\n configFile: string,\n requireNumber: (value: unknown, key: string, configFile: string) => number\n): Record<string, number> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n const validated: Record<string, number> = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${knownKeys.join(', ')}).`\n );\n }\n validated[key] = requireNumber(setting, `${settingName}.${key}`, configFile);\n }\n return validated;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\n/** Tolerances may be fractional (e.g. Halstead volume), so only finiteness and sign are checked. */\nfunction requireNonNegativeNumber(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isFinite, 'a non-negative number');\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isSafeInteger, 'a non-negative integer');\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n const parsed = requireNumber(value, key, configFile, Number.isSafeInteger, 'a positive integer');\n if (parsed < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return parsed;\n}\n\n/** Shared core of the numeric validators: the right kind of number, and never negative. */\nfunction requireNumber(\n value: unknown,\n key: string,\n configFile: string,\n isValidKind: (value: unknown) => boolean,\n description: string\n): number {\n if (typeof value !== 'number' || !isValidKind(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be ${description}.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import type { ExcludePatterns } from './exclusion.js';
1
2
  import { type GateOptions, type GateTolerances, type NewFunctionThresholds } from './regressionGate.js';
2
3
  import type { DuplicationOptions } from './types.js';
3
4
  export declare const configFileName = "code-gauge.config.json";
@@ -18,9 +19,17 @@ export interface CodeGaugeConfig {
18
19
  };
19
20
  /** Regression-gate settings for `code-gauge diff`. */
20
21
  gate?: GateConfig;
22
+ /** Glob patterns, relative to the config file's directory, of files left out of every command. */
23
+ exclude?: string[];
21
24
  includeTests?: boolean;
22
25
  failOnError?: boolean;
23
26
  }
27
+ /** A parsed configuration file and the directory its relative settings resolve against. */
28
+ export interface LoadedConfig {
29
+ config: CodeGaugeConfig;
30
+ /** The config file's directory; the search start directory when no config file was found. */
31
+ directory: string;
32
+ }
24
33
  /** Raw command-line options; every field is undefined unless the user passed the flag. */
25
34
  export interface CliOptions {
26
35
  config?: string;
@@ -35,6 +44,7 @@ export interface CliOptions {
35
44
  /** Options after merging command-line flags, the configuration file, and the built-in defaults. */
36
45
  export interface ResolvedOptions {
37
46
  duplication: Required<DuplicationOptions>;
47
+ exclude: ExcludePatterns;
38
48
  /** Number of top-ranked files to report. */
39
49
  top: number;
40
50
  includeTests: boolean;
@@ -42,11 +52,11 @@ export interface ResolvedOptions {
42
52
  json: boolean;
43
53
  }
44
54
  /** Resolves options with precedence command-line flags > configuration file > built-in defaults. */
45
- export declare function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions;
55
+ export declare function resolveOptions(cli: CliOptions, { config, directory }: LoadedConfig): ResolvedOptions;
46
56
  /** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */
47
- export declare function resolveGateOptions(config: CodeGaugeConfig): GateOptions;
57
+ export declare function resolveGateOptions({ config }: LoadedConfig): GateOptions;
48
58
  /**
49
59
  * Loads the configuration file. An explicit path must exist; otherwise the nearest
50
60
  * `code-gauge.config.json` is searched by walking up from the target directory.
51
61
  */
52
- export declare function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig>;
62
+ export declare function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<LoadedConfig>;
package/dist/cliConfig.js CHANGED
@@ -1,2 +1,2 @@
1
- import{defaultDuplicationOptions as e}from"./duplication.js";import{defaultGateOptions as t}from"./regressionGate.js";import{readFile as n,stat as r}from"node:fs/promises";import i from"node:path";const a=`code-gauge.config.json`;function o(t,n){return{duplication:{minTokens:t.duplicationMinTokens??n.duplication?.minTokens??e.minTokens,maxGapTokens:t.duplicationMaxGapTokens??n.duplication?.maxGapTokens??e.maxGapTokens,minSimilarityPercent:t.duplicationMinSimilarityPercent??n.duplication?.minSimilarityPercent??e.minSimilarityPercent},top:t.top??n.rank?.top??10,includeTests:t.includeTests??n.includeTests??!1,failOnError:t.failOnError??n.failOnError??!1,json:t.json??!1}}function s(e){return{newFunction:{...t.newFunction,...e.gate?.newFunction},tolerance:{...t.tolerance,...e.gate?.tolerance},matchSimilarityPercent:e.gate?.matchSimilarityPercent??t.matchSimilarityPercent}}async function c(e,t){let r=e??await l(t);if(!r)return{};let i;try{i=await n(r,`utf8`)}catch(t){if(e)throw Error(`Cannot read config file "${r}": ${x(t)}`);return{}}let a;try{a=JSON.parse(i)}catch(e){throw Error(`Invalid JSON in config file "${r}": ${x(e)}`)}return d(a,r)}async function l(e){let t=e;for(;;){let e=i.join(t,a);if(await u(e))return e;let n=i.dirname(t);if(n===t)return;t=n}}async function u(e){try{return(await r(e)).isFile()}catch{return!1}}function d(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r=new Set([`duplication`,`rank`,`gate`,`includeTests`,`failOnError`]);for(let e of Object.keys(n))if(!r.has(e))throw Error(`Config file "${t}": unknown setting "${e}" (expected ${[...r].join(`, `)}).`);let i={};n.duplication!==void 0&&(i.duplication=h(n.duplication,t)),n.rank!==void 0&&(i.rank=f(n.rank,t)),n.gate!==void 0&&(i.gate=p(n.gate,t));for(let e of[`includeTests`,`failOnError`])n[e]!==void 0&&(i[e]=b(n[e],e,t));return i}function f(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "rank" must be an object.`);let n={};for(let[r,i]of Object.entries(e)){if(r!==`top`)throw Error(`Config file "${t}": unknown setting "${r}" in "rank" (expected top).`);n.top=v(i,`rank.top`,t)}return n}function p(e,n){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${n}": "gate" must be an object.`);let r={};for(let[i,a]of Object.entries(e))if(i===`newFunction`)r.newFunction=m(a,`gate.newFunction`,Object.keys(t.newFunction),n,_);else if(i===`tolerance`)r.tolerance=m(a,`gate.tolerance`,Object.keys(t.tolerance),n,g);else if(i===`matchSimilarityPercent`){let e=v(a,`gate.matchSimilarityPercent`,n);if(e>100)throw Error(`Config file "${n}": "gate.matchSimilarityPercent" must be between 1 and 100.`);r.matchSimilarityPercent=e}else throw Error(`Config file "${n}": unknown setting "${i}" in "gate" (expected newFunction, tolerance, or matchSimilarityPercent).`);return r}function m(e,t,n,r,i){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${r}": "${t}" must be an object.`);let a={};for(let[o,s]of Object.entries(e)){if(!n.includes(o))throw Error(`Config file "${r}": unknown setting "${o}" in "${t}" (expected ${n.join(`, `)}).`);a[o]=i(s,`${t}.${o}`,r)}return a}function h(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "duplication" must be an object.`);let n={};for(let[r,i]of Object.entries(e))if(r===`minTokens`)n.minTokens=v(i,`duplication.minTokens`,t);else if(r===`maxGapTokens`)n.maxGapTokens=_(i,`duplication.maxGapTokens`,t);else if(r===`minSimilarityPercent`){let e=v(i,`duplication.minSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "duplication.minSimilarityPercent" must be between 1 and 100.`);n.minSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${r}" in "duplication" (expected minTokens, maxGapTokens, or minSimilarityPercent).`);return n}function g(e,t,n){return y(e,t,n,Number.isFinite,`a non-negative number`)}function _(e,t,n){return y(e,t,n,Number.isSafeInteger,`a non-negative integer`)}function v(e,t,n){let r=y(e,t,n,Number.isSafeInteger,`a positive integer`);if(r<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return r}function y(e,t,n,r,i){if(typeof e!=`number`||!r(e)||e<0)throw Error(`Config file "${n}": "${t}" must be ${i}.`);return e}function b(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function x(e){return e instanceof Error?e.message:String(e)}export{a as configFileName,c as loadConfig,s as resolveGateOptions,o as resolveOptions};
1
+ import{defaultDuplicationOptions as e}from"./duplication.js";import{defaultGateOptions as t}from"./regressionGate.js";import{readFile as n,stat as r}from"node:fs/promises";import i from"node:path";const a=`code-gauge.config.json`;function o(t,{config:n,directory:r}){return{duplication:{minTokens:t.duplicationMinTokens??n.duplication?.minTokens??e.minTokens,maxGapTokens:t.duplicationMaxGapTokens??n.duplication?.maxGapTokens??e.maxGapTokens,minSimilarityPercent:t.duplicationMinSimilarityPercent??n.duplication?.minSimilarityPercent??e.minSimilarityPercent},exclude:{patterns:n.exclude??[],root:r},top:t.top??n.rank?.top??10,includeTests:t.includeTests??n.includeTests??!1,failOnError:t.failOnError??n.failOnError??!1,json:t.json??!1}}function s({config:e}){return{newFunction:{...t.newFunction,...e.gate?.newFunction},tolerance:{...t.tolerance,...e.gate?.tolerance},matchSimilarityPercent:e.gate?.matchSimilarityPercent??t.matchSimilarityPercent}}async function c(e,t){let r=e??await l(t);if(!r)return{config:{},directory:t};let a;try{a=await n(r,`utf8`)}catch(n){if(e)throw Error(`Cannot read config file "${r}": ${S(n)}`);return{config:{},directory:t}}let o;try{o=JSON.parse(a)}catch(e){throw Error(`Invalid JSON in config file "${r}": ${S(e)}`)}return{config:d(o,r),directory:i.dirname(i.resolve(r))}}async function l(e){let t=e;for(;;){let e=i.join(t,a);if(await u(e))return e;let n=i.dirname(t);if(n===t)return;t=n}}async function u(e){try{return(await r(e)).isFile()}catch{return!1}}function d(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}" must contain a JSON object.`);let n=e,r=/* @__PURE__ */ new Set([`duplication`,`rank`,`gate`,`exclude`,`includeTests`,`failOnError`]);for(let e of Object.keys(n))if(!r.has(e))throw Error(`Config file "${t}": unknown setting "${e}" (expected ${[...r].join(`, `)}).`);let i={};n.duplication!==void 0&&(i.duplication=g(n.duplication,t)),n.rank!==void 0&&(i.rank=p(n.rank,t)),n.gate!==void 0&&(i.gate=m(n.gate,t)),n.exclude!==void 0&&(i.exclude=f(n.exclude,t));for(let e of[`includeTests`,`failOnError`])n[e]!==void 0&&(i[e]=x(n[e],e,t));return i}function f(e,t){if(!Array.isArray(e)||!e.every(e=>typeof e==`string`&&e!==``))throw TypeError(`Config file "${t}": "exclude" must be an array of non-empty glob patterns.`);return e}function p(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "rank" must be an object.`);let n={};for(let[r,i]of Object.entries(e)){if(r!==`top`)throw Error(`Config file "${t}": unknown setting "${r}" in "rank" (expected top).`);n.top=y(i,`rank.top`,t)}return n}function m(e,n){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${n}": "gate" must be an object.`);let r={};for(let[i,a]of Object.entries(e))if(i===`newFunction`)r.newFunction=h(a,`gate.newFunction`,Object.keys(t.newFunction),n,v);else if(i===`tolerance`)r.tolerance=h(a,`gate.tolerance`,Object.keys(t.tolerance),n,_);else if(i===`matchSimilarityPercent`){let e=y(a,`gate.matchSimilarityPercent`,n);if(e>100)throw Error(`Config file "${n}": "gate.matchSimilarityPercent" must be between 1 and 100.`);r.matchSimilarityPercent=e}else throw Error(`Config file "${n}": unknown setting "${i}" in "gate" (expected newFunction, tolerance, or matchSimilarityPercent).`);return r}function h(e,t,n,r,i){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${r}": "${t}" must be an object.`);let a={};for(let[o,s]of Object.entries(e)){if(!n.includes(o))throw Error(`Config file "${r}": unknown setting "${o}" in "${t}" (expected ${n.join(`, `)}).`);a[o]=i(s,`${t}.${o}`,r)}return a}function g(e,t){if(typeof e!=`object`||!e||Array.isArray(e))throw Error(`Config file "${t}": "duplication" must be an object.`);let n={};for(let[r,i]of Object.entries(e))if(r===`minTokens`)n.minTokens=y(i,`duplication.minTokens`,t);else if(r===`maxGapTokens`)n.maxGapTokens=v(i,`duplication.maxGapTokens`,t);else if(r===`minSimilarityPercent`){let e=y(i,`duplication.minSimilarityPercent`,t);if(e>100)throw Error(`Config file "${t}": "duplication.minSimilarityPercent" must be between 1 and 100.`);n.minSimilarityPercent=e}else throw Error(`Config file "${t}": unknown setting "${r}" in "duplication" (expected minTokens, maxGapTokens, or minSimilarityPercent).`);return n}function _(e,t,n){return b(e,t,n,Number.isFinite,`a non-negative number`)}function v(e,t,n){return b(e,t,n,Number.isSafeInteger,`a non-negative integer`)}function y(e,t,n){let r=b(e,t,n,Number.isSafeInteger,`a positive integer`);if(r<1)throw Error(`Config file "${n}": "${t}" must be a positive integer.`);return r}function b(e,t,n,r,i){if(typeof e!=`number`||!r(e)||e<0)throw Error(`Config file "${n}": "${t}" must be ${i}.`);return e}function x(e,t,n){if(typeof e!=`boolean`)throw TypeError(`Config file "${n}": "${t}" must be a boolean.`);return e}function S(e){return e instanceof Error?e.message:String(e)}export{a as configFileName,c as loadConfig,s as resolveGateOptions,o as resolveOptions};
2
2
  //# sourceMappingURL=cliConfig.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cliConfig.js","names":[],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport {\n defaultGateOptions,\n type GateOptions,\n type GateTolerances,\n type NewFunctionThresholds,\n} from './regressionGate.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */\nexport interface GateConfig {\n newFunction?: Partial<NewFunctionThresholds>;\n tolerance?: Partial<GateTolerances>;\n matchSimilarityPercent?: number;\n}\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n /** Regression-gate settings for `code-gauge diff`. */\n gate?: GateConfig;\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, config: CodeGaugeConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */\nexport function resolveGateOptions(config: CodeGaugeConfig): GateOptions {\n return {\n newFunction: { ...defaultGateOptions.newFunction, ...config.gate?.newFunction },\n tolerance: { ...defaultGateOptions.tolerance, ...config.gate?.tolerance },\n matchSimilarityPercent: config.gate?.matchSimilarityPercent ?? defaultGateOptions.matchSimilarityPercent,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<CodeGaugeConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return {};\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return validateConfig(parsed, configFile);\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'gate', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n if (raw.gate !== undefined) {\n config.gate = validateGateObject(raw.gate, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateGateObject(value: unknown, configFile: string): GateConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"gate\" must be an object.`);\n }\n const gate: GateConfig = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'newFunction') {\n // Zero is a meaningful upper bound (branch-free or unnested new functions), so the limits\n // are validated as non-negative rather than positive.\n gate.newFunction = validateGateNumberObject(\n setting,\n 'gate.newFunction',\n Object.keys(defaultGateOptions.newFunction),\n configFile,\n requireNonNegativeInteger\n ) as Partial<NewFunctionThresholds>;\n } else if (key === 'tolerance') {\n gate.tolerance = validateGateNumberObject(\n setting,\n 'gate.tolerance',\n Object.keys(defaultGateOptions.tolerance),\n configFile,\n requireNonNegativeNumber\n ) as Partial<GateTolerances>;\n } else if (key === 'matchSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'gate.matchSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"gate.matchSimilarityPercent\" must be between 1 and 100.`);\n }\n gate.matchSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"gate\" (expected newFunction, tolerance, or matchSimilarityPercent).`\n );\n }\n }\n return gate;\n}\n\nfunction validateGateNumberObject(\n value: unknown,\n settingName: string,\n knownKeys: string[],\n configFile: string,\n requireNumber: (value: unknown, key: string, configFile: string) => number\n): Record<string, number> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n const validated: Record<string, number> = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${knownKeys.join(', ')}).`\n );\n }\n validated[key] = requireNumber(setting, `${settingName}.${key}`, configFile);\n }\n return validated;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\n/** Tolerances may be fractional (e.g. Halstead volume), so only finiteness and sign are checked. */\nfunction requireNonNegativeNumber(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isFinite, 'a non-negative number');\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isSafeInteger, 'a non-negative integer');\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n const parsed = requireNumber(value, key, configFile, Number.isSafeInteger, 'a positive integer');\n if (parsed < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return parsed;\n}\n\n/** Shared core of the numeric validators: the right kind of number, and never negative. */\nfunction requireNumber(\n value: unknown,\n key: string,\n configFile: string,\n isValidKind: (value: unknown) => boolean,\n description: string\n): number {\n if (typeof value !== 'number' || !isValidKind(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be ${description}.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cliConfig.js","names":[],"sources":["../src/cliConfig.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { defaultDuplicationOptions } from './duplication.js';\nimport type { ExcludePatterns } from './exclusion.js';\nimport {\n defaultGateOptions,\n type GateOptions,\n type GateTolerances,\n type NewFunctionThresholds,\n} from './regressionGate.js';\nimport type { DuplicationOptions } from './types.js';\n\nexport const configFileName = 'code-gauge.config.json';\nexport const defaultTopFileCount = 10;\n\n/** Regression-gate settings for `code-gauge diff`; unset fields use the built-in defaults. */\nexport interface GateConfig {\n newFunction?: Partial<NewFunctionThresholds>;\n tolerance?: Partial<GateTolerances>;\n matchSimilarityPercent?: number;\n}\n\n/** Shape of the JSON configuration file. All fields are optional and fall back to the built-in defaults. */\nexport interface CodeGaugeConfig {\n /** Duplication detection settings applied to every measured file. */\n duplication?: DuplicationOptions;\n /** Refactoring-candidate ranking settings. */\n rank?: { top?: number };\n /** Regression-gate settings for `code-gauge diff`. */\n gate?: GateConfig;\n /** Glob patterns, relative to the config file's directory, of files left out of every command. */\n exclude?: string[];\n includeTests?: boolean;\n failOnError?: boolean;\n}\n\n/** A parsed configuration file and the directory its relative settings resolve against. */\nexport interface LoadedConfig {\n config: CodeGaugeConfig;\n /** The config file's directory; the search start directory when no config file was found. */\n directory: string;\n}\n\n/** Raw command-line options; every field is undefined unless the user passed the flag. */\nexport interface CliOptions {\n config?: string;\n top?: number;\n duplicationMinTokens?: number;\n duplicationMaxGapTokens?: number;\n duplicationMinSimilarityPercent?: number;\n includeTests?: boolean;\n failOnError?: boolean;\n json?: boolean;\n}\n\n/** Options after merging command-line flags, the configuration file, and the built-in defaults. */\nexport interface ResolvedOptions {\n duplication: Required<DuplicationOptions>;\n exclude: ExcludePatterns;\n /** Number of top-ranked files to report. */\n top: number;\n includeTests: boolean;\n failOnError: boolean;\n json: boolean;\n}\n\n/** Resolves options with precedence command-line flags > configuration file > built-in defaults. */\nexport function resolveOptions(cli: CliOptions, { config, directory }: LoadedConfig): ResolvedOptions {\n return {\n duplication: {\n minTokens: cli.duplicationMinTokens ?? config.duplication?.minTokens ?? defaultDuplicationOptions.minTokens,\n maxGapTokens:\n cli.duplicationMaxGapTokens ?? config.duplication?.maxGapTokens ?? defaultDuplicationOptions.maxGapTokens,\n minSimilarityPercent:\n cli.duplicationMinSimilarityPercent ??\n config.duplication?.minSimilarityPercent ??\n defaultDuplicationOptions.minSimilarityPercent,\n },\n exclude: { patterns: config.exclude ?? [], root: directory },\n top: cli.top ?? config.rank?.top ?? defaultTopFileCount,\n includeTests: cli.includeTests ?? config.includeTests ?? false,\n failOnError: cli.failOnError ?? config.failOnError ?? false,\n json: cli.json ?? false,\n };\n}\n\n/** Resolves the regression-gate settings with precedence configuration file > built-in defaults. */\nexport function resolveGateOptions({ config }: LoadedConfig): GateOptions {\n return {\n newFunction: { ...defaultGateOptions.newFunction, ...config.gate?.newFunction },\n tolerance: { ...defaultGateOptions.tolerance, ...config.gate?.tolerance },\n matchSimilarityPercent: config.gate?.matchSimilarityPercent ?? defaultGateOptions.matchSimilarityPercent,\n };\n}\n\n/**\n * Loads the configuration file. An explicit path must exist; otherwise the nearest\n * `code-gauge.config.json` is searched by walking up from the target directory.\n */\nexport async function loadConfig(explicitPath: string | undefined, targetDirectory: string): Promise<LoadedConfig> {\n const configFile = explicitPath ?? (await findNearestConfig(targetDirectory));\n if (!configFile) {\n return { config: {}, directory: targetDirectory };\n }\n\n let content;\n try {\n content = await readFile(configFile, 'utf8');\n } catch (error) {\n if (explicitPath) {\n throw new Error(`Cannot read config file \"${configFile}\": ${formatError(error)}`);\n }\n return { config: {}, directory: targetDirectory };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch (error) {\n throw new Error(`Invalid JSON in config file \"${configFile}\": ${formatError(error)}`);\n }\n\n return { config: validateConfig(parsed, configFile), directory: path.dirname(path.resolve(configFile)) };\n}\n\nasync function findNearestConfig(targetDirectory: string): Promise<string | undefined> {\n let currentDirectory = targetDirectory;\n while (true) {\n const configFile = path.join(currentDirectory, configFileName);\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nfunction validateConfig(value: unknown, configFile: string): CodeGaugeConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\" must contain a JSON object.`);\n }\n\n const raw = value as Record<string, unknown>;\n const knownKeys = new Set(['duplication', 'rank', 'gate', 'exclude', 'includeTests', 'failOnError']);\n for (const key of Object.keys(raw)) {\n if (!knownKeys.has(key)) {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" (expected ${[...knownKeys].join(', ')}).`);\n }\n }\n const config: CodeGaugeConfig = {};\n\n if (raw.duplication !== undefined) {\n config.duplication = validateDuplicationObject(raw.duplication, configFile);\n }\n\n if (raw.rank !== undefined) {\n config.rank = validateRankObject(raw.rank, configFile);\n }\n\n if (raw.gate !== undefined) {\n config.gate = validateGateObject(raw.gate, configFile);\n }\n\n if (raw.exclude !== undefined) {\n config.exclude = validateExcludePatterns(raw.exclude, configFile);\n }\n\n for (const key of ['includeTests', 'failOnError'] as const) {\n if (raw[key] !== undefined) {\n config[key] = requireBoolean(raw[key], key, configFile);\n }\n }\n\n return config;\n}\n\nfunction validateExcludePatterns(value: unknown, configFile: string): string[] {\n if (!Array.isArray(value) || !value.every((pattern) => typeof pattern === 'string' && pattern !== '')) {\n throw new TypeError(`Config file \"${configFile}\": \"exclude\" must be an array of non-empty glob patterns.`);\n }\n return value as string[];\n}\n\nfunction validateRankObject(value: unknown, configFile: string): { top?: number } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"rank\" must be an object.`);\n }\n const rank: { top?: number } = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key !== 'top') {\n throw new Error(`Config file \"${configFile}\": unknown setting \"${key}\" in \"rank\" (expected top).`);\n }\n rank.top = requirePositiveInteger(setting, 'rank.top', configFile);\n }\n return rank;\n}\n\nfunction validateGateObject(value: unknown, configFile: string): GateConfig {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"gate\" must be an object.`);\n }\n const gate: GateConfig = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'newFunction') {\n // Zero is a meaningful upper bound (branch-free or unnested new functions), so the limits\n // are validated as non-negative rather than positive.\n gate.newFunction = validateGateNumberObject(\n setting,\n 'gate.newFunction',\n Object.keys(defaultGateOptions.newFunction),\n configFile,\n requireNonNegativeInteger\n ) as Partial<NewFunctionThresholds>;\n } else if (key === 'tolerance') {\n gate.tolerance = validateGateNumberObject(\n setting,\n 'gate.tolerance',\n Object.keys(defaultGateOptions.tolerance),\n configFile,\n requireNonNegativeNumber\n ) as Partial<GateTolerances>;\n } else if (key === 'matchSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'gate.matchSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"gate.matchSimilarityPercent\" must be between 1 and 100.`);\n }\n gate.matchSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"gate\" (expected newFunction, tolerance, or matchSimilarityPercent).`\n );\n }\n }\n return gate;\n}\n\nfunction validateGateNumberObject(\n value: unknown,\n settingName: string,\n knownKeys: string[],\n configFile: string,\n requireNumber: (value: unknown, key: string, configFile: string) => number\n): Record<string, number> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"${settingName}\" must be an object.`);\n }\n const validated: Record<string, number> = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (!knownKeys.includes(key)) {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"${settingName}\" (expected ${knownKeys.join(', ')}).`\n );\n }\n validated[key] = requireNumber(setting, `${settingName}.${key}`, configFile);\n }\n return validated;\n}\n\nfunction validateDuplicationObject(value: unknown, configFile: string): DuplicationOptions {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`Config file \"${configFile}\": \"duplication\" must be an object.`);\n }\n const duplication: DuplicationOptions = {};\n for (const [key, setting] of Object.entries(value as Record<string, unknown>)) {\n if (key === 'minTokens') {\n duplication.minTokens = requirePositiveInteger(setting, 'duplication.minTokens', configFile);\n } else if (key === 'maxGapTokens') {\n // 0 is meaningful: it disables gapped-clone merging.\n duplication.maxGapTokens = requireNonNegativeInteger(setting, 'duplication.maxGapTokens', configFile);\n } else if (key === 'minSimilarityPercent') {\n const parsed = requirePositiveInteger(setting, 'duplication.minSimilarityPercent', configFile);\n if (parsed > 100) {\n throw new Error(`Config file \"${configFile}\": \"duplication.minSimilarityPercent\" must be between 1 and 100.`);\n }\n duplication.minSimilarityPercent = parsed;\n } else {\n throw new Error(\n `Config file \"${configFile}\": unknown setting \"${key}\" in \"duplication\" (expected minTokens, maxGapTokens, or minSimilarityPercent).`\n );\n }\n }\n return duplication;\n}\n\n/** Tolerances may be fractional (e.g. Halstead volume), so only finiteness and sign are checked. */\nfunction requireNonNegativeNumber(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isFinite, 'a non-negative number');\n}\n\nfunction requireNonNegativeInteger(value: unknown, key: string, configFile: string): number {\n return requireNumber(value, key, configFile, Number.isSafeInteger, 'a non-negative integer');\n}\n\nfunction requirePositiveInteger(value: unknown, key: string, configFile: string): number {\n const parsed = requireNumber(value, key, configFile, Number.isSafeInteger, 'a positive integer');\n if (parsed < 1) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be a positive integer.`);\n }\n return parsed;\n}\n\n/** Shared core of the numeric validators: the right kind of number, and never negative. */\nfunction requireNumber(\n value: unknown,\n key: string,\n configFile: string,\n isValidKind: (value: unknown) => boolean,\n description: string\n): number {\n if (typeof value !== 'number' || !isValidKind(value) || value < 0) {\n throw new Error(`Config file \"${configFile}\": \"${key}\" must be ${description}.`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, key: string, configFile: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`Config file \"${configFile}\": \"${key}\" must be a boolean.`);\n }\n return value;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:o,maxGapTokens:d,minSimilarityPercent:f}=e.resolveDuplicationOptions(r),p=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of a(t,o))p.push(e);let m=n.selectMaximalGroups(p,s,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),h=c(t,d),g=l([...m.values()],h,d);for(let e of i(t,g,h,o,f))g.push(e);return u(g,t,h)}function i(e,n,r,i,a){let o=e.map(()=>[]);for(let{fileIndex:e,startTokenIndex:t,endTokenIndex:i}of n.flat()){let n=r[e]??0;o[e]?.push({startTokenIndex:t-n,endTokenIndex:i-n})}return t.collectCrossFileNearMissGroups(e,o,i,a).map(t=>t.map(t=>{let n=r[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>({startTokenIndex:e.startTokenIndex+n,endTokenIndex:e.endTokenIndex+n})),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function a(t,n){let r=[],i=[];for(let[n,{tokens:a,containerStatements:o}]of t.entries())a&&o&&(r.push(n),i.push({tokens:a,literalCountPrefix:e.buildLiteralCountPrefix(a),containers:o}));return i.length<2?[]:e.collectSequenceWindowCandidates(i,n,!0).flatMap(({candidate:e,contextIndex:n})=>{let i=r[n],a=i===void 0?void 0:t[i];return i===void 0||a===void 0?[]:[{...e,regionBucket:i,file:a.file}]})}function o(e){return new Set(e.map(e=>e.file)).size>=2}function s(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function c(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function l(t,n,r){let i=t.map(e=>e.map(e=>{let t=e.startTokenIndex+(n[e.regionBucket]??0),r=e.endTokenIndex+(n[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:t,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:t,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return e.mergeAdjacentGroups(i,r,o)}function u(t,n,r){let i=[],a=new Map,o=new Map(n.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:r[t]??0}])),s=new Map,c=0;for(let n of t){c+=e.countRedundantFragments(n);for(let e of n)d(e,o,s);let t=n.map(({file:e,startLine:t,endLine:n})=>({file:e,startLine:t,endLine:n})).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),r=[...new Set(t.map(({file:e})=>e))];for(let e of r)a.set(e,(a.get(e)??0)+1);i.push({files:r,occurrences:t,tokenCount:Math.min(...n.map(({tokenCount:e})=>e))})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function d(t,n,r){let i=n.get(t.file);if(!i?.tokens)return;let a=r.get(t.file);a||(a=new Set,r.set(t.file,a));for(let n of t.segments)e.collectSegmentLines({startTokenIndex:n.startTokenIndex-i.offset,endTokenIndex:n.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,a)}exports.measureCrossFileDuplication=r;
1
+ "use strict";const e=require("./duplication.cjs"),t=require("./crossFileNearMiss.cjs"),n=require("./duplicateSelection.cjs");function r(t,r){let{minTokens:o,maxGapTokens:d,minSimilarityPercent:f}=e.resolveDuplicationOptions(r),p=t.flatMap(({file:e,candidates:t},n)=>t.map(t=>({...t,regionBucket:n,file:e})));for(let e of a(t,o))p.push(e);let m=n.selectMaximalGroups(p,s,(e,t)=>e.regionBucket-t.regionBucket||e.startIndex-t.startIndex),h=c(t,d),g=l([...m.values()],h,d);for(let e of i(t,g,h,o,f))g.push(e);return u(g,t,h)}function i(e,n,r,i,a){let o=e.map(()=>[]);for(let{fileIndex:e,startTokenIndex:t,endTokenIndex:i}of n.flat()){let n=r[e]??0;o[e]?.push({startTokenIndex:t-n,endTokenIndex:i-n})}return t.collectCrossFileNearMissGroups(e,o,i,a).map(t=>t.map(t=>{let n=r[t.fileIndex]??0;return{...t,file:e[t.fileIndex]?.file??``,segments:t.segments.map(e=>({startTokenIndex:e.startTokenIndex+n,endTokenIndex:e.endTokenIndex+n})),startTokenIndex:t.startTokenIndex+n,endTokenIndex:t.endTokenIndex+n}}))}function a(t,n){let r=[],i=[];for(let[n,{tokens:a,containerStatements:o}]of t.entries())a&&o&&(r.push(n),i.push({tokens:a,literalCountPrefix:e.buildLiteralCountPrefix(a),containers:o}));return i.length<2?[]:e.collectSequenceWindowCandidates(i,n,!0).flatMap(({candidate:e,contextIndex:n})=>{let i=r[n],a=i===void 0?void 0:t[i];return i===void 0||a===void 0?[]:[{...e,regionBucket:i,file:a.file}]})}function o(e){return new Set(e.map(e=>e.file)).size>=2}function s(e){return e.length>=2&&new Set(e.map(e=>e.regionBucket)).size>=2}function c(e,t){let n=[],r=0;for(let{tokens:i,candidates:a}of e){n.push(r);let e=i?.length??0;if(!i)for(let t of a)e=Math.max(e,t.endTokenIndex);r+=e+t+1}return n}function l(t,n,r){let i=t.map(e=>e.map(e=>{let t=e.startTokenIndex+(n[e.regionBucket]??0),r=e.endTokenIndex+(n[e.regionBucket]??0);return{file:e.file,fileIndex:e.regionBucket,spanCountedElsewhere:e.nestedInLargerGroup,nestedInLargerGroup:e.nestedInLargerGroup,segments:[{startTokenIndex:t,endTokenIndex:r}],tokenCount:e.tokenCount,startTokenIndex:t,endTokenIndex:r,startIndex:e.startIndex,endIndex:e.endIndex,startLine:e.startLine,endLine:e.endLine}}).toSorted((e,t)=>e.startTokenIndex-t.startTokenIndex));return e.mergeAdjacentGroups(i,r,o)}function u(t,n,r){let i=[],a=/* @__PURE__ */ new Map,o=new Map(n.map((e,t)=>[e.file,{tokens:e.tokens,codeLineNumbers:e.codeLineNumbers,offset:r[t]??0}])),s=/* @__PURE__ */ new Map,c=0;for(let n of t){c+=e.countRedundantFragments(n);for(let e of n)d(e,o,s);let t=n.map(({file:e,startLine:t,endLine:n})=>({file:e,startLine:t,endLine:n})).toSorted((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),r=[...new Set(t.map(({file:e})=>e))];for(let e of r)a.set(e,(a.get(e)??0)+1);i.push({files:r,occurrences:t,tokenCount:Math.min(...n.map(({tokenCount:e})=>e))})}return i.sort((e,t)=>t.tokenCount-e.tokenCount||(e.occurrences[0]?.file??``).localeCompare(t.occurrences[0]?.file??``)||(e.occurrences[0]?.startLine??0)-(t.occurrences[0]?.startLine??0)),{duplicateBlockCount:c,duplicateBlockGroupCountByFile:Object.fromEntries(a),duplicateLineNumbersByFile:Object.fromEntries([...s].map(([e,t])=>[e,[...t].toSorted((e,t)=>e-t)])),groups:i}}function d(t,n,r){let i=n.get(t.file);if(!i?.tokens)return;let a=r.get(t.file);a||(a=/* @__PURE__ */ new Set,r.set(t.file,a));for(let n of t.segments)e.collectSegmentLines({startTokenIndex:n.startTokenIndex-i.offset,endTokenIndex:n.endTokenIndex-i.offset},i.tokens,i.codeLineNumbers,a)}exports.measureCrossFileDuplication=r;
2
2
  //# sourceMappingURL=crossFileDuplication.cjs.map