code-gauge 4.8.0 → 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.
- package/README.md +34 -9
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +13 -3
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/diffCommand.cjs +3 -3
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +3 -3
- package/dist/diffCommand.js.map +1 -1
- package/dist/exclusion.cjs +2 -0
- package/dist/exclusion.cjs.map +1 -0
- package/dist/exclusion.d.ts +30 -0
- package/dist/exclusion.js +2 -0
- package/dist/exclusion.js.map +1 -0
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +16 -0
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/newDuplication.cjs +2 -0
- package/dist/newDuplication.cjs.map +1 -0
- package/dist/newDuplication.d.ts +40 -0
- package/dist/newDuplication.js +2 -0
- package/dist/newDuplication.js.map +1 -0
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.d.ts +8 -6
- package/dist/regressionGate.js +1 -1
- package/dist/regressionGate.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.d.ts +12 -3
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/native/code-gauge.wasm +0 -0
- package/native/src/duplication.rs +105 -2
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regressionGate.cjs","names":["lcsLength"],"sources":["../src/regressionGate.ts"],"sourcesContent":["import { lcsLength } from './duplication.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/**\n * Absolute limits applied only to functions with no base counterpart (SonarQube-style \"Clean as\n * You Code\"): new code is the one place absolute thresholds are required, since there is nothing\n * to ratchet against.\n */\nexport interface NewFunctionThresholds {\n maxCognitiveComplexity: number;\n maxNcss: number;\n maxNestingDepth: number;\n}\n\n/**\n * Allowances added on top of each base value before a ratchet fires, so legitimate refactorings\n * that transiently worsen one number (e.g. an extraction adding a few NCSS lines) don't trap the\n * agent in a loop.\n */\nexport interface GateTolerances {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n /** File-total NCSS growth allowed when base functions disappeared (the anti-gaming backstop). */\n fileNcss: number;\n /** Duplicated-line growth allowed per changed file. */\n duplicateLines: number;\n}\n\nexport interface GateOptions {\n newFunction: NewFunctionThresholds;\n tolerance: GateTolerances;\n /**\n * Minimum token-LCS similarity percent for re-matching a renamed/moved function to its base\n * counterpart, so renames don't appear as delete+add and hit the new-code thresholds.\n */\n matchSimilarityPercent: number;\n}\n\nexport const defaultGateOptions: GateOptions = {\n // 15 is SonarSource's conventional default; 4 is where the measured cognitive-load response to\n // nesting saturates (Peitek et al. 2021); 60 matches PMD's NcssCount method default.\n newFunction: { maxCognitiveComplexity: 15, maxNcss: 60, maxNestingDepth: 4 },\n // The per-metric allowances are calibrated to admit roughly the same edit: ~5 added statements\n // or one added branch (cognitive 2 ~ nesting 1 ~ ncss 5 ~ depDegree 10 ~ Halstead volume 150;\n // the volume allowance additionally scales with the base value, see ratchetMetrics), so one\n // ratchet cannot block an addition the others deliberately allow.\n tolerance: {\n cognitiveComplexity: 2,\n ncss: 5,\n nestingDepth: 1,\n depDegree: 10,\n halsteadVolume: 150,\n fileNcss: 20,\n duplicateLines: 0,\n },\n matchSimilarityPercent: 70,\n};\n\n/** One changed file with both revisions measured; the caller resolves git and duplication data. */\nexport interface GateFileInput {\n /** Display path (repository-relative head path; base path for deleted files). */\n file: string;\n /** Absent for added files. */\n baseMetrics?: CodeMetrics;\n /** Absent for deleted files. */\n headMetrics?: CodeMetrics;\n /** Token sequences index-parallel to baseMetrics.functions, for similarity re-matching. */\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n /** Distinct duplicated lines of this file at the base revision (within-file and cross-file). */\n baseDuplicatedLineCount: number;\n headDuplicatedLineCount: number;\n /** Other files sharing cross-file duplicates with this file at head, as remediation evidence. */\n duplicationPartners: string[];\n /**\n * false: the file only feeds function matching and the duplication universes (it lies outside\n * the gated target directory); no violations are evaluated or reported for it. Default true.\n */\n gated?: boolean;\n}\n\nexport interface GateViolation {\n gate: 'function-regression' | 'new-function' | 'file-regression' | 'duplication';\n metric: string;\n file: string;\n functionName?: string;\n startLine: number;\n endLine: number;\n /** Absent for new-function violations, which have no base value. */\n baseValue?: number;\n headValue: number;\n /** The largest value that would have passed. */\n allowedValue: number;\n /** One-line human/agent-readable statement with the remediation direction. */\n message: string;\n}\n\n/** The gated per-function values of one checked (matched or new) function, for `--full` output. */\nexport interface CheckedFunctionReport {\n /** The head file (a cross-file move is reported where the function now lives). */\n file: string;\n name: string;\n startLine: number;\n endLine: number;\n /** Absent for new functions. */\n base?: GateFunctionValues;\n head: GateFunctionValues;\n}\n\nexport interface GateFunctionValues {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n}\n\nexport interface GateResult {\n violations: GateViolation[];\n checkedFileCount: number;\n checkedFunctionCount: number;\n newFunctionCount: number;\n /** Every checked function with its gated values, ordered by file and line. */\n checkedFunctions: CheckedFunctionReport[];\n}\n\ninterface RatchetMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n /** The growth allowed on top of the base value before the ratchet fires. */\n allowance: (tolerances: GateTolerances, baseValue: number) => number;\n remediation: string;\n format: (value: number) => string;\n}\n\nconst formatInteger = String;\nconst formatFloat = (value: number): string => value.toFixed(1);\n\n/**\n * Gate-eligible per-function metrics (monotone, stable, actionable): a matched function must not\n * worsen any of them beyond its tolerance. \"Your change made this function worse\" is defensible\n * regardless of metric calibration, so no absolute threshold is involved.\n */\nconst ratchetMetrics: RatchetMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n allowance: (tolerances) => tolerances.cognitiveComplexity,\n remediation: 'Simplify the added branching or split the function.',\n format: formatInteger,\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n allowance: (tolerances) => tolerances.ncss,\n remediation: 'Extract the added statements into helper functions or remove them.',\n format: formatInteger,\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n allowance: (tolerances) => tolerances.nestingDepth,\n remediation: 'Flatten the added nesting with early returns or extracted helpers.',\n format: formatInteger,\n },\n {\n metric: 'DepDegree',\n value: (fn) => fn.depDegree,\n allowance: (tolerances) => tolerances.depDegree,\n remediation: 'Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.',\n format: formatInteger,\n },\n {\n metric: 'Halstead volume',\n value: (fn) => fn.halstead.volume,\n // Volume grows as length * log2(vocabulary), so a flat allowance that admits ~5 statements in\n // a small function admits barely one in a large-vocabulary function; the relative term keeps\n // the ~5-statement calibration at every function size.\n allowance: (tolerances, baseValue) => Math.max(tolerances.halsteadVolume, baseValue * 0.25),\n remediation: 'Shrink the function: fewer distinct names and operations.',\n format: formatFloat,\n },\n];\n\ninterface NewFunctionMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n limit: (thresholds: NewFunctionThresholds) => number;\n remediation: string;\n}\n\nconst newFunctionMetrics: NewFunctionMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n limit: (thresholds) => thresholds.maxCognitiveComplexity,\n remediation: 'Split the new function or simplify its branching.',\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n limit: (thresholds) => thresholds.maxNcss,\n remediation: 'Split the new function into smaller ones.',\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n limit: (thresholds) => thresholds.maxNestingDepth,\n remediation: 'Flatten the new function with early returns or extracted helpers.',\n },\n];\n\n/**\n * Evaluates the regression gate over changed files measured at both revisions. Violations are the\n * whole output contract: everything that passes stays silent, so a clean change costs the\n * consuming agent almost no tokens.\n */\nexport function evaluateRegressionGate(files: GateFileInput[], options: GateOptions): GateResult {\n const matching = matchAllFunctions(files, options.matchSimilarityPercent);\n const violations: GateViolation[] = [];\n const checkedFunctions: CheckedFunctionReport[] = [];\n let checkedPairCount = 0;\n let newFunctionCount = 0;\n\n for (const pair of matching.pairs) {\n const file = files[pair.headFileIndex] as GateFileInput;\n if (file.gated === false) {\n continue;\n }\n checkedPairCount += 1;\n violations.push(...checkRatchets(file.file, pair.base, pair.head, options.tolerance));\n checkedFunctions.push(toFunctionReport(file.file, pair.head, pair.base));\n }\n\n let gatedFileCount = 0;\n for (const [fileIndex, file] of files.entries()) {\n if (file.gated === false) {\n continue;\n }\n gatedFileCount += 1;\n const headFunctions = file.headMetrics?.functions ?? [];\n const headMatched = matching.headMatched[fileIndex] as boolean[];\n for (const [functionIndex, fn] of headFunctions.entries()) {\n if (headMatched[functionIndex]) {\n continue;\n }\n newFunctionCount += 1;\n violations.push(...checkNewFunction(file.file, fn, options.newFunction));\n checkedFunctions.push(toFunctionReport(file.file, fn));\n }\n violations.push(...checkFileBackstops(file, hasSplitEvidence(file, fileIndex, matching), violations, options));\n violations.push(...checkDuplication(file, options.tolerance));\n }\n\n violations.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n checkedFunctions.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n return {\n violations,\n checkedFileCount: gatedFileCount,\n checkedFunctionCount: checkedPairCount + newFunctionCount,\n newFunctionCount,\n checkedFunctions,\n };\n}\n\n/**\n * Similarity below the match threshold but above this marks a removed function's content as\n * reappearing inside unmatched new code — the signature of a split or evasive rewrite.\n */\nconst splitEvidenceSimilarityPercent = 30;\n\n/**\n * Whether a removed NAMED base function's content partially reappears in an unmatched head\n * function of the same file. This is what arms the file-level backstops: splitting a function to\n * hide a worsening behind the (laxer) new-code thresholds leaves its fragments partially similar\n * to the removed original, while an ordinary \"delete a helper, add an unrelated function\" change\n * shares nothing and must not have its brand-new code re-judged against the file's old aggregates.\n */\nfunction hasSplitEvidence(file: GateFileInput, fileIndex: number, matching: FunctionMatching): boolean {\n const baseFunctions = file.baseMetrics?.functions ?? [];\n const baseMatchedFlags = matching.baseMatched[fileIndex] as boolean[];\n const headMatchedFlags = matching.headMatched[fileIndex] as boolean[];\n const removedNamed = baseFunctions.flatMap((fn, index) =>\n !baseMatchedFlags[index] && fn.name !== undefined ? [index] : []\n );\n if (removedNamed.length === 0) {\n return false;\n }\n const unmatchedHead = (file.headMetrics?.functions ?? []).flatMap((_, index) =>\n headMatchedFlags[index] ? [] : [index]\n );\n if (removedNamed.length * unmatchedHead.length > maxSimilarityComparisons) {\n return false;\n }\n return removedNamed.some((baseIndex) =>\n unmatchedHead.some(\n (headIndex) =>\n tokenSimilarityPercent(\n file.baseFunctionTokens?.[baseIndex],\n file.headFunctionTokens?.[headIndex],\n splitEvidenceSimilarityPercent\n ) >= splitEvidenceSimilarityPercent\n )\n );\n}\n\nfunction toFunctionReport(file: string, head: FunctionMetrics, base?: FunctionMetrics): CheckedFunctionReport {\n return {\n file,\n name: head.name ?? '<anonymous>',\n startLine: head.startLine,\n endLine: head.endLine,\n base: base ? toFunctionValues(base) : undefined,\n head: toFunctionValues(head),\n };\n}\n\nfunction toFunctionValues(fn: FunctionMetrics): GateFunctionValues {\n return {\n cognitiveComplexity: fn.cognitiveComplexity,\n ncss: fn.ncss,\n nestingDepth: fn.nestingDepth,\n depDegree: fn.depDegree,\n halsteadVolume: fn.halstead.volume,\n };\n}\n\n/** One side of a match candidate: a function of one changed file plus its token sequence. */\ninterface IndexedFunction {\n fileIndex: number;\n functionIndex: number;\n fn: FunctionMetrics;\n tokens: Int32Array | undefined;\n}\n\ninterface MatchedPair {\n base: FunctionMetrics;\n head: FunctionMetrics;\n headFileIndex: number;\n}\n\ninterface FunctionMatching {\n pairs: MatchedPair[];\n /** Per file, per function index: whether it found a counterpart (anywhere in the change set). */\n baseMatched: boolean[][];\n headMatched: boolean[][];\n}\n\ninterface SimilarityCandidate {\n similarity: number;\n basePosition: number;\n headPosition: number;\n}\n\n/**\n * Matches head functions to base functions: within each file first by name + arity, then by name\n * alone (signature changes) — resolving ambiguous same-name groups by token similarity instead of\n * list position, so an inserted overload cannot shift the pairing — and finally by\n * normalized-token LCS similarity across ALL changed files (renames and moves between files,\n * reusing the near-miss clone machinery), so refactorings don't appear as delete+add and hit the\n * new-code thresholds.\n */\nfunction matchAllFunctions(files: GateFileInput[], matchSimilarityPercent: number): FunctionMatching {\n const baseMatched = files.map((file) => (file.baseMetrics?.functions ?? []).map(() => false));\n const headMatched = files.map((file) => (file.headMetrics?.functions ?? []).map(() => false));\n const pairs: MatchedPair[] = [];\n\n for (const fileIndex of files.keys()) {\n const pairByKey = (key: (fn: FunctionMetrics) => string | undefined): void => {\n const baseByKey = groupByKey(collectUnmatched(files, baseMatched, 'base', fileIndex), key);\n const headByKey = groupByKey(collectUnmatched(files, headMatched, 'head', fileIndex), key);\n for (const [groupKey, baseGroup] of baseByKey) {\n pairGroup(baseGroup, headByKey.get(groupKey) ?? [], baseMatched, headMatched, pairs);\n }\n };\n pairByKey((fn) => (fn.name === undefined ? undefined : `${fn.name}\\u0000${fn.parameterCount}`));\n pairByKey((fn) => fn.name);\n }\n\n pairAcrossChangeSet(files, baseMatched, headMatched, pairs, matchSimilarityPercent);\n return { pairs, baseMatched, headMatched };\n}\n\n/** Unmatched functions of one file (or of every file when onlyFileIndex is undefined). */\nfunction collectUnmatched(\n files: GateFileInput[],\n matchedByFile: boolean[][],\n side: 'base' | 'head',\n onlyFileIndex?: number\n): IndexedFunction[] {\n const unmatched: IndexedFunction[] = [];\n for (const [fileIndex, file] of files.entries()) {\n if (onlyFileIndex !== undefined && fileIndex !== onlyFileIndex) {\n continue;\n }\n const matched = matchedByFile[fileIndex] as boolean[];\n const metrics = side === 'base' ? file.baseMetrics : file.headMetrics;\n const tokens = side === 'base' ? file.baseFunctionTokens : file.headFunctionTokens;\n for (const [functionIndex, fn] of (metrics?.functions ?? []).entries()) {\n if (!matched[functionIndex]) {\n unmatched.push({ fileIndex, functionIndex, fn, tokens: tokens?.[functionIndex] });\n }\n }\n }\n return unmatched;\n}\n\nfunction groupByKey(\n functions: IndexedFunction[],\n key: (fn: FunctionMetrics) => string | undefined\n): Map<string, IndexedFunction[]> {\n const groups = new Map<string, IndexedFunction[]>();\n for (const indexed of functions) {\n const groupKey = key(indexed.fn);\n if (groupKey === undefined) {\n continue;\n }\n const group = groups.get(groupKey) ?? [];\n group.push(indexed);\n groups.set(groupKey, group);\n }\n return groups;\n}\n\n/** Bounds the quadratic similarity passes; beyond this, leftovers gate as new/removed functions. */\nconst maxSimilarityComparisons = 10_000;\n\n/**\n * Pairs one key group. An unambiguous 1x1 group matches directly; a larger group (same-name\n * methods of different classes, overload sets) is assigned best-similarity-first so an inserted\n * function cannot shift the pairing of the others. The names already match, so any pairing is\n * acceptable: similarity only ORDERS the assignment (no threshold), and without token sequences\n * (or in an oversized group) the stable tie-break reproduces positional order.\n */\nfunction pairGroup(\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n const ambiguous =\n baseGroup.length + headGroup.length > 2 && baseGroup.length * headGroup.length <= maxSimilarityComparisons;\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of baseGroup.entries()) {\n for (const [headPosition, head] of headGroup.entries()) {\n candidates.push({\n similarity: ambiguous ? tokenSimilarityPercent(base.tokens, head.tokens, 0) : 0,\n basePosition,\n headPosition,\n });\n }\n }\n takeMaximumMatches(candidates, baseGroup, headGroup, baseMatched, headMatched, pairs);\n}\n\n/** Cross-file (and remaining within-file) similarity matching over every unmatched function. */\nfunction pairAcrossChangeSet(\n files: GateFileInput[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[],\n matchSimilarityPercent: number\n): void {\n const unmatchedBase = collectUnmatched(files, baseMatched, 'base');\n const unmatchedHead = collectUnmatched(files, headMatched, 'head');\n if (unmatchedBase.length * unmatchedHead.length > maxSimilarityComparisons) {\n return;\n }\n\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of unmatchedBase.entries()) {\n for (const [headPosition, head] of unmatchedHead.entries()) {\n const similarity = tokenSimilarityPercent(base.tokens, head.tokens, matchSimilarityPercent);\n if (similarity >= matchSimilarityPercent) {\n candidates.push({ similarity, basePosition, headPosition });\n }\n }\n }\n takeMaximumMatches(candidates, unmatchedBase, unmatchedHead, baseMatched, headMatched, pairs);\n}\n\n/**\n * Assignment over the candidate edges: a greedy best-similarity-first seed, then Kuhn's\n * augmenting paths for the bases the seed left unmatched, so a function whose only counterpart\n * was taken by a slightly better pair is re-matched by rewiring instead of being falsely gated as\n * new code. Augmentation runs only when it increases cardinality, and an exact (100%) seed pair —\n * certain identity — is locked and never rewired even then: trading it for two partial matches\n * would ratchet an untouched function against some other function's rewrite. Non-exact seed pairs\n * may be rewired, but only when that matches strictly more functions (which a maximum-weight\n * assignment would also prefer). Without token sequences every similarity ties at 0 and the seed\n * reproduces positional order.\n */\nfunction takeMaximumMatches(\n candidates: SimilarityCandidate[],\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n candidates.sort(\n (left, right) =>\n right.similarity - left.similarity ||\n left.basePosition - right.basePosition ||\n left.headPosition - right.headPosition\n );\n const adjacency = new Map<number, number[]>();\n const baseOfHead = new Map<number, number>();\n const lockedHeads = new Set<number>();\n const matchedBases = seedGreedyMatches(candidates, adjacency, baseOfHead, lockedHeads);\n for (const basePosition of [...adjacency.keys()].toSorted((left, right) => left - right)) {\n if (!matchedBases.has(basePosition)) {\n tryAugment(basePosition, adjacency, baseOfHead, lockedHeads, new Set());\n }\n }\n\n for (const [headPosition, basePosition] of [...baseOfHead.entries()].toSorted((left, right) => left[0] - right[0])) {\n const base = baseGroup[basePosition] as IndexedFunction;\n const head = headGroup[headPosition] as IndexedFunction;\n (baseMatched[base.fileIndex] as boolean[])[base.functionIndex] = true;\n (headMatched[head.fileIndex] as boolean[])[head.functionIndex] = true;\n pairs.push({ base: base.fn, head: head.fn, headFileIndex: head.fileIndex });\n }\n}\n\n/**\n * Fills the adjacency lists and takes each best edge whose endpoints are both still free; a taken\n * exact (100%) edge locks its head against later rewiring.\n */\nfunction seedGreedyMatches(\n candidates: SimilarityCandidate[],\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>\n): Set<number> {\n const matchedBases = new Set<number>();\n for (const candidate of candidates) {\n const edges = adjacency.get(candidate.basePosition) ?? [];\n edges.push(candidate.headPosition);\n adjacency.set(candidate.basePosition, edges);\n if (!matchedBases.has(candidate.basePosition) && !baseOfHead.has(candidate.headPosition)) {\n baseOfHead.set(candidate.headPosition, candidate.basePosition);\n matchedBases.add(candidate.basePosition);\n if (candidate.similarity === 100) {\n lockedHeads.add(candidate.headPosition);\n }\n }\n }\n return matchedBases;\n}\n\n/** One Kuhn augmentation step: rewires existing (unlocked) matches only when that frees a head for `basePosition`. */\nfunction tryAugment(\n basePosition: number,\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>,\n visitedHeads: Set<number>\n): boolean {\n for (const headPosition of adjacency.get(basePosition) ?? []) {\n if (visitedHeads.has(headPosition) || lockedHeads.has(headPosition)) {\n continue;\n }\n visitedHeads.add(headPosition);\n const currentBase = baseOfHead.get(headPosition);\n if (currentBase === undefined || tryAugment(currentBase, adjacency, baseOfHead, lockedHeads, visitedHeads)) {\n baseOfHead.set(headPosition, basePosition);\n return true;\n }\n }\n return false;\n}\n\n/** Token-LCS similarity of two sequences relative to the longer one, in percent (0 when unusable). */\nfunction tokenSimilarityPercent(\n base: Int32Array | undefined,\n head: Int32Array | undefined,\n minPercent: number\n): number {\n if (!base || !head || base.length === 0 || head.length === 0) {\n return 0;\n }\n // The LCS cannot exceed the shorter sequence, so mismatched lengths are pruned for free.\n const maxLength = Math.max(base.length, head.length);\n if (Math.min(base.length, head.length) * 100 < minPercent * maxLength) {\n return 0;\n }\n return (lcsLength(base, head) * 100) / maxLength;\n}\n\nfunction checkRatchets(\n file: string,\n base: FunctionMetrics,\n head: FunctionMetrics,\n tolerances: GateTolerances\n): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const ratchet of ratchetMetrics) {\n const baseValue = ratchet.value(base);\n const headValue = ratchet.value(head);\n const allowedValue = baseValue + ratchet.allowance(tolerances, baseValue);\n if (headValue > allowedValue) {\n const name = head.name ?? '<anonymous>';\n violations.push({\n gate: 'function-regression',\n metric: ratchet.metric,\n file,\n functionName: name,\n startLine: head.startLine,\n endLine: head.endLine,\n baseValue,\n headValue,\n allowedValue,\n message:\n `${file}:${head.startLine}-${head.endLine} ${name}: ${ratchet.metric} worsened ` +\n `${ratchet.format(baseValue)} -> ${ratchet.format(headValue)} (allowed <= ${ratchet.format(allowedValue)}). ` +\n ratchet.remediation,\n });\n }\n }\n return violations;\n}\n\nfunction checkNewFunction(file: string, fn: FunctionMetrics, thresholds: NewFunctionThresholds): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const check of newFunctionMetrics) {\n const headValue = check.value(fn);\n const allowedValue = check.limit(thresholds);\n if (headValue > allowedValue) {\n const name = fn.name ?? '<anonymous>';\n violations.push({\n gate: 'new-function',\n metric: check.metric,\n file,\n functionName: name,\n startLine: fn.startLine,\n endLine: fn.endLine,\n headValue,\n allowedValue,\n message:\n `${file}:${fn.startLine}-${fn.endLine} new function ${name}: ${check.metric} ${headValue} ` +\n `exceeds the new-code limit ${allowedValue}. ${check.remediation}`,\n });\n }\n }\n return violations;\n}\n\n/**\n * Anti-gaming backstops: splitting a function resets its entity identity and could hide a\n * worsening behind the (laxer) new-code thresholds, so when a removed named function's content\n * reappears in unmatched new code (see hasSplitEvidence) the file-level aggregates ratchet too —\n * the max cognitive complexity per file and the file's total NCSS. Purely additive changes and\n * unrelated remove-plus-add changes stay ungated. The max-cognitive backstop is also skipped when\n * a function-level cognitive violation already reports the file's maximum, since it would only\n * restate it.\n */\nfunction checkFileBackstops(\n file: GateFileInput,\n splitEvidence: boolean,\n reportedViolations: GateViolation[],\n options: GateOptions\n): GateViolation[] {\n const { baseMetrics, headMetrics } = file;\n if (!baseMetrics || !headMetrics || !splitEvidence) {\n return [];\n }\n const violations: GateViolation[] = [];\n const tolerances = options.tolerance;\n\n const maximumAlreadyReported = reportedViolations.some(\n (violation) =>\n violation.file === file.file &&\n violation.metric === 'cognitive complexity' &&\n (violation.gate === 'function-regression' || violation.gate === 'new-function') &&\n violation.headValue === headMetrics.maxCognitiveComplexity\n );\n const allowedMaxCognitive = baseMetrics.maxCognitiveComplexity + tolerances.cognitiveComplexity;\n if (!maximumAlreadyReported && headMetrics.maxCognitiveComplexity > allowedMaxCognitive) {\n const worst = findMostComplexFunction(headMetrics.functions);\n const startLine = worst?.startLine ?? 1;\n const endLine = worst?.endLine ?? headMetrics.lines.total;\n violations.push({\n gate: 'file-regression',\n metric: 'file max cognitive complexity',\n file: file.file,\n functionName: worst?.name ?? undefined,\n startLine,\n endLine,\n baseValue: baseMetrics.maxCognitiveComplexity,\n headValue: headMetrics.maxCognitiveComplexity,\n allowedValue: allowedMaxCognitive,\n message:\n `${file.file}:${startLine}-${endLine}: the file's max cognitive complexity worsened ` +\n `${baseMetrics.maxCognitiveComplexity} -> ${headMetrics.maxCognitiveComplexity} ` +\n `(allowed <= ${allowedMaxCognitive}). Simplify or split the most complex ` +\n `function${worst ? ` (${worst.name ?? '<anonymous>'})` : ''}.`,\n });\n }\n\n const allowedFileNcss = baseMetrics.ncssCount + tolerances.fileNcss;\n if (headMetrics.ncssCount > allowedFileNcss) {\n violations.push({\n gate: 'file-regression',\n metric: 'file NCSS',\n file: file.file,\n startLine: 1,\n endLine: headMetrics.lines.total,\n baseValue: baseMetrics.ncssCount,\n headValue: headMetrics.ncssCount,\n allowedValue: allowedFileNcss,\n message:\n `${file.file}:1-${headMetrics.lines.total}: functions were removed or split while the file grew from NCSS ` +\n `${baseMetrics.ncssCount} to ${headMetrics.ncssCount} (allowed <= ${allowedFileNcss}). ` +\n `Remove the added statements or move genuinely new code into its own module.`,\n });\n }\n\n return violations;\n}\n\nfunction findMostComplexFunction(functions: FunctionMetrics[]): FunctionMetrics | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (!worst || fn.cognitiveComplexity > worst.cognitiveComplexity) {\n worst = fn;\n }\n }\n return worst;\n}\n\n/**\n * No new duplication: the distinct duplicated lines of a changed file (within-file plus\n * cross-file against the whole project, so copy-paste from existing code into new files is\n * caught) must not exceed the base revision's count.\n */\nfunction checkDuplication(file: GateFileInput, tolerances: GateTolerances): GateViolation[] {\n const allowedValue = file.baseDuplicatedLineCount + tolerances.duplicateLines;\n if (file.headDuplicatedLineCount <= allowedValue) {\n return [];\n }\n const partners = file.duplicationPartners.slice(0, 3).join(', ');\n const endLine = file.headMetrics?.lines.total ?? 1;\n return [\n {\n gate: 'duplication',\n metric: 'duplicated lines',\n file: file.file,\n startLine: 1,\n endLine,\n baseValue: file.baseDuplicatedLineCount,\n headValue: file.headDuplicatedLineCount,\n allowedValue,\n message:\n `${file.file}:1-${endLine}: duplicated lines increased ${file.baseDuplicatedLineCount} -> ` +\n `${file.headDuplicatedLineCount} (allowed <= ${allowedValue}). Deduplicate` +\n `${partners ? ` against ${partners}` : ' the repeated code'} by extracting a shared helper.`,\n },\n ];\n}\n"],"mappings":"kDAyCa,EAAkC,CAG7C,YAAa,CAAE,uBAAwB,GAAI,QAAS,GAAI,gBAAiB,CAAE,EAK3E,UAAW,CACT,oBAAqB,EACrB,KAAM,EACN,aAAc,EACd,UAAW,GACX,eAAgB,IAChB,SAAU,GACV,eAAgB,CAClB,EACA,uBAAwB,EAC1B,EA+EM,EAAgB,OAQhB,EAAkC,CACtC,CACE,OAAQ,uBACR,MAAQ,GAAO,EAAG,oBAClB,UAAY,GAAe,EAAW,oBACtC,YAAa,sDACb,OAAQ,CACV,EACA,CACE,OAAQ,OACR,MAAQ,GAAO,EAAG,KAClB,UAAY,GAAe,EAAW,KACtC,YAAa,qEACb,OAAQ,CACV,EACA,CACE,OAAQ,oBACR,MAAQ,GAAO,EAAG,aAClB,UAAY,GAAe,EAAW,aACtC,YAAa,qEACb,OAAQ,CACV,EACA,CACE,OAAQ,YACR,MAAQ,GAAO,EAAG,UAClB,UAAY,GAAe,EAAW,UACtC,YAAa,uFACb,OAAQ,CACV,EACA,CACE,OAAQ,kBACR,MAAQ,GAAO,EAAG,SAAS,OAI3B,WAAY,EAAY,IAAc,KAAK,IAAI,EAAW,eAAgB,EAAY,GAAI,EAC1F,YAAa,4DACb,OA5CiB,GAA0B,EAAM,QAAQ,CAAC,CA6C5D,CACF,EASM,EAA0C,CAC9C,CACE,OAAQ,uBACR,MAAQ,GAAO,EAAG,oBAClB,MAAQ,GAAe,EAAW,uBAClC,YAAa,mDACf,EACA,CACE,OAAQ,OACR,MAAQ,GAAO,EAAG,KAClB,MAAQ,GAAe,EAAW,QAClC,YAAa,2CACf,EACA,CACE,OAAQ,oBACR,MAAQ,GAAO,EAAG,aAClB,MAAQ,GAAe,EAAW,gBAClC,YAAa,mEACf,CACF,EAOA,SAAgB,EAAuB,EAAwB,EAAkC,CAC/F,IAAM,EAAW,EAAkB,EAAO,EAAQ,sBAAsB,EAClE,EAA8B,CAAC,EAC/B,EAA4C,CAAC,EAC/C,EAAmB,EACnB,EAAmB,EAEvB,IAAK,IAAM,KAAQ,EAAS,MAAO,CACjC,IAAM,EAAO,EAAM,EAAK,eACpB,EAAK,QAAU,KAGnB,GAAoB,EACpB,EAAW,KAAK,GAAG,EAAc,EAAK,KAAM,EAAK,KAAM,EAAK,KAAM,EAAQ,SAAS,CAAC,EACpF,EAAiB,KAAK,EAAiB,EAAK,KAAM,EAAK,KAAM,EAAK,IAAI,CAAC,EACzE,CAEA,IAAI,EAAiB,EACrB,IAAK,GAAM,CAAC,EAAW,KAAS,EAAM,QAAQ,EAAG,CAC/C,GAAI,EAAK,QAAU,GACjB,SAEF,GAAkB,EAClB,IAAM,EAAgB,EAAK,aAAa,WAAa,CAAC,EAChD,EAAc,EAAS,YAAY,GACzC,IAAK,GAAM,CAAC,EAAe,KAAO,EAAc,QAAQ,EAClD,EAAY,KAGhB,GAAoB,EACpB,EAAW,KAAK,GAAG,EAAiB,EAAK,KAAM,EAAI,EAAQ,WAAW,CAAC,EACvE,EAAiB,KAAK,EAAiB,EAAK,KAAM,CAAE,CAAC,GAEvD,EAAW,KAAK,GAAG,EAAmB,EAAM,EAAiB,EAAM,EAAW,CAAQ,EAAG,EAAY,CAAO,CAAC,EAC7G,EAAW,KAAK,GAAG,EAAiB,EAAM,EAAQ,SAAS,CAAC,CAC9D,CAIA,OAFA,EAAW,MAAM,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EACxG,EAAiB,MAAM,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,GAAK,EAAK,UAAY,EAAM,SAAS,EACvG,CACL,aACA,iBAAkB,EAClB,qBAAsB,EAAmB,EACzC,mBACA,kBACF,CACF,CAeA,SAAS,EAAiB,EAAqB,EAAmB,EAAqC,CACrG,IAAM,EAAgB,EAAK,aAAa,WAAa,CAAC,EAChD,EAAmB,EAAS,YAAY,GACxC,EAAmB,EAAS,YAAY,GACxC,EAAe,EAAc,SAAS,EAAI,IAC9C,CAAC,EAAiB,IAAU,EAAG,OAAS,IAAA,GAAY,CAAC,CAAK,EAAI,CAAC,CACjE,EACA,GAAI,EAAa,SAAW,EAC1B,MAAO,GAET,IAAM,GAAiB,EAAK,aAAa,WAAa,CAAC,EAAA,CAAG,SAAS,EAAG,IACpE,EAAiB,GAAS,CAAC,EAAI,CAAC,CAAK,CACvC,EAIA,OAHI,EAAa,OAAS,EAAc,OAAS,EACxC,GAEF,EAAa,KAAM,GACxB,EAAc,KACX,GACC,EACE,EAAK,qBAAqB,GAC1B,EAAK,qBAAqB,GAC1B,EACF,GAAK,EACT,CACF,CACF,CAEA,SAAS,EAAiB,EAAc,EAAuB,EAA+C,CAC5G,MAAO,CACL,OACA,KAAM,EAAK,MAAQ,cACnB,UAAW,EAAK,UAChB,QAAS,EAAK,QACd,KAAM,EAAO,EAAiB,CAAI,EAAI,IAAA,GACtC,KAAM,EAAiB,CAAI,CAC7B,CACF,CAEA,SAAS,EAAiB,EAAyC,CACjE,MAAO,CACL,oBAAqB,EAAG,oBACxB,KAAM,EAAG,KACT,aAAc,EAAG,aACjB,UAAW,EAAG,UACd,eAAgB,EAAG,SAAS,MAC9B,CACF,CAqCA,SAAS,EAAkB,EAAwB,EAAkD,CACnG,IAAM,EAAc,EAAM,IAAK,IAAU,EAAK,aAAa,WAAa,CAAC,EAAA,CAAG,QAAU,EAAK,CAAC,EACtF,EAAc,EAAM,IAAK,IAAU,EAAK,aAAa,WAAa,CAAC,EAAA,CAAG,QAAU,EAAK,CAAC,EACtF,EAAuB,CAAC,EAE9B,IAAK,IAAM,KAAa,EAAM,KAAK,EAAG,CACpC,IAAM,EAAa,GAA2D,CAC5E,IAAM,EAAY,EAAW,EAAiB,EAAO,EAAa,OAAQ,CAAS,EAAG,CAAG,EACnF,EAAY,EAAW,EAAiB,EAAO,EAAa,OAAQ,CAAS,EAAG,CAAG,EACzF,IAAK,GAAM,CAAC,EAAU,KAAc,EAClC,EAAU,EAAW,EAAU,IAAI,CAAQ,GAAK,CAAC,EAAG,EAAa,EAAa,CAAK,CAEvF,EACA,EAAW,GAAQ,EAAG,OAAS,IAAA,GAAY,IAAA,GAAY,GAAG,EAAG,KAAK,QAAQ,EAAG,gBAAiB,EAC9F,EAAW,GAAO,EAAG,IAAI,CAC3B,CAGA,OADA,EAAoB,EAAO,EAAa,EAAa,EAAO,CAAsB,EAC3E,CAAE,QAAO,cAAa,aAAY,CAC3C,CAGA,SAAS,EACP,EACA,EACA,EACA,EACmB,CACnB,IAAM,EAA+B,CAAC,EACtC,IAAK,GAAM,CAAC,EAAW,KAAS,EAAM,QAAQ,EAAG,CAC/C,GAAI,IAAkB,IAAA,IAAa,IAAc,EAC/C,SAEF,IAAM,EAAU,EAAc,GACxB,EAAU,IAAS,OAAS,EAAK,YAAc,EAAK,YACpD,EAAS,IAAS,OAAS,EAAK,mBAAqB,EAAK,mBAChE,IAAK,GAAM,CAAC,EAAe,KAAQ,GAAS,WAAa,CAAC,EAAA,CAAG,QAAQ,EAC9D,EAAQ,IACX,EAAU,KAAK,CAAE,YAAW,gBAAe,KAAI,OAAQ,IAAS,EAAe,CAAC,CAGtF,CACA,OAAO,CACT,CAEA,SAAS,EACP,EACA,EACgC,CAChC,IAAM,kBAAS,IAAI,IACnB,IAAK,IAAM,KAAW,EAAW,CAC/B,IAAM,EAAW,EAAI,EAAQ,EAAE,EAC/B,GAAI,IAAa,IAAA,GACf,SAEF,IAAM,EAAQ,EAAO,IAAI,CAAQ,GAAK,CAAC,EACvC,EAAM,KAAK,CAAO,EAClB,EAAO,IAAI,EAAU,CAAK,CAC5B,CACA,OAAO,CACT,CAGA,MAAM,EAA2B,IASjC,SAAS,EACP,EACA,EACA,EACA,EACA,EACM,CACN,IAAM,EACJ,EAAU,OAAS,EAAU,OAAS,GAAK,EAAU,OAAS,EAAU,QAAU,EAC9E,EAAoC,CAAC,EAC3C,IAAK,GAAM,CAAC,EAAc,KAAS,EAAU,QAAQ,EACnD,IAAK,GAAM,CAAC,EAAc,KAAS,EAAU,QAAQ,EACnD,EAAW,KAAK,CACd,WAAY,EAAY,EAAuB,EAAK,OAAQ,EAAK,OAAQ,CAAC,EAAI,EAC9E,eACA,cACF,CAAC,EAGL,EAAmB,EAAY,EAAW,EAAW,EAAa,EAAa,CAAK,CACtF,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACM,CACN,IAAM,EAAgB,EAAiB,EAAO,EAAa,MAAM,EAC3D,EAAgB,EAAiB,EAAO,EAAa,MAAM,EACjE,GAAI,EAAc,OAAS,EAAc,OAAS,EAChD,OAGF,IAAM,EAAoC,CAAC,EAC3C,IAAK,GAAM,CAAC,EAAc,KAAS,EAAc,QAAQ,EACvD,IAAK,GAAM,CAAC,EAAc,KAAS,EAAc,QAAQ,EAAG,CAC1D,IAAM,EAAa,EAAuB,EAAK,OAAQ,EAAK,OAAQ,CAAsB,EACtF,GAAc,GAChB,EAAW,KAAK,CAAE,aAAY,eAAc,cAAa,CAAC,CAE9D,CAEF,EAAmB,EAAY,EAAe,EAAe,EAAa,EAAa,CAAK,CAC9F,CAaA,SAAS,EACP,EACA,EACA,EACA,EACA,EACA,EACM,CACN,EAAW,MACR,EAAM,IACL,EAAM,WAAa,EAAK,YACxB,EAAK,aAAe,EAAM,cAC1B,EAAK,aAAe,EAAM,YAC9B,EACA,IAAM,kBAAY,IAAI,IAChB,kBAAa,IAAI,IACjB,kBAAc,IAAI,IAClB,EAAe,EAAkB,EAAY,EAAW,EAAY,CAAW,EACrF,IAAK,IAAM,IAAgB,CAAC,GAAG,EAAU,KAAK,CAAC,CAAC,CAAC,UAAU,EAAM,IAAU,EAAO,CAAK,EAChF,EAAa,IAAI,CAAY,GAChC,EAAW,EAAc,EAAW,EAAY,kBAAa,IAAI,GAAK,EAI1E,IAAK,GAAM,CAAC,EAAc,IAAiB,CAAC,GAAG,EAAW,QAAQ,CAAC,CAAC,CAAC,UAAU,EAAM,IAAU,EAAK,GAAK,EAAM,EAAE,EAAG,CAClH,IAAM,EAAO,EAAU,GACjB,EAAO,EAAU,GACvB,EAAa,EAAK,UAAU,CAAe,EAAK,eAAiB,GACjE,EAAa,EAAK,UAAU,CAAe,EAAK,eAAiB,GACjE,EAAM,KAAK,CAAE,KAAM,EAAK,GAAI,KAAM,EAAK,GAAI,cAAe,EAAK,SAAU,CAAC,CAC5E,CACF,CAMA,SAAS,EACP,EACA,EACA,EACA,EACa,CACb,IAAM,kBAAe,IAAI,IACzB,IAAK,IAAM,KAAa,EAAY,CAClC,IAAM,EAAQ,EAAU,IAAI,EAAU,YAAY,GAAK,CAAC,EACxD,EAAM,KAAK,EAAU,YAAY,EACjC,EAAU,IAAI,EAAU,aAAc,CAAK,EACvC,CAAC,EAAa,IAAI,EAAU,YAAY,GAAK,CAAC,EAAW,IAAI,EAAU,YAAY,IACrF,EAAW,IAAI,EAAU,aAAc,EAAU,YAAY,EAC7D,EAAa,IAAI,EAAU,YAAY,EACnC,EAAU,aAAe,KAC3B,EAAY,IAAI,EAAU,YAAY,EAG5C,CACA,OAAO,CACT,CAGA,SAAS,EACP,EACA,EACA,EACA,EACA,EACS,CACT,IAAK,IAAM,KAAgB,EAAU,IAAI,CAAY,GAAK,CAAC,EAAG,CAC5D,GAAI,EAAa,IAAI,CAAY,GAAK,EAAY,IAAI,CAAY,EAChE,SAEF,EAAa,IAAI,CAAY,EAC7B,IAAM,EAAc,EAAW,IAAI,CAAY,EAC/C,GAAI,IAAgB,IAAA,IAAa,EAAW,EAAa,EAAW,EAAY,EAAa,CAAY,EAEvG,OADA,EAAW,IAAI,EAAc,CAAY,EAClC,EAEX,CACA,MAAO,EACT,CAGA,SAAS,EACP,EACA,EACA,EACQ,CACR,GAAI,CAAC,GAAQ,CAAC,GAAQ,EAAK,SAAW,GAAK,EAAK,SAAW,EACzD,MAAO,GAGT,IAAM,EAAY,KAAK,IAAI,EAAK,OAAQ,EAAK,MAAM,EAInD,OAHI,KAAK,IAAI,EAAK,OAAQ,EAAK,MAAM,EAAI,IAAM,EAAa,EACnD,EAEDA,EAAAA,UAAU,EAAM,CAAI,EAAI,IAAO,CACzC,CAEA,SAAS,EACP,EACA,EACA,EACA,EACiB,CACjB,IAAM,EAA8B,CAAC,EACrC,IAAK,IAAM,KAAW,EAAgB,CACpC,IAAM,EAAY,EAAQ,MAAM,CAAI,EAC9B,EAAY,EAAQ,MAAM,CAAI,EAC9B,EAAe,EAAY,EAAQ,UAAU,EAAY,CAAS,EACxE,GAAI,EAAY,EAAc,CAC5B,IAAM,EAAO,EAAK,MAAQ,cAC1B,EAAW,KAAK,CACd,KAAM,sBACN,OAAQ,EAAQ,OAChB,OACA,aAAc,EACd,UAAW,EAAK,UAChB,QAAS,EAAK,QACd,YACA,YACA,eACA,QACE,GAAG,EAAK,GAAG,EAAK,UAAU,GAAG,EAAK,QAAQ,GAAG,EAAK,IAAI,EAAQ,OAAO,YAClE,EAAQ,OAAO,CAAS,EAAE,MAAM,EAAQ,OAAO,CAAS,EAAE,eAAe,EAAQ,OAAO,CAAY,EAAE,KACzG,EAAQ,WACZ,CAAC,CACH,CACF,CACA,OAAO,CACT,CAEA,SAAS,EAAiB,EAAc,EAAqB,EAAoD,CAC/G,IAAM,EAA8B,CAAC,EACrC,IAAK,IAAM,KAAS,EAAoB,CACtC,IAAM,EAAY,EAAM,MAAM,CAAE,EAC1B,EAAe,EAAM,MAAM,CAAU,EAC3C,GAAI,EAAY,EAAc,CAC5B,IAAM,EAAO,EAAG,MAAQ,cACxB,EAAW,KAAK,CACd,KAAM,eACN,OAAQ,EAAM,OACd,OACA,aAAc,EACd,UAAW,EAAG,UACd,QAAS,EAAG,QACZ,YACA,eACA,QACE,GAAG,EAAK,GAAG,EAAG,UAAU,GAAG,EAAG,QAAQ,gBAAgB,EAAK,IAAI,EAAM,OAAO,GAAG,EAAU,8BAC3D,EAAa,IAAI,EAAM,aACzD,CAAC,CACH,CACF,CACA,OAAO,CACT,CAWA,SAAS,EACP,EACA,EACA,EACA,EACiB,CACjB,GAAM,CAAE,cAAa,eAAgB,EACrC,GAAI,CAAC,GAAe,CAAC,GAAe,CAAC,EACnC,MAAO,CAAC,EAEV,IAAM,EAA8B,CAAC,EAC/B,EAAa,EAAQ,UAErB,EAAyB,EAAmB,KAC/C,GACC,EAAU,OAAS,EAAK,MACxB,EAAU,SAAW,yBACpB,EAAU,OAAS,uBAAyB,EAAU,OAAS,iBAChE,EAAU,YAAc,EAAY,sBACxC,EACM,EAAsB,EAAY,uBAAyB,EAAW,oBAC5E,GAAI,CAAC,GAA0B,EAAY,uBAAyB,EAAqB,CACvF,IAAM,EAAQ,EAAwB,EAAY,SAAS,EACrD,EAAY,GAAO,WAAa,EAChC,EAAU,GAAO,SAAW,EAAY,MAAM,MACpD,EAAW,KAAK,CACd,KAAM,kBACN,OAAQ,gCACR,KAAM,EAAK,KACX,aAAc,GAAO,MAAQ,IAAA,GAC7B,YACA,UACA,UAAW,EAAY,uBACvB,UAAW,EAAY,uBACvB,aAAc,EACd,QACE,GAAG,EAAK,KAAK,GAAG,EAAU,GAAG,EAAQ,iDAClC,EAAY,uBAAuB,MAAM,EAAY,uBAAuB,eAChE,EAAoB,gDACxB,EAAQ,KAAK,EAAM,MAAQ,cAAc,GAAK,GAAG,EAChE,CAAC,CACH,CAEA,IAAM,EAAkB,EAAY,UAAY,EAAW,SAkB3D,OAjBI,EAAY,UAAY,GAC1B,EAAW,KAAK,CACd,KAAM,kBACN,OAAQ,YACR,KAAM,EAAK,KACX,UAAW,EACX,QAAS,EAAY,MAAM,MAC3B,UAAW,EAAY,UACvB,UAAW,EAAY,UACvB,aAAc,EACd,QACE,GAAG,EAAK,KAAK,KAAK,EAAY,MAAM,MAAM,kEACvC,EAAY,UAAU,MAAM,EAAY,UAAU,eAAe,EAAgB,+EAExF,CAAC,EAGI,CACT,CAEA,SAAS,EAAwB,EAA2D,CAC1F,IAAI,EACJ,IAAK,IAAM,KAAM,GACX,CAAC,GAAS,EAAG,oBAAsB,EAAM,uBAC3C,EAAQ,GAGZ,OAAO,CACT,CAOA,SAAS,EAAiB,EAAqB,EAA6C,CAC1F,IAAM,EAAe,EAAK,wBAA0B,EAAW,eAC/D,GAAI,EAAK,yBAA2B,EAClC,MAAO,CAAC,EAEV,IAAM,EAAW,EAAK,oBAAoB,MAAM,EAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EACzD,EAAU,EAAK,aAAa,MAAM,OAAS,EACjD,MAAO,CACL,CACE,KAAM,cACN,OAAQ,mBACR,KAAM,EAAK,KACX,UAAW,EACX,UACA,UAAW,EAAK,wBAChB,UAAW,EAAK,wBAChB,eACA,QACE,GAAG,EAAK,KAAK,KAAK,EAAQ,+BAA+B,EAAK,wBAAwB,MACnF,EAAK,wBAAwB,eAAe,EAAa,gBACzD,EAAW,YAAY,IAAa,qBAAqB,gCAChE,CACF,CACF"}
|
|
1
|
+
{"version":3,"file":"regressionGate.cjs","names":["lcsLength"],"sources":["../src/regressionGate.ts"],"sourcesContent":["import { lcsLength } from './duplication.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/**\n * Absolute limits applied only to functions with no base counterpart (SonarQube-style \"Clean as\n * You Code\"): new code is the one place absolute thresholds are required, since there is nothing\n * to ratchet against.\n */\nexport interface NewFunctionThresholds {\n maxCognitiveComplexity: number;\n maxNcss: number;\n maxNestingDepth: number;\n}\n\n/**\n * Allowances added on top of each base value before a ratchet fires, so legitimate refactorings\n * that transiently worsen one number (e.g. an extraction adding a few NCSS lines) don't trap the\n * agent in a loop.\n */\nexport interface GateTolerances {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n /** File-total NCSS growth allowed when base functions disappeared (the anti-gaming backstop). */\n fileNcss: number;\n /** Newly duplicated changed lines allowed per changed file. */\n duplicateLines: number;\n}\n\nexport interface GateOptions {\n newFunction: NewFunctionThresholds;\n tolerance: GateTolerances;\n /**\n * Minimum token-LCS similarity percent for re-matching a renamed/moved function to its base\n * counterpart, so renames don't appear as delete+add and hit the new-code thresholds.\n */\n matchSimilarityPercent: number;\n}\n\nexport const defaultGateOptions: GateOptions = {\n // 15 is SonarSource's conventional default; 4 is where the measured cognitive-load response to\n // nesting saturates (Peitek et al. 2021); 60 matches PMD's NcssCount method default.\n newFunction: { maxCognitiveComplexity: 15, maxNcss: 60, maxNestingDepth: 4 },\n // The per-metric allowances are calibrated to admit roughly the same edit: ~5 added statements\n // or one added branch (cognitive 2 ~ nesting 1 ~ ncss 5 ~ depDegree 10 ~ Halstead volume 150;\n // the volume allowance additionally scales with the base value, see ratchetMetrics), so one\n // ratchet cannot block an addition the others deliberately allow.\n tolerance: {\n cognitiveComplexity: 2,\n ncss: 5,\n nestingDepth: 1,\n depDegree: 10,\n halsteadVolume: 150,\n fileNcss: 20,\n duplicateLines: 0,\n },\n matchSimilarityPercent: 70,\n};\n\n/** One changed file with both revisions measured; the caller resolves git and duplication data. */\nexport interface GateFileInput {\n /** Display path (repository-relative head path; base path for deleted files). */\n file: string;\n /** Absent for added files. */\n baseMetrics?: CodeMetrics;\n /** Absent for deleted files. */\n headMetrics?: CodeMetrics;\n /** Token sequences index-parallel to baseMetrics.functions, for similarity re-matching. */\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n /**\n * Head lines the change newly made duplicated (within-file or cross-file), ascending; the\n * caller derives them from the line diff (see findNewlyDuplicatedLines).\n */\n newlyDuplicatedLines: number[];\n /** Other files sharing the clones behind newlyDuplicatedLines, as remediation evidence. */\n duplicationPartners: string[];\n /**\n * false: the file only feeds function matching and the duplication universes (it lies outside\n * the gated target directory); no violations are evaluated or reported for it. Default true.\n */\n gated?: boolean;\n}\n\nexport interface GateViolation {\n gate: 'function-regression' | 'new-function' | 'file-regression' | 'duplication';\n metric: string;\n file: string;\n functionName?: string;\n startLine: number;\n endLine: number;\n /** Absent for new-function and duplication violations, which have no base value. */\n baseValue?: number;\n headValue: number;\n /** The largest value that would have passed. */\n allowedValue: number;\n /** One-line human/agent-readable statement with the remediation direction. */\n message: string;\n}\n\n/** The gated per-function values of one checked (matched or new) function, for `--full` output. */\nexport interface CheckedFunctionReport {\n /** The head file (a cross-file move is reported where the function now lives). */\n file: string;\n name: string;\n startLine: number;\n endLine: number;\n /** Absent for new functions. */\n base?: GateFunctionValues;\n head: GateFunctionValues;\n}\n\nexport interface GateFunctionValues {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n}\n\nexport interface GateResult {\n violations: GateViolation[];\n checkedFileCount: number;\n checkedFunctionCount: number;\n newFunctionCount: number;\n /** Every checked function with its gated values, ordered by file and line. */\n checkedFunctions: CheckedFunctionReport[];\n}\n\ninterface RatchetMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n /** The growth allowed on top of the base value before the ratchet fires. */\n allowance: (tolerances: GateTolerances, baseValue: number) => number;\n remediation: string;\n format: (value: number) => string;\n}\n\nconst formatInteger = String;\nconst formatFloat = (value: number): string => value.toFixed(1);\n\n/**\n * Gate-eligible per-function metrics (monotone, stable, actionable): a matched function must not\n * worsen any of them beyond its tolerance. \"Your change made this function worse\" is defensible\n * regardless of metric calibration, so no absolute threshold is involved.\n */\nconst ratchetMetrics: RatchetMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n allowance: (tolerances) => tolerances.cognitiveComplexity,\n remediation: 'Simplify the added branching or split the function.',\n format: formatInteger,\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n allowance: (tolerances) => tolerances.ncss,\n remediation: 'Extract the added statements into helper functions or remove them.',\n format: formatInteger,\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n allowance: (tolerances) => tolerances.nestingDepth,\n remediation: 'Flatten the added nesting with early returns or extracted helpers.',\n format: formatInteger,\n },\n {\n metric: 'DepDegree',\n value: (fn) => fn.depDegree,\n allowance: (tolerances) => tolerances.depDegree,\n remediation: 'Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.',\n format: formatInteger,\n },\n {\n metric: 'Halstead volume',\n value: (fn) => fn.halstead.volume,\n // Volume grows as length * log2(vocabulary), so a flat allowance that admits ~5 statements in\n // a small function admits barely one in a large-vocabulary function; the relative term keeps\n // the ~5-statement calibration at every function size.\n allowance: (tolerances, baseValue) => Math.max(tolerances.halsteadVolume, baseValue * 0.25),\n remediation: 'Shrink the function: fewer distinct names and operations.',\n format: formatFloat,\n },\n];\n\ninterface NewFunctionMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n limit: (thresholds: NewFunctionThresholds) => number;\n remediation: string;\n}\n\nconst newFunctionMetrics: NewFunctionMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n limit: (thresholds) => thresholds.maxCognitiveComplexity,\n remediation: 'Split the new function or simplify its branching.',\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n limit: (thresholds) => thresholds.maxNcss,\n remediation: 'Split the new function into smaller ones.',\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n limit: (thresholds) => thresholds.maxNestingDepth,\n remediation: 'Flatten the new function with early returns or extracted helpers.',\n },\n];\n\n/**\n * Evaluates the regression gate over changed files measured at both revisions. Violations are the\n * whole output contract: everything that passes stays silent, so a clean change costs the\n * consuming agent almost no tokens.\n */\nexport function evaluateRegressionGate(files: GateFileInput[], options: GateOptions): GateResult {\n const matching = matchAllFunctions(files, options.matchSimilarityPercent);\n const violations: GateViolation[] = [];\n const checkedFunctions: CheckedFunctionReport[] = [];\n let checkedPairCount = 0;\n let newFunctionCount = 0;\n\n for (const pair of matching.pairs) {\n const file = files[pair.headFileIndex] as GateFileInput;\n if (file.gated === false) {\n continue;\n }\n checkedPairCount += 1;\n violations.push(...checkRatchets(file.file, pair.base, pair.head, options.tolerance));\n checkedFunctions.push(toFunctionReport(file.file, pair.head, pair.base));\n }\n\n let gatedFileCount = 0;\n for (const [fileIndex, file] of files.entries()) {\n if (file.gated === false) {\n continue;\n }\n gatedFileCount += 1;\n const headFunctions = file.headMetrics?.functions ?? [];\n const headMatched = matching.headMatched[fileIndex] as boolean[];\n for (const [functionIndex, fn] of headFunctions.entries()) {\n if (headMatched[functionIndex]) {\n continue;\n }\n newFunctionCount += 1;\n violations.push(...checkNewFunction(file.file, fn, options.newFunction));\n checkedFunctions.push(toFunctionReport(file.file, fn));\n }\n violations.push(...checkFileBackstops(file, hasSplitEvidence(file, fileIndex, matching), violations, options));\n violations.push(...checkDuplication(file, options.tolerance));\n }\n\n violations.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n checkedFunctions.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n return {\n violations,\n checkedFileCount: gatedFileCount,\n checkedFunctionCount: checkedPairCount + newFunctionCount,\n newFunctionCount,\n checkedFunctions,\n };\n}\n\n/**\n * Similarity below the match threshold but above this marks a removed function's content as\n * reappearing inside unmatched new code — the signature of a split or evasive rewrite.\n */\nconst splitEvidenceSimilarityPercent = 30;\n\n/**\n * Whether a removed NAMED base function's content partially reappears in an unmatched head\n * function of the same file. This is what arms the file-level backstops: splitting a function to\n * hide a worsening behind the (laxer) new-code thresholds leaves its fragments partially similar\n * to the removed original, while an ordinary \"delete a helper, add an unrelated function\" change\n * shares nothing and must not have its brand-new code re-judged against the file's old aggregates.\n */\nfunction hasSplitEvidence(file: GateFileInput, fileIndex: number, matching: FunctionMatching): boolean {\n const baseFunctions = file.baseMetrics?.functions ?? [];\n const baseMatchedFlags = matching.baseMatched[fileIndex] as boolean[];\n const headMatchedFlags = matching.headMatched[fileIndex] as boolean[];\n const removedNamed = baseFunctions.flatMap((fn, index) =>\n !baseMatchedFlags[index] && fn.name !== undefined ? [index] : []\n );\n if (removedNamed.length === 0) {\n return false;\n }\n const unmatchedHead = (file.headMetrics?.functions ?? []).flatMap((_, index) =>\n headMatchedFlags[index] ? [] : [index]\n );\n if (removedNamed.length * unmatchedHead.length > maxSimilarityComparisons) {\n return false;\n }\n return removedNamed.some((baseIndex) =>\n unmatchedHead.some(\n (headIndex) =>\n tokenSimilarityPercent(\n file.baseFunctionTokens?.[baseIndex],\n file.headFunctionTokens?.[headIndex],\n splitEvidenceSimilarityPercent\n ) >= splitEvidenceSimilarityPercent\n )\n );\n}\n\nfunction toFunctionReport(file: string, head: FunctionMetrics, base?: FunctionMetrics): CheckedFunctionReport {\n return {\n file,\n name: head.name ?? '<anonymous>',\n startLine: head.startLine,\n endLine: head.endLine,\n base: base ? toFunctionValues(base) : undefined,\n head: toFunctionValues(head),\n };\n}\n\nfunction toFunctionValues(fn: FunctionMetrics): GateFunctionValues {\n return {\n cognitiveComplexity: fn.cognitiveComplexity,\n ncss: fn.ncss,\n nestingDepth: fn.nestingDepth,\n depDegree: fn.depDegree,\n halsteadVolume: fn.halstead.volume,\n };\n}\n\n/** One side of a match candidate: a function of one changed file plus its token sequence. */\ninterface IndexedFunction {\n fileIndex: number;\n functionIndex: number;\n fn: FunctionMetrics;\n tokens: Int32Array | undefined;\n}\n\ninterface MatchedPair {\n base: FunctionMetrics;\n head: FunctionMetrics;\n headFileIndex: number;\n}\n\ninterface FunctionMatching {\n pairs: MatchedPair[];\n /** Per file, per function index: whether it found a counterpart (anywhere in the change set). */\n baseMatched: boolean[][];\n headMatched: boolean[][];\n}\n\ninterface SimilarityCandidate {\n similarity: number;\n basePosition: number;\n headPosition: number;\n}\n\n/**\n * Matches head functions to base functions: within each file first by name + arity, then by name\n * alone (signature changes) — resolving ambiguous same-name groups by token similarity instead of\n * list position, so an inserted overload cannot shift the pairing — and finally by\n * normalized-token LCS similarity across ALL changed files (renames and moves between files,\n * reusing the near-miss clone machinery), so refactorings don't appear as delete+add and hit the\n * new-code thresholds.\n */\nfunction matchAllFunctions(files: GateFileInput[], matchSimilarityPercent: number): FunctionMatching {\n const baseMatched = files.map((file) => (file.baseMetrics?.functions ?? []).map(() => false));\n const headMatched = files.map((file) => (file.headMetrics?.functions ?? []).map(() => false));\n const pairs: MatchedPair[] = [];\n\n for (const fileIndex of files.keys()) {\n const pairByKey = (key: (fn: FunctionMetrics) => string | undefined): void => {\n const baseByKey = groupByKey(collectUnmatched(files, baseMatched, 'base', fileIndex), key);\n const headByKey = groupByKey(collectUnmatched(files, headMatched, 'head', fileIndex), key);\n for (const [groupKey, baseGroup] of baseByKey) {\n pairGroup(baseGroup, headByKey.get(groupKey) ?? [], baseMatched, headMatched, pairs);\n }\n };\n pairByKey((fn) => (fn.name === undefined ? undefined : `${fn.name}\\u0000${fn.parameterCount}`));\n pairByKey((fn) => fn.name);\n }\n\n pairAcrossChangeSet(files, baseMatched, headMatched, pairs, matchSimilarityPercent);\n return { pairs, baseMatched, headMatched };\n}\n\n/** Unmatched functions of one file (or of every file when onlyFileIndex is undefined). */\nfunction collectUnmatched(\n files: GateFileInput[],\n matchedByFile: boolean[][],\n side: 'base' | 'head',\n onlyFileIndex?: number\n): IndexedFunction[] {\n const unmatched: IndexedFunction[] = [];\n for (const [fileIndex, file] of files.entries()) {\n if (onlyFileIndex !== undefined && fileIndex !== onlyFileIndex) {\n continue;\n }\n const matched = matchedByFile[fileIndex] as boolean[];\n const metrics = side === 'base' ? file.baseMetrics : file.headMetrics;\n const tokens = side === 'base' ? file.baseFunctionTokens : file.headFunctionTokens;\n for (const [functionIndex, fn] of (metrics?.functions ?? []).entries()) {\n if (!matched[functionIndex]) {\n unmatched.push({ fileIndex, functionIndex, fn, tokens: tokens?.[functionIndex] });\n }\n }\n }\n return unmatched;\n}\n\nfunction groupByKey(\n functions: IndexedFunction[],\n key: (fn: FunctionMetrics) => string | undefined\n): Map<string, IndexedFunction[]> {\n const groups = new Map<string, IndexedFunction[]>();\n for (const indexed of functions) {\n const groupKey = key(indexed.fn);\n if (groupKey === undefined) {\n continue;\n }\n const group = groups.get(groupKey) ?? [];\n group.push(indexed);\n groups.set(groupKey, group);\n }\n return groups;\n}\n\n/** Bounds the quadratic similarity passes; beyond this, leftovers gate as new/removed functions. */\nconst maxSimilarityComparisons = 10_000;\n\n/**\n * Pairs one key group. An unambiguous 1x1 group matches directly; a larger group (same-name\n * methods of different classes, overload sets) is assigned best-similarity-first so an inserted\n * function cannot shift the pairing of the others. The names already match, so any pairing is\n * acceptable: similarity only ORDERS the assignment (no threshold), and without token sequences\n * (or in an oversized group) the stable tie-break reproduces positional order.\n */\nfunction pairGroup(\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n const ambiguous =\n baseGroup.length + headGroup.length > 2 && baseGroup.length * headGroup.length <= maxSimilarityComparisons;\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of baseGroup.entries()) {\n for (const [headPosition, head] of headGroup.entries()) {\n candidates.push({\n similarity: ambiguous ? tokenSimilarityPercent(base.tokens, head.tokens, 0) : 0,\n basePosition,\n headPosition,\n });\n }\n }\n takeMaximumMatches(candidates, baseGroup, headGroup, baseMatched, headMatched, pairs);\n}\n\n/** Cross-file (and remaining within-file) similarity matching over every unmatched function. */\nfunction pairAcrossChangeSet(\n files: GateFileInput[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[],\n matchSimilarityPercent: number\n): void {\n const unmatchedBase = collectUnmatched(files, baseMatched, 'base');\n const unmatchedHead = collectUnmatched(files, headMatched, 'head');\n if (unmatchedBase.length * unmatchedHead.length > maxSimilarityComparisons) {\n return;\n }\n\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of unmatchedBase.entries()) {\n for (const [headPosition, head] of unmatchedHead.entries()) {\n const similarity = tokenSimilarityPercent(base.tokens, head.tokens, matchSimilarityPercent);\n if (similarity >= matchSimilarityPercent) {\n candidates.push({ similarity, basePosition, headPosition });\n }\n }\n }\n takeMaximumMatches(candidates, unmatchedBase, unmatchedHead, baseMatched, headMatched, pairs);\n}\n\n/**\n * Assignment over the candidate edges: a greedy best-similarity-first seed, then Kuhn's\n * augmenting paths for the bases the seed left unmatched, so a function whose only counterpart\n * was taken by a slightly better pair is re-matched by rewiring instead of being falsely gated as\n * new code. Augmentation runs only when it increases cardinality, and an exact (100%) seed pair —\n * certain identity — is locked and never rewired even then: trading it for two partial matches\n * would ratchet an untouched function against some other function's rewrite. Non-exact seed pairs\n * may be rewired, but only when that matches strictly more functions (which a maximum-weight\n * assignment would also prefer). Without token sequences every similarity ties at 0 and the seed\n * reproduces positional order.\n */\nfunction takeMaximumMatches(\n candidates: SimilarityCandidate[],\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n candidates.sort(\n (left, right) =>\n right.similarity - left.similarity ||\n left.basePosition - right.basePosition ||\n left.headPosition - right.headPosition\n );\n const adjacency = new Map<number, number[]>();\n const baseOfHead = new Map<number, number>();\n const lockedHeads = new Set<number>();\n const matchedBases = seedGreedyMatches(candidates, adjacency, baseOfHead, lockedHeads);\n for (const basePosition of [...adjacency.keys()].toSorted((left, right) => left - right)) {\n if (!matchedBases.has(basePosition)) {\n tryAugment(basePosition, adjacency, baseOfHead, lockedHeads, new Set());\n }\n }\n\n for (const [headPosition, basePosition] of [...baseOfHead.entries()].toSorted((left, right) => left[0] - right[0])) {\n const base = baseGroup[basePosition] as IndexedFunction;\n const head = headGroup[headPosition] as IndexedFunction;\n (baseMatched[base.fileIndex] as boolean[])[base.functionIndex] = true;\n (headMatched[head.fileIndex] as boolean[])[head.functionIndex] = true;\n pairs.push({ base: base.fn, head: head.fn, headFileIndex: head.fileIndex });\n }\n}\n\n/**\n * Fills the adjacency lists and takes each best edge whose endpoints are both still free; a taken\n * exact (100%) edge locks its head against later rewiring.\n */\nfunction seedGreedyMatches(\n candidates: SimilarityCandidate[],\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>\n): Set<number> {\n const matchedBases = new Set<number>();\n for (const candidate of candidates) {\n const edges = adjacency.get(candidate.basePosition) ?? [];\n edges.push(candidate.headPosition);\n adjacency.set(candidate.basePosition, edges);\n if (!matchedBases.has(candidate.basePosition) && !baseOfHead.has(candidate.headPosition)) {\n baseOfHead.set(candidate.headPosition, candidate.basePosition);\n matchedBases.add(candidate.basePosition);\n if (candidate.similarity === 100) {\n lockedHeads.add(candidate.headPosition);\n }\n }\n }\n return matchedBases;\n}\n\n/** One Kuhn augmentation step: rewires existing (unlocked) matches only when that frees a head for `basePosition`. */\nfunction tryAugment(\n basePosition: number,\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>,\n visitedHeads: Set<number>\n): boolean {\n for (const headPosition of adjacency.get(basePosition) ?? []) {\n if (visitedHeads.has(headPosition) || lockedHeads.has(headPosition)) {\n continue;\n }\n visitedHeads.add(headPosition);\n const currentBase = baseOfHead.get(headPosition);\n if (currentBase === undefined || tryAugment(currentBase, adjacency, baseOfHead, lockedHeads, visitedHeads)) {\n baseOfHead.set(headPosition, basePosition);\n return true;\n }\n }\n return false;\n}\n\n/** Token-LCS similarity of two sequences relative to the longer one, in percent (0 when unusable). */\nfunction tokenSimilarityPercent(\n base: Int32Array | undefined,\n head: Int32Array | undefined,\n minPercent: number\n): number {\n if (!base || !head || base.length === 0 || head.length === 0) {\n return 0;\n }\n // The LCS cannot exceed the shorter sequence, so mismatched lengths are pruned for free.\n const maxLength = Math.max(base.length, head.length);\n if (Math.min(base.length, head.length) * 100 < minPercent * maxLength) {\n return 0;\n }\n return (lcsLength(base, head) * 100) / maxLength;\n}\n\nfunction checkRatchets(\n file: string,\n base: FunctionMetrics,\n head: FunctionMetrics,\n tolerances: GateTolerances\n): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const ratchet of ratchetMetrics) {\n const baseValue = ratchet.value(base);\n const headValue = ratchet.value(head);\n const allowedValue = baseValue + ratchet.allowance(tolerances, baseValue);\n if (headValue > allowedValue) {\n const name = head.name ?? '<anonymous>';\n violations.push({\n gate: 'function-regression',\n metric: ratchet.metric,\n file,\n functionName: name,\n startLine: head.startLine,\n endLine: head.endLine,\n baseValue,\n headValue,\n allowedValue,\n message:\n `${file}:${head.startLine}-${head.endLine} ${name}: ${ratchet.metric} worsened ` +\n `${ratchet.format(baseValue)} -> ${ratchet.format(headValue)} (allowed <= ${ratchet.format(allowedValue)}). ` +\n ratchet.remediation,\n });\n }\n }\n return violations;\n}\n\nfunction checkNewFunction(file: string, fn: FunctionMetrics, thresholds: NewFunctionThresholds): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const check of newFunctionMetrics) {\n const headValue = check.value(fn);\n const allowedValue = check.limit(thresholds);\n if (headValue > allowedValue) {\n const name = fn.name ?? '<anonymous>';\n violations.push({\n gate: 'new-function',\n metric: check.metric,\n file,\n functionName: name,\n startLine: fn.startLine,\n endLine: fn.endLine,\n headValue,\n allowedValue,\n message:\n `${file}:${fn.startLine}-${fn.endLine} new function ${name}: ${check.metric} ${headValue} ` +\n `exceeds the new-code limit ${allowedValue}. ${check.remediation}`,\n });\n }\n }\n return violations;\n}\n\n/**\n * Anti-gaming backstops: splitting a function resets its entity identity and could hide a\n * worsening behind the (laxer) new-code thresholds, so when a removed named function's content\n * reappears in unmatched new code (see hasSplitEvidence) the file-level aggregates ratchet too —\n * the max cognitive complexity per file and the file's total NCSS. Purely additive changes and\n * unrelated remove-plus-add changes stay ungated. The max-cognitive backstop is also skipped when\n * a function-level cognitive violation already reports the file's maximum, since it would only\n * restate it.\n */\nfunction checkFileBackstops(\n file: GateFileInput,\n splitEvidence: boolean,\n reportedViolations: GateViolation[],\n options: GateOptions\n): GateViolation[] {\n const { baseMetrics, headMetrics } = file;\n if (!baseMetrics || !headMetrics || !splitEvidence) {\n return [];\n }\n const violations: GateViolation[] = [];\n const tolerances = options.tolerance;\n\n const maximumAlreadyReported = reportedViolations.some(\n (violation) =>\n violation.file === file.file &&\n violation.metric === 'cognitive complexity' &&\n (violation.gate === 'function-regression' || violation.gate === 'new-function') &&\n violation.headValue === headMetrics.maxCognitiveComplexity\n );\n const allowedMaxCognitive = baseMetrics.maxCognitiveComplexity + tolerances.cognitiveComplexity;\n if (!maximumAlreadyReported && headMetrics.maxCognitiveComplexity > allowedMaxCognitive) {\n const worst = findMostComplexFunction(headMetrics.functions);\n const startLine = worst?.startLine ?? 1;\n const endLine = worst?.endLine ?? headMetrics.lines.total;\n violations.push({\n gate: 'file-regression',\n metric: 'file max cognitive complexity',\n file: file.file,\n functionName: worst?.name ?? undefined,\n startLine,\n endLine,\n baseValue: baseMetrics.maxCognitiveComplexity,\n headValue: headMetrics.maxCognitiveComplexity,\n allowedValue: allowedMaxCognitive,\n message:\n `${file.file}:${startLine}-${endLine}: the file's max cognitive complexity worsened ` +\n `${baseMetrics.maxCognitiveComplexity} -> ${headMetrics.maxCognitiveComplexity} ` +\n `(allowed <= ${allowedMaxCognitive}). Simplify or split the most complex ` +\n `function${worst ? ` (${worst.name ?? '<anonymous>'})` : ''}.`,\n });\n }\n\n const allowedFileNcss = baseMetrics.ncssCount + tolerances.fileNcss;\n if (headMetrics.ncssCount > allowedFileNcss) {\n violations.push({\n gate: 'file-regression',\n metric: 'file NCSS',\n file: file.file,\n startLine: 1,\n endLine: headMetrics.lines.total,\n baseValue: baseMetrics.ncssCount,\n headValue: headMetrics.ncssCount,\n allowedValue: allowedFileNcss,\n message:\n `${file.file}:1-${headMetrics.lines.total}: functions were removed or split while the file grew from NCSS ` +\n `${baseMetrics.ncssCount} to ${headMetrics.ncssCount} (allowed <= ${allowedFileNcss}). ` +\n `Remove the added statements or move genuinely new code into its own module.`,\n });\n }\n\n return violations;\n}\n\nfunction findMostComplexFunction(functions: FunctionMetrics[]): FunctionMetrics | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (!worst || fn.cognitiveComplexity > worst.cognitiveComplexity) {\n worst = fn;\n }\n }\n return worst;\n}\n\n/**\n * No new duplication: the changed lines of a file must not newly duplicate code (within-file, or\n * cross-file against the whole project, so copy-paste from unchanged code is caught where it was\n * pasted).\n */\nfunction checkDuplication(file: GateFileInput, tolerances: GateTolerances): GateViolation[] {\n const lines = file.newlyDuplicatedLines;\n const allowedValue = tolerances.duplicateLines;\n if (lines.length <= allowedValue) {\n return [];\n }\n const startLine = lines[0] as number;\n const endLine = lines.at(-1) as number;\n const partners = file.duplicationPartners.slice(0, 3).join(', ');\n return [\n {\n gate: 'duplication',\n metric: 'newly duplicated lines',\n file: file.file,\n startLine,\n endLine,\n headValue: lines.length,\n allowedValue,\n message:\n `${file.file}:${startLine}-${endLine}: ${lines.length} changed lines duplicate existing code ` +\n `(lines ${formatLineRanges(lines)}; allowed <= ${allowedValue}). Deduplicate` +\n `${partners ? ` against ${partners}` : ' the repeated code'} by extracting a shared helper.`,\n },\n ];\n}\n\n/** Ascending line numbers as compact ranges, e.g. `3-5, 9`. */\nfunction formatLineRanges(lines: number[]): string {\n const ranges: string[] = [];\n let start = lines[0] as number;\n for (const [index, line] of lines.entries()) {\n const next = lines[index + 1];\n if (next !== line + 1) {\n ranges.push(start === line ? String(line) : `${start}-${line}`);\n start = next as number;\n }\n }\n return ranges.join(', ');\n}\n"],"mappings":"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"}
|
package/dist/regressionGate.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface GateTolerances {
|
|
|
22
22
|
halsteadVolume: number;
|
|
23
23
|
/** File-total NCSS growth allowed when base functions disappeared (the anti-gaming backstop). */
|
|
24
24
|
fileNcss: number;
|
|
25
|
-
/**
|
|
25
|
+
/** Newly duplicated changed lines allowed per changed file. */
|
|
26
26
|
duplicateLines: number;
|
|
27
27
|
}
|
|
28
28
|
export interface GateOptions {
|
|
@@ -46,10 +46,12 @@ export interface GateFileInput {
|
|
|
46
46
|
/** Token sequences index-parallel to baseMetrics.functions, for similarity re-matching. */
|
|
47
47
|
baseFunctionTokens?: Int32Array[];
|
|
48
48
|
headFunctionTokens?: Int32Array[];
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Head lines the change newly made duplicated (within-file or cross-file), ascending; the
|
|
51
|
+
* caller derives them from the line diff (see findNewlyDuplicatedLines).
|
|
52
|
+
*/
|
|
53
|
+
newlyDuplicatedLines: number[];
|
|
54
|
+
/** Other files sharing the clones behind newlyDuplicatedLines, as remediation evidence. */
|
|
53
55
|
duplicationPartners: string[];
|
|
54
56
|
/**
|
|
55
57
|
* false: the file only feeds function matching and the duplication universes (it lies outside
|
|
@@ -64,7 +66,7 @@ export interface GateViolation {
|
|
|
64
66
|
functionName?: string;
|
|
65
67
|
startLine: number;
|
|
66
68
|
endLine: number;
|
|
67
|
-
/** Absent for new-function violations, which have no base value. */
|
|
69
|
+
/** Absent for new-function and duplication violations, which have no base value. */
|
|
68
70
|
baseValue?: number;
|
|
69
71
|
headValue: number;
|
|
70
72
|
/** The largest value that would have passed. */
|
package/dist/regressionGate.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{lcsLength as e}from"./duplication.js";const t={newFunction:{maxCognitiveComplexity:15,maxNcss:60,maxNestingDepth:4},tolerance:{cognitiveComplexity:2,ncss:5,nestingDepth:1,depDegree:10,halsteadVolume:150,fileNcss:20,duplicateLines:0},matchSimilarityPercent:70},n=String,r=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,allowance:e=>e.cognitiveComplexity,remediation:`Simplify the added branching or split the function.`,format:n},{metric:`NCSS`,value:e=>e.ncss,allowance:e=>e.ncss,remediation:`Extract the added statements into helper functions or remove them.`,format:n},{metric:`max nesting depth`,value:e=>e.nestingDepth,allowance:e=>e.nestingDepth,remediation:`Flatten the added nesting with early returns or extracted helpers.`,format:n},{metric:`DepDegree`,value:e=>e.depDegree,allowance:e=>e.depDegree,remediation:`Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.`,format:n},{metric:`Halstead volume`,value:e=>e.halstead.volume,allowance:(e,t)=>Math.max(e.halsteadVolume,t*.25),remediation:`Shrink the function: fewer distinct names and operations.`,format:e=>e.toFixed(1)}],i=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,limit:e=>e.maxCognitiveComplexity,remediation:`Split the new function or simplify its branching.`},{metric:`NCSS`,value:e=>e.ncss,limit:e=>e.maxNcss,remediation:`Split the new function into smaller ones.`},{metric:`max nesting depth`,value:e=>e.nestingDepth,limit:e=>e.maxNestingDepth,remediation:`Flatten the new function with early returns or extracted helpers.`}];function a(e,t){let n=l(e,t.matchSimilarityPercent),r=[],i=[],a=0,c=0;for(let o of n.pairs){let n=e[o.headFileIndex];n.gated!==!1&&(a+=1,r.push(...y(n.file,o.base,o.head,t.tolerance)),i.push(s(n.file,o.head,o.base)))}let u=0;for(let[a,l]of e.entries()){if(l.gated===!1)continue;u+=1;let e=l.headMetrics?.functions??[],d=n.headMatched[a];for(let[n,a]of e.entries())d[n]||(c+=1,r.push(...b(l.file,a,t.newFunction)),i.push(s(l.file,a)));r.push(...x(l,o(l,a,n),r,t)),r.push(...C(l,t.tolerance))}return r.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),i.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),{violations:r,checkedFileCount:u,checkedFunctionCount:a+c,newFunctionCount:c,checkedFunctions:i}}function o(e,t,n){let r=e.baseMetrics?.functions??[],i=n.baseMatched[t],a=n.headMatched[t],o=r.flatMap((e,t)=>!i[t]&&e.name!==void 0?[t]:[]);if(o.length===0)return!1;let s=(e.headMetrics?.functions??[]).flatMap((e,t)=>a[t]?[]:[t]);return o.length*s.length>f?!1:o.some(t=>s.some(n=>v(e.baseFunctionTokens?.[t],e.headFunctionTokens?.[n],30)>=30))}function s(e,t,n){return{file:e,name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,base:n?c(n):void 0,head:c(t)}}function c(e){return{cognitiveComplexity:e.cognitiveComplexity,ncss:e.ncss,nestingDepth:e.nestingDepth,depDegree:e.depDegree,halsteadVolume:e.halstead.volume}}function l(e,t){let n=e.map(e=>(e.baseMetrics?.functions??[]).map(()=>!1)),r=e.map(e=>(e.headMetrics?.functions??[]).map(()=>!1)),i=[];for(let t of e.keys()){let a=a=>{let o=d(u(e,n,`base`,t),a),s=d(u(e,r,`head`,t),a);for(let[e,t]of o)p(t,s.get(e)??[],n,r,i)};a(e=>e.name===void 0?void 0:`${e.name}\u0000${e.parameterCount}`),a(e=>e.name)}return m(e,n,r,i,t),{pairs:i,baseMatched:n,headMatched:r}}function u(e,t,n,r){let i=[];for(let[a,o]of e.entries()){if(r!==void 0&&a!==r)continue;let e=t[a],s=n===`base`?o.baseMetrics:o.headMetrics,c=n===`base`?o.baseFunctionTokens:o.headFunctionTokens;for(let[t,n]of(s?.functions??[]).entries())e[t]||i.push({fileIndex:a,functionIndex:t,fn:n,tokens:c?.[t]})}return i}function d(e,t){let n=/* @__PURE__ */ new Map;for(let r of e){let e=t(r.fn);if(e===void 0)continue;let i=n.get(e)??[];i.push(r),n.set(e,i)}return n}const f=1e4;function p(e,t,n,r,i){let a=e.length+t.length>2&&e.length*t.length<=f,o=[];for(let[n,r]of e.entries())for(let[e,i]of t.entries())o.push({similarity:a?v(r.tokens,i.tokens,0):0,basePosition:n,headPosition:e});h(o,e,t,n,r,i)}function m(e,t,n,r,i){let a=u(e,t,`base`),o=u(e,n,`head`);if(a.length*o.length>f)return;let s=[];for(let[e,t]of a.entries())for(let[n,r]of o.entries()){let a=v(t.tokens,r.tokens,i);a>=i&&s.push({similarity:a,basePosition:e,headPosition:n})}h(s,a,o,t,n,r)}function h(e,t,n,r,i,a){e.sort((e,t)=>t.similarity-e.similarity||e.basePosition-t.basePosition||e.headPosition-t.headPosition);let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set,l=g(e,o,s,c);for(let e of[...o.keys()].toSorted((e,t)=>e-t))l.has(e)||_(e,o,s,c,/* @__PURE__ */ new Set);for(let[e,o]of[...s.entries()].toSorted((e,t)=>e[0]-t[0])){let s=t[o],c=n[e];r[s.fileIndex][s.functionIndex]=!0,i[c.fileIndex][c.functionIndex]=!0,a.push({base:s.fn,head:c.fn,headFileIndex:c.fileIndex})}}function g(e,t,n,r){let i=/* @__PURE__ */ new Set;for(let a of e){let e=t.get(a.basePosition)??[];e.push(a.headPosition),t.set(a.basePosition,e),!i.has(a.basePosition)&&!n.has(a.headPosition)&&(n.set(a.headPosition,a.basePosition),i.add(a.basePosition),a.similarity===100&&r.add(a.headPosition))}return i}function _(e,t,n,r,i){for(let a of t.get(e)??[]){if(i.has(a)||r.has(a))continue;i.add(a);let o=n.get(a);if(o===void 0||_(o,t,n,r,i))return n.set(a,e),!0}return!1}function v(t,n,r){if(!t||!n||t.length===0||n.length===0)return 0;let i=Math.max(t.length,n.length);return Math.min(t.length,n.length)*100<r*i?0:e(t,n)*100/i}function y(e,t,n,i){let a=[];for(let o of r){let r=o.value(t),s=o.value(n),c=r+o.allowance(i,r);if(s>c){let t=n.name??`<anonymous>`;a.push({gate:`function-regression`,metric:o.metric,file:e,functionName:t,startLine:n.startLine,endLine:n.endLine,baseValue:r,headValue:s,allowedValue:c,message:`${e}:${n.startLine}-${n.endLine} ${t}: ${o.metric} worsened ${o.format(r)} -> ${o.format(s)} (allowed <= ${o.format(c)}). `+o.remediation})}}return a}function b(e,t,n){let r=[];for(let a of i){let i=a.value(t),o=a.limit(n);if(i>o){let n=t.name??`<anonymous>`;r.push({gate:`new-function`,metric:a.metric,file:e,functionName:n,startLine:t.startLine,endLine:t.endLine,headValue:i,allowedValue:o,message:`${e}:${t.startLine}-${t.endLine} new function ${n}: ${a.metric} ${i} exceeds the new-code limit ${o}. ${a.remediation}`})}}return r}function x(e,t,n,r){let{baseMetrics:i,headMetrics:a}=e;if(!i||!a||!t)return[];let o=[],s=r.tolerance,c=n.some(t=>t.file===e.file&&t.metric===`cognitive complexity`&&(t.gate===`function-regression`||t.gate===`new-function`)&&t.headValue===a.maxCognitiveComplexity),l=i.maxCognitiveComplexity+s.cognitiveComplexity;if(!c&&a.maxCognitiveComplexity>l){let t=S(a.functions),n=t?.startLine??1,r=t?.endLine??a.lines.total;o.push({gate:`file-regression`,metric:`file max cognitive complexity`,file:e.file,functionName:t?.name??void 0,startLine:n,endLine:r,baseValue:i.maxCognitiveComplexity,headValue:a.maxCognitiveComplexity,allowedValue:l,message:`${e.file}:${n}-${r}: the file's max cognitive complexity worsened ${i.maxCognitiveComplexity} -> ${a.maxCognitiveComplexity} (allowed <= ${l}). Simplify or split the most complex function${t?` (${t.name??`<anonymous>`})`:``}.`})}let u=i.ncssCount+s.fileNcss;return a.ncssCount>u&&o.push({gate:`file-regression`,metric:`file NCSS`,file:e.file,startLine:1,endLine:a.lines.total,baseValue:i.ncssCount,headValue:a.ncssCount,allowedValue:u,message:`${e.file}:1-${a.lines.total}: functions were removed or split while the file grew from NCSS ${i.ncssCount} to ${a.ncssCount} (allowed <= ${u}). Remove the added statements or move genuinely new code into its own module.`}),o}function S(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity)&&(t=n);return t}function C(e,t){let n=e.
|
|
1
|
+
import{lcsLength as e}from"./duplication.js";const t={newFunction:{maxCognitiveComplexity:15,maxNcss:60,maxNestingDepth:4},tolerance:{cognitiveComplexity:2,ncss:5,nestingDepth:1,depDegree:10,halsteadVolume:150,fileNcss:20,duplicateLines:0},matchSimilarityPercent:70},n=String,r=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,allowance:e=>e.cognitiveComplexity,remediation:`Simplify the added branching or split the function.`,format:n},{metric:`NCSS`,value:e=>e.ncss,allowance:e=>e.ncss,remediation:`Extract the added statements into helper functions or remove them.`,format:n},{metric:`max nesting depth`,value:e=>e.nestingDepth,allowance:e=>e.nestingDepth,remediation:`Flatten the added nesting with early returns or extracted helpers.`,format:n},{metric:`DepDegree`,value:e=>e.depDegree,allowance:e=>e.depDegree,remediation:`Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.`,format:n},{metric:`Halstead volume`,value:e=>e.halstead.volume,allowance:(e,t)=>Math.max(e.halsteadVolume,t*.25),remediation:`Shrink the function: fewer distinct names and operations.`,format:e=>e.toFixed(1)}],i=[{metric:`cognitive complexity`,value:e=>e.cognitiveComplexity,limit:e=>e.maxCognitiveComplexity,remediation:`Split the new function or simplify its branching.`},{metric:`NCSS`,value:e=>e.ncss,limit:e=>e.maxNcss,remediation:`Split the new function into smaller ones.`},{metric:`max nesting depth`,value:e=>e.nestingDepth,limit:e=>e.maxNestingDepth,remediation:`Flatten the new function with early returns or extracted helpers.`}];function a(e,t){let n=l(e,t.matchSimilarityPercent),r=[],i=[],a=0,c=0;for(let o of n.pairs){let n=e[o.headFileIndex];n.gated!==!1&&(a+=1,r.push(...y(n.file,o.base,o.head,t.tolerance)),i.push(s(n.file,o.head,o.base)))}let u=0;for(let[a,l]of e.entries()){if(l.gated===!1)continue;u+=1;let e=l.headMetrics?.functions??[],d=n.headMatched[a];for(let[n,a]of e.entries())d[n]||(c+=1,r.push(...b(l.file,a,t.newFunction)),i.push(s(l.file,a)));r.push(...x(l,o(l,a,n),r,t)),r.push(...C(l,t.tolerance))}return r.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),i.sort((e,t)=>e.file.localeCompare(t.file)||e.startLine-t.startLine),{violations:r,checkedFileCount:u,checkedFunctionCount:a+c,newFunctionCount:c,checkedFunctions:i}}function o(e,t,n){let r=e.baseMetrics?.functions??[],i=n.baseMatched[t],a=n.headMatched[t],o=r.flatMap((e,t)=>!i[t]&&e.name!==void 0?[t]:[]);if(o.length===0)return!1;let s=(e.headMetrics?.functions??[]).flatMap((e,t)=>a[t]?[]:[t]);return o.length*s.length>f?!1:o.some(t=>s.some(n=>v(e.baseFunctionTokens?.[t],e.headFunctionTokens?.[n],30)>=30))}function s(e,t,n){return{file:e,name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,base:n?c(n):void 0,head:c(t)}}function c(e){return{cognitiveComplexity:e.cognitiveComplexity,ncss:e.ncss,nestingDepth:e.nestingDepth,depDegree:e.depDegree,halsteadVolume:e.halstead.volume}}function l(e,t){let n=e.map(e=>(e.baseMetrics?.functions??[]).map(()=>!1)),r=e.map(e=>(e.headMetrics?.functions??[]).map(()=>!1)),i=[];for(let t of e.keys()){let a=a=>{let o=d(u(e,n,`base`,t),a),s=d(u(e,r,`head`,t),a);for(let[e,t]of o)p(t,s.get(e)??[],n,r,i)};a(e=>e.name===void 0?void 0:`${e.name}\u0000${e.parameterCount}`),a(e=>e.name)}return m(e,n,r,i,t),{pairs:i,baseMatched:n,headMatched:r}}function u(e,t,n,r){let i=[];for(let[a,o]of e.entries()){if(r!==void 0&&a!==r)continue;let e=t[a],s=n===`base`?o.baseMetrics:o.headMetrics,c=n===`base`?o.baseFunctionTokens:o.headFunctionTokens;for(let[t,n]of(s?.functions??[]).entries())e[t]||i.push({fileIndex:a,functionIndex:t,fn:n,tokens:c?.[t]})}return i}function d(e,t){let n=/* @__PURE__ */ new Map;for(let r of e){let e=t(r.fn);if(e===void 0)continue;let i=n.get(e)??[];i.push(r),n.set(e,i)}return n}const f=1e4;function p(e,t,n,r,i){let a=e.length+t.length>2&&e.length*t.length<=f,o=[];for(let[n,r]of e.entries())for(let[e,i]of t.entries())o.push({similarity:a?v(r.tokens,i.tokens,0):0,basePosition:n,headPosition:e});h(o,e,t,n,r,i)}function m(e,t,n,r,i){let a=u(e,t,`base`),o=u(e,n,`head`);if(a.length*o.length>f)return;let s=[];for(let[e,t]of a.entries())for(let[n,r]of o.entries()){let a=v(t.tokens,r.tokens,i);a>=i&&s.push({similarity:a,basePosition:e,headPosition:n})}h(s,a,o,t,n,r)}function h(e,t,n,r,i,a){e.sort((e,t)=>t.similarity-e.similarity||e.basePosition-t.basePosition||e.headPosition-t.headPosition);let o=/* @__PURE__ */ new Map,s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Set,l=g(e,o,s,c);for(let e of[...o.keys()].toSorted((e,t)=>e-t))l.has(e)||_(e,o,s,c,/* @__PURE__ */ new Set);for(let[e,o]of[...s.entries()].toSorted((e,t)=>e[0]-t[0])){let s=t[o],c=n[e];r[s.fileIndex][s.functionIndex]=!0,i[c.fileIndex][c.functionIndex]=!0,a.push({base:s.fn,head:c.fn,headFileIndex:c.fileIndex})}}function g(e,t,n,r){let i=/* @__PURE__ */ new Set;for(let a of e){let e=t.get(a.basePosition)??[];e.push(a.headPosition),t.set(a.basePosition,e),!i.has(a.basePosition)&&!n.has(a.headPosition)&&(n.set(a.headPosition,a.basePosition),i.add(a.basePosition),a.similarity===100&&r.add(a.headPosition))}return i}function _(e,t,n,r,i){for(let a of t.get(e)??[]){if(i.has(a)||r.has(a))continue;i.add(a);let o=n.get(a);if(o===void 0||_(o,t,n,r,i))return n.set(a,e),!0}return!1}function v(t,n,r){if(!t||!n||t.length===0||n.length===0)return 0;let i=Math.max(t.length,n.length);return Math.min(t.length,n.length)*100<r*i?0:e(t,n)*100/i}function y(e,t,n,i){let a=[];for(let o of r){let r=o.value(t),s=o.value(n),c=r+o.allowance(i,r);if(s>c){let t=n.name??`<anonymous>`;a.push({gate:`function-regression`,metric:o.metric,file:e,functionName:t,startLine:n.startLine,endLine:n.endLine,baseValue:r,headValue:s,allowedValue:c,message:`${e}:${n.startLine}-${n.endLine} ${t}: ${o.metric} worsened ${o.format(r)} -> ${o.format(s)} (allowed <= ${o.format(c)}). `+o.remediation})}}return a}function b(e,t,n){let r=[];for(let a of i){let i=a.value(t),o=a.limit(n);if(i>o){let n=t.name??`<anonymous>`;r.push({gate:`new-function`,metric:a.metric,file:e,functionName:n,startLine:t.startLine,endLine:t.endLine,headValue:i,allowedValue:o,message:`${e}:${t.startLine}-${t.endLine} new function ${n}: ${a.metric} ${i} exceeds the new-code limit ${o}. ${a.remediation}`})}}return r}function x(e,t,n,r){let{baseMetrics:i,headMetrics:a}=e;if(!i||!a||!t)return[];let o=[],s=r.tolerance,c=n.some(t=>t.file===e.file&&t.metric===`cognitive complexity`&&(t.gate===`function-regression`||t.gate===`new-function`)&&t.headValue===a.maxCognitiveComplexity),l=i.maxCognitiveComplexity+s.cognitiveComplexity;if(!c&&a.maxCognitiveComplexity>l){let t=S(a.functions),n=t?.startLine??1,r=t?.endLine??a.lines.total;o.push({gate:`file-regression`,metric:`file max cognitive complexity`,file:e.file,functionName:t?.name??void 0,startLine:n,endLine:r,baseValue:i.maxCognitiveComplexity,headValue:a.maxCognitiveComplexity,allowedValue:l,message:`${e.file}:${n}-${r}: the file's max cognitive complexity worsened ${i.maxCognitiveComplexity} -> ${a.maxCognitiveComplexity} (allowed <= ${l}). Simplify or split the most complex function${t?` (${t.name??`<anonymous>`})`:``}.`})}let u=i.ncssCount+s.fileNcss;return a.ncssCount>u&&o.push({gate:`file-regression`,metric:`file NCSS`,file:e.file,startLine:1,endLine:a.lines.total,baseValue:i.ncssCount,headValue:a.ncssCount,allowedValue:u,message:`${e.file}:1-${a.lines.total}: functions were removed or split while the file grew from NCSS ${i.ncssCount} to ${a.ncssCount} (allowed <= ${u}). Remove the added statements or move genuinely new code into its own module.`}),o}function S(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity)&&(t=n);return t}function C(e,t){let n=e.newlyDuplicatedLines,r=t.duplicateLines;if(n.length<=r)return[];let i=n[0],a=n.at(-1),o=e.duplicationPartners.slice(0,3).join(`, `);return[{gate:`duplication`,metric:`newly duplicated lines`,file:e.file,startLine:i,endLine:a,headValue:n.length,allowedValue:r,message:`${e.file}:${i}-${a}: ${n.length} changed lines duplicate existing code (lines ${w(n)}; allowed <= ${r}). Deduplicate${o?` against ${o}`:` the repeated code`} by extracting a shared helper.`}]}function w(e){let t=[],n=e[0];for(let[r,i]of e.entries()){let a=e[r+1];a!==i+1&&(t.push(n===i?String(i):`${n}-${i}`),n=a)}return t.join(`, `)}export{t as defaultGateOptions,a as evaluateRegressionGate};
|
|
2
2
|
//# sourceMappingURL=regressionGate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regressionGate.js","names":[],"sources":["../src/regressionGate.ts"],"sourcesContent":["import { lcsLength } from './duplication.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/**\n * Absolute limits applied only to functions with no base counterpart (SonarQube-style \"Clean as\n * You Code\"): new code is the one place absolute thresholds are required, since there is nothing\n * to ratchet against.\n */\nexport interface NewFunctionThresholds {\n maxCognitiveComplexity: number;\n maxNcss: number;\n maxNestingDepth: number;\n}\n\n/**\n * Allowances added on top of each base value before a ratchet fires, so legitimate refactorings\n * that transiently worsen one number (e.g. an extraction adding a few NCSS lines) don't trap the\n * agent in a loop.\n */\nexport interface GateTolerances {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n /** File-total NCSS growth allowed when base functions disappeared (the anti-gaming backstop). */\n fileNcss: number;\n /** Duplicated-line growth allowed per changed file. */\n duplicateLines: number;\n}\n\nexport interface GateOptions {\n newFunction: NewFunctionThresholds;\n tolerance: GateTolerances;\n /**\n * Minimum token-LCS similarity percent for re-matching a renamed/moved function to its base\n * counterpart, so renames don't appear as delete+add and hit the new-code thresholds.\n */\n matchSimilarityPercent: number;\n}\n\nexport const defaultGateOptions: GateOptions = {\n // 15 is SonarSource's conventional default; 4 is where the measured cognitive-load response to\n // nesting saturates (Peitek et al. 2021); 60 matches PMD's NcssCount method default.\n newFunction: { maxCognitiveComplexity: 15, maxNcss: 60, maxNestingDepth: 4 },\n // The per-metric allowances are calibrated to admit roughly the same edit: ~5 added statements\n // or one added branch (cognitive 2 ~ nesting 1 ~ ncss 5 ~ depDegree 10 ~ Halstead volume 150;\n // the volume allowance additionally scales with the base value, see ratchetMetrics), so one\n // ratchet cannot block an addition the others deliberately allow.\n tolerance: {\n cognitiveComplexity: 2,\n ncss: 5,\n nestingDepth: 1,\n depDegree: 10,\n halsteadVolume: 150,\n fileNcss: 20,\n duplicateLines: 0,\n },\n matchSimilarityPercent: 70,\n};\n\n/** One changed file with both revisions measured; the caller resolves git and duplication data. */\nexport interface GateFileInput {\n /** Display path (repository-relative head path; base path for deleted files). */\n file: string;\n /** Absent for added files. */\n baseMetrics?: CodeMetrics;\n /** Absent for deleted files. */\n headMetrics?: CodeMetrics;\n /** Token sequences index-parallel to baseMetrics.functions, for similarity re-matching. */\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n /** Distinct duplicated lines of this file at the base revision (within-file and cross-file). */\n baseDuplicatedLineCount: number;\n headDuplicatedLineCount: number;\n /** Other files sharing cross-file duplicates with this file at head, as remediation evidence. */\n duplicationPartners: string[];\n /**\n * false: the file only feeds function matching and the duplication universes (it lies outside\n * the gated target directory); no violations are evaluated or reported for it. Default true.\n */\n gated?: boolean;\n}\n\nexport interface GateViolation {\n gate: 'function-regression' | 'new-function' | 'file-regression' | 'duplication';\n metric: string;\n file: string;\n functionName?: string;\n startLine: number;\n endLine: number;\n /** Absent for new-function violations, which have no base value. */\n baseValue?: number;\n headValue: number;\n /** The largest value that would have passed. */\n allowedValue: number;\n /** One-line human/agent-readable statement with the remediation direction. */\n message: string;\n}\n\n/** The gated per-function values of one checked (matched or new) function, for `--full` output. */\nexport interface CheckedFunctionReport {\n /** The head file (a cross-file move is reported where the function now lives). */\n file: string;\n name: string;\n startLine: number;\n endLine: number;\n /** Absent for new functions. */\n base?: GateFunctionValues;\n head: GateFunctionValues;\n}\n\nexport interface GateFunctionValues {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n}\n\nexport interface GateResult {\n violations: GateViolation[];\n checkedFileCount: number;\n checkedFunctionCount: number;\n newFunctionCount: number;\n /** Every checked function with its gated values, ordered by file and line. */\n checkedFunctions: CheckedFunctionReport[];\n}\n\ninterface RatchetMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n /** The growth allowed on top of the base value before the ratchet fires. */\n allowance: (tolerances: GateTolerances, baseValue: number) => number;\n remediation: string;\n format: (value: number) => string;\n}\n\nconst formatInteger = String;\nconst formatFloat = (value: number): string => value.toFixed(1);\n\n/**\n * Gate-eligible per-function metrics (monotone, stable, actionable): a matched function must not\n * worsen any of them beyond its tolerance. \"Your change made this function worse\" is defensible\n * regardless of metric calibration, so no absolute threshold is involved.\n */\nconst ratchetMetrics: RatchetMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n allowance: (tolerances) => tolerances.cognitiveComplexity,\n remediation: 'Simplify the added branching or split the function.',\n format: formatInteger,\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n allowance: (tolerances) => tolerances.ncss,\n remediation: 'Extract the added statements into helper functions or remove them.',\n format: formatInteger,\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n allowance: (tolerances) => tolerances.nestingDepth,\n remediation: 'Flatten the added nesting with early returns or extracted helpers.',\n format: formatInteger,\n },\n {\n metric: 'DepDegree',\n value: (fn) => fn.depDegree,\n allowance: (tolerances) => tolerances.depDegree,\n remediation: 'Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.',\n format: formatInteger,\n },\n {\n metric: 'Halstead volume',\n value: (fn) => fn.halstead.volume,\n // Volume grows as length * log2(vocabulary), so a flat allowance that admits ~5 statements in\n // a small function admits barely one in a large-vocabulary function; the relative term keeps\n // the ~5-statement calibration at every function size.\n allowance: (tolerances, baseValue) => Math.max(tolerances.halsteadVolume, baseValue * 0.25),\n remediation: 'Shrink the function: fewer distinct names and operations.',\n format: formatFloat,\n },\n];\n\ninterface NewFunctionMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n limit: (thresholds: NewFunctionThresholds) => number;\n remediation: string;\n}\n\nconst newFunctionMetrics: NewFunctionMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n limit: (thresholds) => thresholds.maxCognitiveComplexity,\n remediation: 'Split the new function or simplify its branching.',\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n limit: (thresholds) => thresholds.maxNcss,\n remediation: 'Split the new function into smaller ones.',\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n limit: (thresholds) => thresholds.maxNestingDepth,\n remediation: 'Flatten the new function with early returns or extracted helpers.',\n },\n];\n\n/**\n * Evaluates the regression gate over changed files measured at both revisions. Violations are the\n * whole output contract: everything that passes stays silent, so a clean change costs the\n * consuming agent almost no tokens.\n */\nexport function evaluateRegressionGate(files: GateFileInput[], options: GateOptions): GateResult {\n const matching = matchAllFunctions(files, options.matchSimilarityPercent);\n const violations: GateViolation[] = [];\n const checkedFunctions: CheckedFunctionReport[] = [];\n let checkedPairCount = 0;\n let newFunctionCount = 0;\n\n for (const pair of matching.pairs) {\n const file = files[pair.headFileIndex] as GateFileInput;\n if (file.gated === false) {\n continue;\n }\n checkedPairCount += 1;\n violations.push(...checkRatchets(file.file, pair.base, pair.head, options.tolerance));\n checkedFunctions.push(toFunctionReport(file.file, pair.head, pair.base));\n }\n\n let gatedFileCount = 0;\n for (const [fileIndex, file] of files.entries()) {\n if (file.gated === false) {\n continue;\n }\n gatedFileCount += 1;\n const headFunctions = file.headMetrics?.functions ?? [];\n const headMatched = matching.headMatched[fileIndex] as boolean[];\n for (const [functionIndex, fn] of headFunctions.entries()) {\n if (headMatched[functionIndex]) {\n continue;\n }\n newFunctionCount += 1;\n violations.push(...checkNewFunction(file.file, fn, options.newFunction));\n checkedFunctions.push(toFunctionReport(file.file, fn));\n }\n violations.push(...checkFileBackstops(file, hasSplitEvidence(file, fileIndex, matching), violations, options));\n violations.push(...checkDuplication(file, options.tolerance));\n }\n\n violations.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n checkedFunctions.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n return {\n violations,\n checkedFileCount: gatedFileCount,\n checkedFunctionCount: checkedPairCount + newFunctionCount,\n newFunctionCount,\n checkedFunctions,\n };\n}\n\n/**\n * Similarity below the match threshold but above this marks a removed function's content as\n * reappearing inside unmatched new code — the signature of a split or evasive rewrite.\n */\nconst splitEvidenceSimilarityPercent = 30;\n\n/**\n * Whether a removed NAMED base function's content partially reappears in an unmatched head\n * function of the same file. This is what arms the file-level backstops: splitting a function to\n * hide a worsening behind the (laxer) new-code thresholds leaves its fragments partially similar\n * to the removed original, while an ordinary \"delete a helper, add an unrelated function\" change\n * shares nothing and must not have its brand-new code re-judged against the file's old aggregates.\n */\nfunction hasSplitEvidence(file: GateFileInput, fileIndex: number, matching: FunctionMatching): boolean {\n const baseFunctions = file.baseMetrics?.functions ?? [];\n const baseMatchedFlags = matching.baseMatched[fileIndex] as boolean[];\n const headMatchedFlags = matching.headMatched[fileIndex] as boolean[];\n const removedNamed = baseFunctions.flatMap((fn, index) =>\n !baseMatchedFlags[index] && fn.name !== undefined ? [index] : []\n );\n if (removedNamed.length === 0) {\n return false;\n }\n const unmatchedHead = (file.headMetrics?.functions ?? []).flatMap((_, index) =>\n headMatchedFlags[index] ? [] : [index]\n );\n if (removedNamed.length * unmatchedHead.length > maxSimilarityComparisons) {\n return false;\n }\n return removedNamed.some((baseIndex) =>\n unmatchedHead.some(\n (headIndex) =>\n tokenSimilarityPercent(\n file.baseFunctionTokens?.[baseIndex],\n file.headFunctionTokens?.[headIndex],\n splitEvidenceSimilarityPercent\n ) >= splitEvidenceSimilarityPercent\n )\n );\n}\n\nfunction toFunctionReport(file: string, head: FunctionMetrics, base?: FunctionMetrics): CheckedFunctionReport {\n return {\n file,\n name: head.name ?? '<anonymous>',\n startLine: head.startLine,\n endLine: head.endLine,\n base: base ? toFunctionValues(base) : undefined,\n head: toFunctionValues(head),\n };\n}\n\nfunction toFunctionValues(fn: FunctionMetrics): GateFunctionValues {\n return {\n cognitiveComplexity: fn.cognitiveComplexity,\n ncss: fn.ncss,\n nestingDepth: fn.nestingDepth,\n depDegree: fn.depDegree,\n halsteadVolume: fn.halstead.volume,\n };\n}\n\n/** One side of a match candidate: a function of one changed file plus its token sequence. */\ninterface IndexedFunction {\n fileIndex: number;\n functionIndex: number;\n fn: FunctionMetrics;\n tokens: Int32Array | undefined;\n}\n\ninterface MatchedPair {\n base: FunctionMetrics;\n head: FunctionMetrics;\n headFileIndex: number;\n}\n\ninterface FunctionMatching {\n pairs: MatchedPair[];\n /** Per file, per function index: whether it found a counterpart (anywhere in the change set). */\n baseMatched: boolean[][];\n headMatched: boolean[][];\n}\n\ninterface SimilarityCandidate {\n similarity: number;\n basePosition: number;\n headPosition: number;\n}\n\n/**\n * Matches head functions to base functions: within each file first by name + arity, then by name\n * alone (signature changes) — resolving ambiguous same-name groups by token similarity instead of\n * list position, so an inserted overload cannot shift the pairing — and finally by\n * normalized-token LCS similarity across ALL changed files (renames and moves between files,\n * reusing the near-miss clone machinery), so refactorings don't appear as delete+add and hit the\n * new-code thresholds.\n */\nfunction matchAllFunctions(files: GateFileInput[], matchSimilarityPercent: number): FunctionMatching {\n const baseMatched = files.map((file) => (file.baseMetrics?.functions ?? []).map(() => false));\n const headMatched = files.map((file) => (file.headMetrics?.functions ?? []).map(() => false));\n const pairs: MatchedPair[] = [];\n\n for (const fileIndex of files.keys()) {\n const pairByKey = (key: (fn: FunctionMetrics) => string | undefined): void => {\n const baseByKey = groupByKey(collectUnmatched(files, baseMatched, 'base', fileIndex), key);\n const headByKey = groupByKey(collectUnmatched(files, headMatched, 'head', fileIndex), key);\n for (const [groupKey, baseGroup] of baseByKey) {\n pairGroup(baseGroup, headByKey.get(groupKey) ?? [], baseMatched, headMatched, pairs);\n }\n };\n pairByKey((fn) => (fn.name === undefined ? undefined : `${fn.name}\\u0000${fn.parameterCount}`));\n pairByKey((fn) => fn.name);\n }\n\n pairAcrossChangeSet(files, baseMatched, headMatched, pairs, matchSimilarityPercent);\n return { pairs, baseMatched, headMatched };\n}\n\n/** Unmatched functions of one file (or of every file when onlyFileIndex is undefined). */\nfunction collectUnmatched(\n files: GateFileInput[],\n matchedByFile: boolean[][],\n side: 'base' | 'head',\n onlyFileIndex?: number\n): IndexedFunction[] {\n const unmatched: IndexedFunction[] = [];\n for (const [fileIndex, file] of files.entries()) {\n if (onlyFileIndex !== undefined && fileIndex !== onlyFileIndex) {\n continue;\n }\n const matched = matchedByFile[fileIndex] as boolean[];\n const metrics = side === 'base' ? file.baseMetrics : file.headMetrics;\n const tokens = side === 'base' ? file.baseFunctionTokens : file.headFunctionTokens;\n for (const [functionIndex, fn] of (metrics?.functions ?? []).entries()) {\n if (!matched[functionIndex]) {\n unmatched.push({ fileIndex, functionIndex, fn, tokens: tokens?.[functionIndex] });\n }\n }\n }\n return unmatched;\n}\n\nfunction groupByKey(\n functions: IndexedFunction[],\n key: (fn: FunctionMetrics) => string | undefined\n): Map<string, IndexedFunction[]> {\n const groups = new Map<string, IndexedFunction[]>();\n for (const indexed of functions) {\n const groupKey = key(indexed.fn);\n if (groupKey === undefined) {\n continue;\n }\n const group = groups.get(groupKey) ?? [];\n group.push(indexed);\n groups.set(groupKey, group);\n }\n return groups;\n}\n\n/** Bounds the quadratic similarity passes; beyond this, leftovers gate as new/removed functions. */\nconst maxSimilarityComparisons = 10_000;\n\n/**\n * Pairs one key group. An unambiguous 1x1 group matches directly; a larger group (same-name\n * methods of different classes, overload sets) is assigned best-similarity-first so an inserted\n * function cannot shift the pairing of the others. The names already match, so any pairing is\n * acceptable: similarity only ORDERS the assignment (no threshold), and without token sequences\n * (or in an oversized group) the stable tie-break reproduces positional order.\n */\nfunction pairGroup(\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n const ambiguous =\n baseGroup.length + headGroup.length > 2 && baseGroup.length * headGroup.length <= maxSimilarityComparisons;\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of baseGroup.entries()) {\n for (const [headPosition, head] of headGroup.entries()) {\n candidates.push({\n similarity: ambiguous ? tokenSimilarityPercent(base.tokens, head.tokens, 0) : 0,\n basePosition,\n headPosition,\n });\n }\n }\n takeMaximumMatches(candidates, baseGroup, headGroup, baseMatched, headMatched, pairs);\n}\n\n/** Cross-file (and remaining within-file) similarity matching over every unmatched function. */\nfunction pairAcrossChangeSet(\n files: GateFileInput[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[],\n matchSimilarityPercent: number\n): void {\n const unmatchedBase = collectUnmatched(files, baseMatched, 'base');\n const unmatchedHead = collectUnmatched(files, headMatched, 'head');\n if (unmatchedBase.length * unmatchedHead.length > maxSimilarityComparisons) {\n return;\n }\n\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of unmatchedBase.entries()) {\n for (const [headPosition, head] of unmatchedHead.entries()) {\n const similarity = tokenSimilarityPercent(base.tokens, head.tokens, matchSimilarityPercent);\n if (similarity >= matchSimilarityPercent) {\n candidates.push({ similarity, basePosition, headPosition });\n }\n }\n }\n takeMaximumMatches(candidates, unmatchedBase, unmatchedHead, baseMatched, headMatched, pairs);\n}\n\n/**\n * Assignment over the candidate edges: a greedy best-similarity-first seed, then Kuhn's\n * augmenting paths for the bases the seed left unmatched, so a function whose only counterpart\n * was taken by a slightly better pair is re-matched by rewiring instead of being falsely gated as\n * new code. Augmentation runs only when it increases cardinality, and an exact (100%) seed pair —\n * certain identity — is locked and never rewired even then: trading it for two partial matches\n * would ratchet an untouched function against some other function's rewrite. Non-exact seed pairs\n * may be rewired, but only when that matches strictly more functions (which a maximum-weight\n * assignment would also prefer). Without token sequences every similarity ties at 0 and the seed\n * reproduces positional order.\n */\nfunction takeMaximumMatches(\n candidates: SimilarityCandidate[],\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n candidates.sort(\n (left, right) =>\n right.similarity - left.similarity ||\n left.basePosition - right.basePosition ||\n left.headPosition - right.headPosition\n );\n const adjacency = new Map<number, number[]>();\n const baseOfHead = new Map<number, number>();\n const lockedHeads = new Set<number>();\n const matchedBases = seedGreedyMatches(candidates, adjacency, baseOfHead, lockedHeads);\n for (const basePosition of [...adjacency.keys()].toSorted((left, right) => left - right)) {\n if (!matchedBases.has(basePosition)) {\n tryAugment(basePosition, adjacency, baseOfHead, lockedHeads, new Set());\n }\n }\n\n for (const [headPosition, basePosition] of [...baseOfHead.entries()].toSorted((left, right) => left[0] - right[0])) {\n const base = baseGroup[basePosition] as IndexedFunction;\n const head = headGroup[headPosition] as IndexedFunction;\n (baseMatched[base.fileIndex] as boolean[])[base.functionIndex] = true;\n (headMatched[head.fileIndex] as boolean[])[head.functionIndex] = true;\n pairs.push({ base: base.fn, head: head.fn, headFileIndex: head.fileIndex });\n }\n}\n\n/**\n * Fills the adjacency lists and takes each best edge whose endpoints are both still free; a taken\n * exact (100%) edge locks its head against later rewiring.\n */\nfunction seedGreedyMatches(\n candidates: SimilarityCandidate[],\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>\n): Set<number> {\n const matchedBases = new Set<number>();\n for (const candidate of candidates) {\n const edges = adjacency.get(candidate.basePosition) ?? [];\n edges.push(candidate.headPosition);\n adjacency.set(candidate.basePosition, edges);\n if (!matchedBases.has(candidate.basePosition) && !baseOfHead.has(candidate.headPosition)) {\n baseOfHead.set(candidate.headPosition, candidate.basePosition);\n matchedBases.add(candidate.basePosition);\n if (candidate.similarity === 100) {\n lockedHeads.add(candidate.headPosition);\n }\n }\n }\n return matchedBases;\n}\n\n/** One Kuhn augmentation step: rewires existing (unlocked) matches only when that frees a head for `basePosition`. */\nfunction tryAugment(\n basePosition: number,\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>,\n visitedHeads: Set<number>\n): boolean {\n for (const headPosition of adjacency.get(basePosition) ?? []) {\n if (visitedHeads.has(headPosition) || lockedHeads.has(headPosition)) {\n continue;\n }\n visitedHeads.add(headPosition);\n const currentBase = baseOfHead.get(headPosition);\n if (currentBase === undefined || tryAugment(currentBase, adjacency, baseOfHead, lockedHeads, visitedHeads)) {\n baseOfHead.set(headPosition, basePosition);\n return true;\n }\n }\n return false;\n}\n\n/** Token-LCS similarity of two sequences relative to the longer one, in percent (0 when unusable). */\nfunction tokenSimilarityPercent(\n base: Int32Array | undefined,\n head: Int32Array | undefined,\n minPercent: number\n): number {\n if (!base || !head || base.length === 0 || head.length === 0) {\n return 0;\n }\n // The LCS cannot exceed the shorter sequence, so mismatched lengths are pruned for free.\n const maxLength = Math.max(base.length, head.length);\n if (Math.min(base.length, head.length) * 100 < minPercent * maxLength) {\n return 0;\n }\n return (lcsLength(base, head) * 100) / maxLength;\n}\n\nfunction checkRatchets(\n file: string,\n base: FunctionMetrics,\n head: FunctionMetrics,\n tolerances: GateTolerances\n): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const ratchet of ratchetMetrics) {\n const baseValue = ratchet.value(base);\n const headValue = ratchet.value(head);\n const allowedValue = baseValue + ratchet.allowance(tolerances, baseValue);\n if (headValue > allowedValue) {\n const name = head.name ?? '<anonymous>';\n violations.push({\n gate: 'function-regression',\n metric: ratchet.metric,\n file,\n functionName: name,\n startLine: head.startLine,\n endLine: head.endLine,\n baseValue,\n headValue,\n allowedValue,\n message:\n `${file}:${head.startLine}-${head.endLine} ${name}: ${ratchet.metric} worsened ` +\n `${ratchet.format(baseValue)} -> ${ratchet.format(headValue)} (allowed <= ${ratchet.format(allowedValue)}). ` +\n ratchet.remediation,\n });\n }\n }\n return violations;\n}\n\nfunction checkNewFunction(file: string, fn: FunctionMetrics, thresholds: NewFunctionThresholds): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const check of newFunctionMetrics) {\n const headValue = check.value(fn);\n const allowedValue = check.limit(thresholds);\n if (headValue > allowedValue) {\n const name = fn.name ?? '<anonymous>';\n violations.push({\n gate: 'new-function',\n metric: check.metric,\n file,\n functionName: name,\n startLine: fn.startLine,\n endLine: fn.endLine,\n headValue,\n allowedValue,\n message:\n `${file}:${fn.startLine}-${fn.endLine} new function ${name}: ${check.metric} ${headValue} ` +\n `exceeds the new-code limit ${allowedValue}. ${check.remediation}`,\n });\n }\n }\n return violations;\n}\n\n/**\n * Anti-gaming backstops: splitting a function resets its entity identity and could hide a\n * worsening behind the (laxer) new-code thresholds, so when a removed named function's content\n * reappears in unmatched new code (see hasSplitEvidence) the file-level aggregates ratchet too —\n * the max cognitive complexity per file and the file's total NCSS. Purely additive changes and\n * unrelated remove-plus-add changes stay ungated. The max-cognitive backstop is also skipped when\n * a function-level cognitive violation already reports the file's maximum, since it would only\n * restate it.\n */\nfunction checkFileBackstops(\n file: GateFileInput,\n splitEvidence: boolean,\n reportedViolations: GateViolation[],\n options: GateOptions\n): GateViolation[] {\n const { baseMetrics, headMetrics } = file;\n if (!baseMetrics || !headMetrics || !splitEvidence) {\n return [];\n }\n const violations: GateViolation[] = [];\n const tolerances = options.tolerance;\n\n const maximumAlreadyReported = reportedViolations.some(\n (violation) =>\n violation.file === file.file &&\n violation.metric === 'cognitive complexity' &&\n (violation.gate === 'function-regression' || violation.gate === 'new-function') &&\n violation.headValue === headMetrics.maxCognitiveComplexity\n );\n const allowedMaxCognitive = baseMetrics.maxCognitiveComplexity + tolerances.cognitiveComplexity;\n if (!maximumAlreadyReported && headMetrics.maxCognitiveComplexity > allowedMaxCognitive) {\n const worst = findMostComplexFunction(headMetrics.functions);\n const startLine = worst?.startLine ?? 1;\n const endLine = worst?.endLine ?? headMetrics.lines.total;\n violations.push({\n gate: 'file-regression',\n metric: 'file max cognitive complexity',\n file: file.file,\n functionName: worst?.name ?? undefined,\n startLine,\n endLine,\n baseValue: baseMetrics.maxCognitiveComplexity,\n headValue: headMetrics.maxCognitiveComplexity,\n allowedValue: allowedMaxCognitive,\n message:\n `${file.file}:${startLine}-${endLine}: the file's max cognitive complexity worsened ` +\n `${baseMetrics.maxCognitiveComplexity} -> ${headMetrics.maxCognitiveComplexity} ` +\n `(allowed <= ${allowedMaxCognitive}). Simplify or split the most complex ` +\n `function${worst ? ` (${worst.name ?? '<anonymous>'})` : ''}.`,\n });\n }\n\n const allowedFileNcss = baseMetrics.ncssCount + tolerances.fileNcss;\n if (headMetrics.ncssCount > allowedFileNcss) {\n violations.push({\n gate: 'file-regression',\n metric: 'file NCSS',\n file: file.file,\n startLine: 1,\n endLine: headMetrics.lines.total,\n baseValue: baseMetrics.ncssCount,\n headValue: headMetrics.ncssCount,\n allowedValue: allowedFileNcss,\n message:\n `${file.file}:1-${headMetrics.lines.total}: functions were removed or split while the file grew from NCSS ` +\n `${baseMetrics.ncssCount} to ${headMetrics.ncssCount} (allowed <= ${allowedFileNcss}). ` +\n `Remove the added statements or move genuinely new code into its own module.`,\n });\n }\n\n return violations;\n}\n\nfunction findMostComplexFunction(functions: FunctionMetrics[]): FunctionMetrics | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (!worst || fn.cognitiveComplexity > worst.cognitiveComplexity) {\n worst = fn;\n }\n }\n return worst;\n}\n\n/**\n * No new duplication: the distinct duplicated lines of a changed file (within-file plus\n * cross-file against the whole project, so copy-paste from existing code into new files is\n * caught) must not exceed the base revision's count.\n */\nfunction checkDuplication(file: GateFileInput, tolerances: GateTolerances): GateViolation[] {\n const allowedValue = file.baseDuplicatedLineCount + tolerances.duplicateLines;\n if (file.headDuplicatedLineCount <= allowedValue) {\n return [];\n }\n const partners = file.duplicationPartners.slice(0, 3).join(', ');\n const endLine = file.headMetrics?.lines.total ?? 1;\n return [\n {\n gate: 'duplication',\n metric: 'duplicated lines',\n file: file.file,\n startLine: 1,\n endLine,\n baseValue: file.baseDuplicatedLineCount,\n headValue: file.headDuplicatedLineCount,\n allowedValue,\n message:\n `${file.file}:1-${endLine}: duplicated lines increased ${file.baseDuplicatedLineCount} -> ` +\n `${file.headDuplicatedLineCount} (allowed <= ${allowedValue}). Deduplicate` +\n `${partners ? ` against ${partners}` : ' the repeated code'} by extracting a shared helper.`,\n },\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"regressionGate.js","names":[],"sources":["../src/regressionGate.ts"],"sourcesContent":["import { lcsLength } from './duplication.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/**\n * Absolute limits applied only to functions with no base counterpart (SonarQube-style \"Clean as\n * You Code\"): new code is the one place absolute thresholds are required, since there is nothing\n * to ratchet against.\n */\nexport interface NewFunctionThresholds {\n maxCognitiveComplexity: number;\n maxNcss: number;\n maxNestingDepth: number;\n}\n\n/**\n * Allowances added on top of each base value before a ratchet fires, so legitimate refactorings\n * that transiently worsen one number (e.g. an extraction adding a few NCSS lines) don't trap the\n * agent in a loop.\n */\nexport interface GateTolerances {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n /** File-total NCSS growth allowed when base functions disappeared (the anti-gaming backstop). */\n fileNcss: number;\n /** Newly duplicated changed lines allowed per changed file. */\n duplicateLines: number;\n}\n\nexport interface GateOptions {\n newFunction: NewFunctionThresholds;\n tolerance: GateTolerances;\n /**\n * Minimum token-LCS similarity percent for re-matching a renamed/moved function to its base\n * counterpart, so renames don't appear as delete+add and hit the new-code thresholds.\n */\n matchSimilarityPercent: number;\n}\n\nexport const defaultGateOptions: GateOptions = {\n // 15 is SonarSource's conventional default; 4 is where the measured cognitive-load response to\n // nesting saturates (Peitek et al. 2021); 60 matches PMD's NcssCount method default.\n newFunction: { maxCognitiveComplexity: 15, maxNcss: 60, maxNestingDepth: 4 },\n // The per-metric allowances are calibrated to admit roughly the same edit: ~5 added statements\n // or one added branch (cognitive 2 ~ nesting 1 ~ ncss 5 ~ depDegree 10 ~ Halstead volume 150;\n // the volume allowance additionally scales with the base value, see ratchetMetrics), so one\n // ratchet cannot block an addition the others deliberately allow.\n tolerance: {\n cognitiveComplexity: 2,\n ncss: 5,\n nestingDepth: 1,\n depDegree: 10,\n halsteadVolume: 150,\n fileNcss: 20,\n duplicateLines: 0,\n },\n matchSimilarityPercent: 70,\n};\n\n/** One changed file with both revisions measured; the caller resolves git and duplication data. */\nexport interface GateFileInput {\n /** Display path (repository-relative head path; base path for deleted files). */\n file: string;\n /** Absent for added files. */\n baseMetrics?: CodeMetrics;\n /** Absent for deleted files. */\n headMetrics?: CodeMetrics;\n /** Token sequences index-parallel to baseMetrics.functions, for similarity re-matching. */\n baseFunctionTokens?: Int32Array[];\n headFunctionTokens?: Int32Array[];\n /**\n * Head lines the change newly made duplicated (within-file or cross-file), ascending; the\n * caller derives them from the line diff (see findNewlyDuplicatedLines).\n */\n newlyDuplicatedLines: number[];\n /** Other files sharing the clones behind newlyDuplicatedLines, as remediation evidence. */\n duplicationPartners: string[];\n /**\n * false: the file only feeds function matching and the duplication universes (it lies outside\n * the gated target directory); no violations are evaluated or reported for it. Default true.\n */\n gated?: boolean;\n}\n\nexport interface GateViolation {\n gate: 'function-regression' | 'new-function' | 'file-regression' | 'duplication';\n metric: string;\n file: string;\n functionName?: string;\n startLine: number;\n endLine: number;\n /** Absent for new-function and duplication violations, which have no base value. */\n baseValue?: number;\n headValue: number;\n /** The largest value that would have passed. */\n allowedValue: number;\n /** One-line human/agent-readable statement with the remediation direction. */\n message: string;\n}\n\n/** The gated per-function values of one checked (matched or new) function, for `--full` output. */\nexport interface CheckedFunctionReport {\n /** The head file (a cross-file move is reported where the function now lives). */\n file: string;\n name: string;\n startLine: number;\n endLine: number;\n /** Absent for new functions. */\n base?: GateFunctionValues;\n head: GateFunctionValues;\n}\n\nexport interface GateFunctionValues {\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n depDegree: number;\n halsteadVolume: number;\n}\n\nexport interface GateResult {\n violations: GateViolation[];\n checkedFileCount: number;\n checkedFunctionCount: number;\n newFunctionCount: number;\n /** Every checked function with its gated values, ordered by file and line. */\n checkedFunctions: CheckedFunctionReport[];\n}\n\ninterface RatchetMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n /** The growth allowed on top of the base value before the ratchet fires. */\n allowance: (tolerances: GateTolerances, baseValue: number) => number;\n remediation: string;\n format: (value: number) => string;\n}\n\nconst formatInteger = String;\nconst formatFloat = (value: number): string => value.toFixed(1);\n\n/**\n * Gate-eligible per-function metrics (monotone, stable, actionable): a matched function must not\n * worsen any of them beyond its tolerance. \"Your change made this function worse\" is defensible\n * regardless of metric calibration, so no absolute threshold is involved.\n */\nconst ratchetMetrics: RatchetMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n allowance: (tolerances) => tolerances.cognitiveComplexity,\n remediation: 'Simplify the added branching or split the function.',\n format: formatInteger,\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n allowance: (tolerances) => tolerances.ncss,\n remediation: 'Extract the added statements into helper functions or remove them.',\n format: formatInteger,\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n allowance: (tolerances) => tolerances.nestingDepth,\n remediation: 'Flatten the added nesting with early returns or extracted helpers.',\n format: formatInteger,\n },\n {\n metric: 'DepDegree',\n value: (fn) => fn.depDegree,\n allowance: (tolerances) => tolerances.depDegree,\n remediation: 'Reduce how many earlier definitions the code reads, e.g. by splitting the data flow.',\n format: formatInteger,\n },\n {\n metric: 'Halstead volume',\n value: (fn) => fn.halstead.volume,\n // Volume grows as length * log2(vocabulary), so a flat allowance that admits ~5 statements in\n // a small function admits barely one in a large-vocabulary function; the relative term keeps\n // the ~5-statement calibration at every function size.\n allowance: (tolerances, baseValue) => Math.max(tolerances.halsteadVolume, baseValue * 0.25),\n remediation: 'Shrink the function: fewer distinct names and operations.',\n format: formatFloat,\n },\n];\n\ninterface NewFunctionMetric {\n metric: string;\n value: (fn: FunctionMetrics) => number;\n limit: (thresholds: NewFunctionThresholds) => number;\n remediation: string;\n}\n\nconst newFunctionMetrics: NewFunctionMetric[] = [\n {\n metric: 'cognitive complexity',\n value: (fn) => fn.cognitiveComplexity,\n limit: (thresholds) => thresholds.maxCognitiveComplexity,\n remediation: 'Split the new function or simplify its branching.',\n },\n {\n metric: 'NCSS',\n value: (fn) => fn.ncss,\n limit: (thresholds) => thresholds.maxNcss,\n remediation: 'Split the new function into smaller ones.',\n },\n {\n metric: 'max nesting depth',\n value: (fn) => fn.nestingDepth,\n limit: (thresholds) => thresholds.maxNestingDepth,\n remediation: 'Flatten the new function with early returns or extracted helpers.',\n },\n];\n\n/**\n * Evaluates the regression gate over changed files measured at both revisions. Violations are the\n * whole output contract: everything that passes stays silent, so a clean change costs the\n * consuming agent almost no tokens.\n */\nexport function evaluateRegressionGate(files: GateFileInput[], options: GateOptions): GateResult {\n const matching = matchAllFunctions(files, options.matchSimilarityPercent);\n const violations: GateViolation[] = [];\n const checkedFunctions: CheckedFunctionReport[] = [];\n let checkedPairCount = 0;\n let newFunctionCount = 0;\n\n for (const pair of matching.pairs) {\n const file = files[pair.headFileIndex] as GateFileInput;\n if (file.gated === false) {\n continue;\n }\n checkedPairCount += 1;\n violations.push(...checkRatchets(file.file, pair.base, pair.head, options.tolerance));\n checkedFunctions.push(toFunctionReport(file.file, pair.head, pair.base));\n }\n\n let gatedFileCount = 0;\n for (const [fileIndex, file] of files.entries()) {\n if (file.gated === false) {\n continue;\n }\n gatedFileCount += 1;\n const headFunctions = file.headMetrics?.functions ?? [];\n const headMatched = matching.headMatched[fileIndex] as boolean[];\n for (const [functionIndex, fn] of headFunctions.entries()) {\n if (headMatched[functionIndex]) {\n continue;\n }\n newFunctionCount += 1;\n violations.push(...checkNewFunction(file.file, fn, options.newFunction));\n checkedFunctions.push(toFunctionReport(file.file, fn));\n }\n violations.push(...checkFileBackstops(file, hasSplitEvidence(file, fileIndex, matching), violations, options));\n violations.push(...checkDuplication(file, options.tolerance));\n }\n\n violations.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n checkedFunctions.sort((left, right) => left.file.localeCompare(right.file) || left.startLine - right.startLine);\n return {\n violations,\n checkedFileCount: gatedFileCount,\n checkedFunctionCount: checkedPairCount + newFunctionCount,\n newFunctionCount,\n checkedFunctions,\n };\n}\n\n/**\n * Similarity below the match threshold but above this marks a removed function's content as\n * reappearing inside unmatched new code — the signature of a split or evasive rewrite.\n */\nconst splitEvidenceSimilarityPercent = 30;\n\n/**\n * Whether a removed NAMED base function's content partially reappears in an unmatched head\n * function of the same file. This is what arms the file-level backstops: splitting a function to\n * hide a worsening behind the (laxer) new-code thresholds leaves its fragments partially similar\n * to the removed original, while an ordinary \"delete a helper, add an unrelated function\" change\n * shares nothing and must not have its brand-new code re-judged against the file's old aggregates.\n */\nfunction hasSplitEvidence(file: GateFileInput, fileIndex: number, matching: FunctionMatching): boolean {\n const baseFunctions = file.baseMetrics?.functions ?? [];\n const baseMatchedFlags = matching.baseMatched[fileIndex] as boolean[];\n const headMatchedFlags = matching.headMatched[fileIndex] as boolean[];\n const removedNamed = baseFunctions.flatMap((fn, index) =>\n !baseMatchedFlags[index] && fn.name !== undefined ? [index] : []\n );\n if (removedNamed.length === 0) {\n return false;\n }\n const unmatchedHead = (file.headMetrics?.functions ?? []).flatMap((_, index) =>\n headMatchedFlags[index] ? [] : [index]\n );\n if (removedNamed.length * unmatchedHead.length > maxSimilarityComparisons) {\n return false;\n }\n return removedNamed.some((baseIndex) =>\n unmatchedHead.some(\n (headIndex) =>\n tokenSimilarityPercent(\n file.baseFunctionTokens?.[baseIndex],\n file.headFunctionTokens?.[headIndex],\n splitEvidenceSimilarityPercent\n ) >= splitEvidenceSimilarityPercent\n )\n );\n}\n\nfunction toFunctionReport(file: string, head: FunctionMetrics, base?: FunctionMetrics): CheckedFunctionReport {\n return {\n file,\n name: head.name ?? '<anonymous>',\n startLine: head.startLine,\n endLine: head.endLine,\n base: base ? toFunctionValues(base) : undefined,\n head: toFunctionValues(head),\n };\n}\n\nfunction toFunctionValues(fn: FunctionMetrics): GateFunctionValues {\n return {\n cognitiveComplexity: fn.cognitiveComplexity,\n ncss: fn.ncss,\n nestingDepth: fn.nestingDepth,\n depDegree: fn.depDegree,\n halsteadVolume: fn.halstead.volume,\n };\n}\n\n/** One side of a match candidate: a function of one changed file plus its token sequence. */\ninterface IndexedFunction {\n fileIndex: number;\n functionIndex: number;\n fn: FunctionMetrics;\n tokens: Int32Array | undefined;\n}\n\ninterface MatchedPair {\n base: FunctionMetrics;\n head: FunctionMetrics;\n headFileIndex: number;\n}\n\ninterface FunctionMatching {\n pairs: MatchedPair[];\n /** Per file, per function index: whether it found a counterpart (anywhere in the change set). */\n baseMatched: boolean[][];\n headMatched: boolean[][];\n}\n\ninterface SimilarityCandidate {\n similarity: number;\n basePosition: number;\n headPosition: number;\n}\n\n/**\n * Matches head functions to base functions: within each file first by name + arity, then by name\n * alone (signature changes) — resolving ambiguous same-name groups by token similarity instead of\n * list position, so an inserted overload cannot shift the pairing — and finally by\n * normalized-token LCS similarity across ALL changed files (renames and moves between files,\n * reusing the near-miss clone machinery), so refactorings don't appear as delete+add and hit the\n * new-code thresholds.\n */\nfunction matchAllFunctions(files: GateFileInput[], matchSimilarityPercent: number): FunctionMatching {\n const baseMatched = files.map((file) => (file.baseMetrics?.functions ?? []).map(() => false));\n const headMatched = files.map((file) => (file.headMetrics?.functions ?? []).map(() => false));\n const pairs: MatchedPair[] = [];\n\n for (const fileIndex of files.keys()) {\n const pairByKey = (key: (fn: FunctionMetrics) => string | undefined): void => {\n const baseByKey = groupByKey(collectUnmatched(files, baseMatched, 'base', fileIndex), key);\n const headByKey = groupByKey(collectUnmatched(files, headMatched, 'head', fileIndex), key);\n for (const [groupKey, baseGroup] of baseByKey) {\n pairGroup(baseGroup, headByKey.get(groupKey) ?? [], baseMatched, headMatched, pairs);\n }\n };\n pairByKey((fn) => (fn.name === undefined ? undefined : `${fn.name}\\u0000${fn.parameterCount}`));\n pairByKey((fn) => fn.name);\n }\n\n pairAcrossChangeSet(files, baseMatched, headMatched, pairs, matchSimilarityPercent);\n return { pairs, baseMatched, headMatched };\n}\n\n/** Unmatched functions of one file (or of every file when onlyFileIndex is undefined). */\nfunction collectUnmatched(\n files: GateFileInput[],\n matchedByFile: boolean[][],\n side: 'base' | 'head',\n onlyFileIndex?: number\n): IndexedFunction[] {\n const unmatched: IndexedFunction[] = [];\n for (const [fileIndex, file] of files.entries()) {\n if (onlyFileIndex !== undefined && fileIndex !== onlyFileIndex) {\n continue;\n }\n const matched = matchedByFile[fileIndex] as boolean[];\n const metrics = side === 'base' ? file.baseMetrics : file.headMetrics;\n const tokens = side === 'base' ? file.baseFunctionTokens : file.headFunctionTokens;\n for (const [functionIndex, fn] of (metrics?.functions ?? []).entries()) {\n if (!matched[functionIndex]) {\n unmatched.push({ fileIndex, functionIndex, fn, tokens: tokens?.[functionIndex] });\n }\n }\n }\n return unmatched;\n}\n\nfunction groupByKey(\n functions: IndexedFunction[],\n key: (fn: FunctionMetrics) => string | undefined\n): Map<string, IndexedFunction[]> {\n const groups = new Map<string, IndexedFunction[]>();\n for (const indexed of functions) {\n const groupKey = key(indexed.fn);\n if (groupKey === undefined) {\n continue;\n }\n const group = groups.get(groupKey) ?? [];\n group.push(indexed);\n groups.set(groupKey, group);\n }\n return groups;\n}\n\n/** Bounds the quadratic similarity passes; beyond this, leftovers gate as new/removed functions. */\nconst maxSimilarityComparisons = 10_000;\n\n/**\n * Pairs one key group. An unambiguous 1x1 group matches directly; a larger group (same-name\n * methods of different classes, overload sets) is assigned best-similarity-first so an inserted\n * function cannot shift the pairing of the others. The names already match, so any pairing is\n * acceptable: similarity only ORDERS the assignment (no threshold), and without token sequences\n * (or in an oversized group) the stable tie-break reproduces positional order.\n */\nfunction pairGroup(\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n const ambiguous =\n baseGroup.length + headGroup.length > 2 && baseGroup.length * headGroup.length <= maxSimilarityComparisons;\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of baseGroup.entries()) {\n for (const [headPosition, head] of headGroup.entries()) {\n candidates.push({\n similarity: ambiguous ? tokenSimilarityPercent(base.tokens, head.tokens, 0) : 0,\n basePosition,\n headPosition,\n });\n }\n }\n takeMaximumMatches(candidates, baseGroup, headGroup, baseMatched, headMatched, pairs);\n}\n\n/** Cross-file (and remaining within-file) similarity matching over every unmatched function. */\nfunction pairAcrossChangeSet(\n files: GateFileInput[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[],\n matchSimilarityPercent: number\n): void {\n const unmatchedBase = collectUnmatched(files, baseMatched, 'base');\n const unmatchedHead = collectUnmatched(files, headMatched, 'head');\n if (unmatchedBase.length * unmatchedHead.length > maxSimilarityComparisons) {\n return;\n }\n\n const candidates: SimilarityCandidate[] = [];\n for (const [basePosition, base] of unmatchedBase.entries()) {\n for (const [headPosition, head] of unmatchedHead.entries()) {\n const similarity = tokenSimilarityPercent(base.tokens, head.tokens, matchSimilarityPercent);\n if (similarity >= matchSimilarityPercent) {\n candidates.push({ similarity, basePosition, headPosition });\n }\n }\n }\n takeMaximumMatches(candidates, unmatchedBase, unmatchedHead, baseMatched, headMatched, pairs);\n}\n\n/**\n * Assignment over the candidate edges: a greedy best-similarity-first seed, then Kuhn's\n * augmenting paths for the bases the seed left unmatched, so a function whose only counterpart\n * was taken by a slightly better pair is re-matched by rewiring instead of being falsely gated as\n * new code. Augmentation runs only when it increases cardinality, and an exact (100%) seed pair —\n * certain identity — is locked and never rewired even then: trading it for two partial matches\n * would ratchet an untouched function against some other function's rewrite. Non-exact seed pairs\n * may be rewired, but only when that matches strictly more functions (which a maximum-weight\n * assignment would also prefer). Without token sequences every similarity ties at 0 and the seed\n * reproduces positional order.\n */\nfunction takeMaximumMatches(\n candidates: SimilarityCandidate[],\n baseGroup: IndexedFunction[],\n headGroup: IndexedFunction[],\n baseMatched: boolean[][],\n headMatched: boolean[][],\n pairs: MatchedPair[]\n): void {\n candidates.sort(\n (left, right) =>\n right.similarity - left.similarity ||\n left.basePosition - right.basePosition ||\n left.headPosition - right.headPosition\n );\n const adjacency = new Map<number, number[]>();\n const baseOfHead = new Map<number, number>();\n const lockedHeads = new Set<number>();\n const matchedBases = seedGreedyMatches(candidates, adjacency, baseOfHead, lockedHeads);\n for (const basePosition of [...adjacency.keys()].toSorted((left, right) => left - right)) {\n if (!matchedBases.has(basePosition)) {\n tryAugment(basePosition, adjacency, baseOfHead, lockedHeads, new Set());\n }\n }\n\n for (const [headPosition, basePosition] of [...baseOfHead.entries()].toSorted((left, right) => left[0] - right[0])) {\n const base = baseGroup[basePosition] as IndexedFunction;\n const head = headGroup[headPosition] as IndexedFunction;\n (baseMatched[base.fileIndex] as boolean[])[base.functionIndex] = true;\n (headMatched[head.fileIndex] as boolean[])[head.functionIndex] = true;\n pairs.push({ base: base.fn, head: head.fn, headFileIndex: head.fileIndex });\n }\n}\n\n/**\n * Fills the adjacency lists and takes each best edge whose endpoints are both still free; a taken\n * exact (100%) edge locks its head against later rewiring.\n */\nfunction seedGreedyMatches(\n candidates: SimilarityCandidate[],\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>\n): Set<number> {\n const matchedBases = new Set<number>();\n for (const candidate of candidates) {\n const edges = adjacency.get(candidate.basePosition) ?? [];\n edges.push(candidate.headPosition);\n adjacency.set(candidate.basePosition, edges);\n if (!matchedBases.has(candidate.basePosition) && !baseOfHead.has(candidate.headPosition)) {\n baseOfHead.set(candidate.headPosition, candidate.basePosition);\n matchedBases.add(candidate.basePosition);\n if (candidate.similarity === 100) {\n lockedHeads.add(candidate.headPosition);\n }\n }\n }\n return matchedBases;\n}\n\n/** One Kuhn augmentation step: rewires existing (unlocked) matches only when that frees a head for `basePosition`. */\nfunction tryAugment(\n basePosition: number,\n adjacency: Map<number, number[]>,\n baseOfHead: Map<number, number>,\n lockedHeads: Set<number>,\n visitedHeads: Set<number>\n): boolean {\n for (const headPosition of adjacency.get(basePosition) ?? []) {\n if (visitedHeads.has(headPosition) || lockedHeads.has(headPosition)) {\n continue;\n }\n visitedHeads.add(headPosition);\n const currentBase = baseOfHead.get(headPosition);\n if (currentBase === undefined || tryAugment(currentBase, adjacency, baseOfHead, lockedHeads, visitedHeads)) {\n baseOfHead.set(headPosition, basePosition);\n return true;\n }\n }\n return false;\n}\n\n/** Token-LCS similarity of two sequences relative to the longer one, in percent (0 when unusable). */\nfunction tokenSimilarityPercent(\n base: Int32Array | undefined,\n head: Int32Array | undefined,\n minPercent: number\n): number {\n if (!base || !head || base.length === 0 || head.length === 0) {\n return 0;\n }\n // The LCS cannot exceed the shorter sequence, so mismatched lengths are pruned for free.\n const maxLength = Math.max(base.length, head.length);\n if (Math.min(base.length, head.length) * 100 < minPercent * maxLength) {\n return 0;\n }\n return (lcsLength(base, head) * 100) / maxLength;\n}\n\nfunction checkRatchets(\n file: string,\n base: FunctionMetrics,\n head: FunctionMetrics,\n tolerances: GateTolerances\n): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const ratchet of ratchetMetrics) {\n const baseValue = ratchet.value(base);\n const headValue = ratchet.value(head);\n const allowedValue = baseValue + ratchet.allowance(tolerances, baseValue);\n if (headValue > allowedValue) {\n const name = head.name ?? '<anonymous>';\n violations.push({\n gate: 'function-regression',\n metric: ratchet.metric,\n file,\n functionName: name,\n startLine: head.startLine,\n endLine: head.endLine,\n baseValue,\n headValue,\n allowedValue,\n message:\n `${file}:${head.startLine}-${head.endLine} ${name}: ${ratchet.metric} worsened ` +\n `${ratchet.format(baseValue)} -> ${ratchet.format(headValue)} (allowed <= ${ratchet.format(allowedValue)}). ` +\n ratchet.remediation,\n });\n }\n }\n return violations;\n}\n\nfunction checkNewFunction(file: string, fn: FunctionMetrics, thresholds: NewFunctionThresholds): GateViolation[] {\n const violations: GateViolation[] = [];\n for (const check of newFunctionMetrics) {\n const headValue = check.value(fn);\n const allowedValue = check.limit(thresholds);\n if (headValue > allowedValue) {\n const name = fn.name ?? '<anonymous>';\n violations.push({\n gate: 'new-function',\n metric: check.metric,\n file,\n functionName: name,\n startLine: fn.startLine,\n endLine: fn.endLine,\n headValue,\n allowedValue,\n message:\n `${file}:${fn.startLine}-${fn.endLine} new function ${name}: ${check.metric} ${headValue} ` +\n `exceeds the new-code limit ${allowedValue}. ${check.remediation}`,\n });\n }\n }\n return violations;\n}\n\n/**\n * Anti-gaming backstops: splitting a function resets its entity identity and could hide a\n * worsening behind the (laxer) new-code thresholds, so when a removed named function's content\n * reappears in unmatched new code (see hasSplitEvidence) the file-level aggregates ratchet too —\n * the max cognitive complexity per file and the file's total NCSS. Purely additive changes and\n * unrelated remove-plus-add changes stay ungated. The max-cognitive backstop is also skipped when\n * a function-level cognitive violation already reports the file's maximum, since it would only\n * restate it.\n */\nfunction checkFileBackstops(\n file: GateFileInput,\n splitEvidence: boolean,\n reportedViolations: GateViolation[],\n options: GateOptions\n): GateViolation[] {\n const { baseMetrics, headMetrics } = file;\n if (!baseMetrics || !headMetrics || !splitEvidence) {\n return [];\n }\n const violations: GateViolation[] = [];\n const tolerances = options.tolerance;\n\n const maximumAlreadyReported = reportedViolations.some(\n (violation) =>\n violation.file === file.file &&\n violation.metric === 'cognitive complexity' &&\n (violation.gate === 'function-regression' || violation.gate === 'new-function') &&\n violation.headValue === headMetrics.maxCognitiveComplexity\n );\n const allowedMaxCognitive = baseMetrics.maxCognitiveComplexity + tolerances.cognitiveComplexity;\n if (!maximumAlreadyReported && headMetrics.maxCognitiveComplexity > allowedMaxCognitive) {\n const worst = findMostComplexFunction(headMetrics.functions);\n const startLine = worst?.startLine ?? 1;\n const endLine = worst?.endLine ?? headMetrics.lines.total;\n violations.push({\n gate: 'file-regression',\n metric: 'file max cognitive complexity',\n file: file.file,\n functionName: worst?.name ?? undefined,\n startLine,\n endLine,\n baseValue: baseMetrics.maxCognitiveComplexity,\n headValue: headMetrics.maxCognitiveComplexity,\n allowedValue: allowedMaxCognitive,\n message:\n `${file.file}:${startLine}-${endLine}: the file's max cognitive complexity worsened ` +\n `${baseMetrics.maxCognitiveComplexity} -> ${headMetrics.maxCognitiveComplexity} ` +\n `(allowed <= ${allowedMaxCognitive}). Simplify or split the most complex ` +\n `function${worst ? ` (${worst.name ?? '<anonymous>'})` : ''}.`,\n });\n }\n\n const allowedFileNcss = baseMetrics.ncssCount + tolerances.fileNcss;\n if (headMetrics.ncssCount > allowedFileNcss) {\n violations.push({\n gate: 'file-regression',\n metric: 'file NCSS',\n file: file.file,\n startLine: 1,\n endLine: headMetrics.lines.total,\n baseValue: baseMetrics.ncssCount,\n headValue: headMetrics.ncssCount,\n allowedValue: allowedFileNcss,\n message:\n `${file.file}:1-${headMetrics.lines.total}: functions were removed or split while the file grew from NCSS ` +\n `${baseMetrics.ncssCount} to ${headMetrics.ncssCount} (allowed <= ${allowedFileNcss}). ` +\n `Remove the added statements or move genuinely new code into its own module.`,\n });\n }\n\n return violations;\n}\n\nfunction findMostComplexFunction(functions: FunctionMetrics[]): FunctionMetrics | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (!worst || fn.cognitiveComplexity > worst.cognitiveComplexity) {\n worst = fn;\n }\n }\n return worst;\n}\n\n/**\n * No new duplication: the changed lines of a file must not newly duplicate code (within-file, or\n * cross-file against the whole project, so copy-paste from unchanged code is caught where it was\n * pasted).\n */\nfunction checkDuplication(file: GateFileInput, tolerances: GateTolerances): GateViolation[] {\n const lines = file.newlyDuplicatedLines;\n const allowedValue = tolerances.duplicateLines;\n if (lines.length <= allowedValue) {\n return [];\n }\n const startLine = lines[0] as number;\n const endLine = lines.at(-1) as number;\n const partners = file.duplicationPartners.slice(0, 3).join(', ');\n return [\n {\n gate: 'duplication',\n metric: 'newly duplicated lines',\n file: file.file,\n startLine,\n endLine,\n headValue: lines.length,\n allowedValue,\n message:\n `${file.file}:${startLine}-${endLine}: ${lines.length} changed lines duplicate existing code ` +\n `(lines ${formatLineRanges(lines)}; allowed <= ${allowedValue}). Deduplicate` +\n `${partners ? ` against ${partners}` : ' the repeated code'} by extracting a shared helper.`,\n },\n ];\n}\n\n/** Ascending line numbers as compact ranges, e.g. `3-5, 9`. */\nfunction formatLineRanges(lines: number[]): string {\n const ranges: string[] = [];\n let start = lines[0] as number;\n for (const [index, line] of lines.entries()) {\n const next = lines[index + 1];\n if (next !== line + 1) {\n ranges.push(start === line ? String(line) : `${start}-${line}`);\n start = next as number;\n }\n }\n return ranges.join(', ');\n}\n"],"mappings":"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"}
|
package/dist/scan.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");let a=require("node:fs/promises"),o=require("node:path");o=e.__toESM(o,1);let s=require("node:os");s=e.__toESM(s,1);const c=/* @__PURE__ */ new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),l=/* @__PURE__ */ new Set([`__tests__`,`test`,`tests`,`spec`]),u=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,d=/Tests?\.(?:java|kt|cs)$/u;function f(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?o.default.join(s.default.homedir(),e.slice(2)):o.default.resolve(e)}async function p(e){try{return(await(0,a.stat)(e)).isDirectory()?e:o.default.dirname(e)}catch{return o.default.dirname(e)}}const m=s.default.availableParallelism()*2;async function h(e,t){let n=e;try{n=await(0,a.realpath)(e)}catch{}let r=o.default.dirname(n),i;try{i=await(0,a.stat)(n)}catch(e){let t=`${
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./languages.cjs"),r=require("./nativeMetrics.cjs"),i=require("./metrics.cjs");let a=require("node:fs/promises"),o=require("node:path");o=e.__toESM(o,1);let s=require("node:os");s=e.__toESM(s,1);const c=/* @__PURE__ */ new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),l=/* @__PURE__ */ new Set([`__tests__`,`test`,`tests`,`spec`]),u=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,d=/Tests?\.(?:java|kt|cs)$/u;function f(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?o.default.join(s.default.homedir(),e.slice(2)):o.default.resolve(e)}async function p(e){try{return(await(0,a.stat)(e)).isDirectory()?e:o.default.dirname(e)}catch{return o.default.dirname(e)}}const m=s.default.availableParallelism()*2;async function h(e,t){let n=e;try{n=await(0,a.realpath)(e)}catch{}let r=o.default.dirname(n),i;try{i=await(0,a.stat)(n)}catch(e){let t=`${I(n,r)}: ${z(e)}`;return{displayRoot:r,files:[],errors:[t],warnings:[],generatedFiles:[],fatalError:t}}if(i.isFile()){let e=o.default.dirname(n),r=F(n,t,!0);if(!r){let t=`${I(n,e)}: unsupported file type`;return{displayRoot:e,files:[],errors:[t],warnings:[],generatedFiles:[],fatalError:t}}let i=b(t,e);return await D(n,r,`single-file`,i,n),y(i,e)}let s=b(t,n);return await w(n,s),await _(s),y(s,n)}async function g(e,t,n,r=/* @__PURE__ */ new Set){let i=b(n,e);for(let s of t){let t=o.default.join(e,s),c=P(s,n,r.has(t))?F(s,n,r.has(t)):void 0;c&&((await(0,a.lstat)(t).catch(()=>{}))?.isSymbolicLink()||i.candidates.push({file:t,language:c}))}return await _(i),y(i,e)}async function _(e){let t=await e.options.loadExclusion?.(e.candidates.flatMap(({file:e,realFile:t})=>t===void 0?[e]:[e,t]));e.exclusion=t;let n=/* @__PURE__ */ new Map;for(let t of e.candidates){let r;try{r=t.realFile??await(0,a.realpath)(t.file)}catch(n){x(e,t.file,n);continue}n.set(r,[...n.get(r)??[],t])}for(let[r,i]of n){if(e.fatalSeen)return;let n=await v(r,i,t);n!==void 0&&await D(n.file,n.language,`directory`,e,r)}}async function v(e,t,n){if(n===void 0)return t[0];if(n.isExcludedPath(e))return;let r=t.filter(({file:e})=>!n.isExcludedPath(e)),i=r.find(({file:t})=>t===e);if(i!==void 0||r.length<=1)return i??r[0];let o=await(0,a.readFile)(e,`utf8`).catch(()=>{});return r.find(({file:e})=>o!==void 0&&!n.isGeneratedCode(e,o))??r[0]}async function y(e,t){let n=[],r=[],i=[],a=[];for(let o of e.outcomes){let e=await o;if(`fatal`in e){let o=z(e.fatal);return{displayRoot:t,files:n,errors:[...r,o],warnings:i,generatedFiles:a,fatalError:o}}if(`generatedFile`in e){a.push(e.generatedFile);continue}if(`error`in e){r.push(e.error);continue}n.push(e.file),e.warning!==void 0&&i.push(e.warning)}return{displayRoot:t,files:n,errors:r,warnings:i,generatedFiles:a}}function b(e,t){return{options:e,outcomes:[],inFlight:/* @__PURE__ */ new Set,fatalSeen:!1,visitedDirectories:/* @__PURE__ */ new Set,visitedFiles:/* @__PURE__ */ new Set,rootDirectory:t,candidates:[]}}function x(e,t,n){e.outcomes.push({error:`${I(t,e.rootDirectory)}: ${z(n)}`})}async function S(e,t,n){try{return await e()}catch(e){x(n,t,e);return}}async function C(e,t){let n=await S(()=>(0,a.realpath)(e),e,t);return n!==void 0&&N(n,t.rootDirectory)?n:void 0}async function w(e,t){let n=await C(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await S(()=>(0,a.readdir)(e,{withFileTypes:!0}),e,t);if(r!==void 0){r.sort((e,t)=>e.name<t.name?-1:+(e.name>t.name));for(let i of r){let r=o.default.join(e,i.name);if(i.isSymbolicLink()){await T(i.name,r,t);continue}if(i.isDirectory()){if(M(i.name,t.options))continue;await w(r,t);continue}i.isFile()&&E(r,t,r,n===e?void 0:o.default.join(n,i.name))}}}async function T(e,t,n){let r=await C(t,n);if(r===void 0)return;let i=await S(()=>(0,a.stat)(t),t,n);if(i!==void 0){if(i.isDirectory()){if(M(e,n.options)||M(o.default.basename(r),n.options))return;await w(t,n);return}i.isFile()&&E(t,n,r,r)}}function E(e,t,n=e,r){let i=F(n,t.options);i&&t.candidates.push({file:e,language:i,realFile:r})}async function D(e,t,n,r,i){if(r.fatalSeen)return;let o;try{o=i??await(0,a.realpath)(e)}catch(t){x(r,e,t);return}if(r.visitedFiles.has(o))return;for(r.visitedFiles.add(o);r.inFlight.size>=m;)await Promise.race(r.inFlight);let s=O(e,o,t,n,r);r.inFlight.add(s),s.then(()=>r.inFlight.delete(s)),r.outcomes.push(s)}async function O(e,t,n,o,s){try{let r=await(0,a.readFile)(e,`utf8`),c={language:n,duplication:s.options.duplication};if(o===`single-file`)return{file:{file:e,metrics:i.measureCode(r,c)}};let l=s.exclusion;if(l&&(l.isGeneratedCode(e,r)||l.isGeneratedCode(t,r)))return{generatedFile:e};let{metrics:u,crossFileData:d,crossFileError:f}=await k(r,c);return{file:{file:e,metrics:u,duplicationCandidates:d},warning:f===void 0?void 0:`${I(e,s.rootDirectory)}: cross-file duplication candidates unavailable: ${f}`}}catch(t){return t instanceof r.NativeAddonError?(s.fatalSeen=!0,{fatal:t}):{error:`${I(e,s.rootDirectory)}: ${z(t)}`}}}async function k(e,t){try{return await i.measureCodeWithCrossFileDataAsync(e,t)}catch(n){if(n instanceof r.NativeAddonError)throw n;return{metrics:i.measureCode(e,t),crossFileError:z(n)}}}function A(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:I(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function j(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function M(e,t){return c.has(e)?!0:!t.includeTests&&l.has(e)}function N(e,t){let n=o.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${o.default.sep}`)&&!o.default.isAbsolute(n)}function P(e,t,n=!1){if(n)return F(e,t,!0)!==void 0;let r=e.split(`/`);for(let e of r.slice(0,-1))if(c.has(e)||!t.includeTests&&l.has(e))return!1;return F(e,t)!==void 0}function F(e,t,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||t.includeTests||!u.test(o.default.basename(e))&&!d.test(o.default.basename(e))))return n.detectLanguage(e)}function I(e,t){return o.default.relative(t,e)||o.default.basename(e)}function L(e){process.stdout.write(e)}function R(e){process.stderr.write(e)}function z(e){return e instanceof Error?e.message:String(e)}exports.addCrossFileDuplication=A,exports.collectDuplicatedLineNumbers=j,exports.configSearchDirectory=p,exports.formatError=z,exports.formatPath=I,exports.getLanguage=F,exports.isScannedPath=P,exports.measureWithCrossFileData=k,exports.resolveTarget=f,exports.scanListedFiles=g,exports.scanTarget=h,exports.writeStderr=R,exports.writeStdout=L;
|
|
2
2
|
//# sourceMappingURL=scan.cjs.map
|