codemetrics-ai 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +98 -8
  2. package/dist/analyzer.js +35 -27
  3. package/dist/async-population.d.ts +12 -0
  4. package/dist/async-population.js +53 -0
  5. package/dist/async-probe.d.ts +8 -0
  6. package/dist/async-probe.js +52 -0
  7. package/dist/async-scoring.d.ts +3 -0
  8. package/dist/async-scoring.js +33 -0
  9. package/dist/complexity-breakdown.d.ts +21 -0
  10. package/dist/complexity-breakdown.js +30 -0
  11. package/dist/decomposition-evidence.d.ts +91 -0
  12. package/dist/decomposition-evidence.js +71 -0
  13. package/dist/error-handling.d.ts +30 -0
  14. package/dist/error-handling.js +114 -0
  15. package/dist/function-measurements.d.ts +17 -0
  16. package/dist/function-measurements.js +85 -0
  17. package/dist/function-nodes.d.ts +3 -0
  18. package/dist/function-nodes.js +5 -0
  19. package/dist/function-scoring.d.ts +5 -0
  20. package/dist/function-scoring.js +50 -0
  21. package/dist/handler-resolution.d.ts +26 -0
  22. package/dist/handler-resolution.js +99 -0
  23. package/dist/metrics.d.ts +3 -0
  24. package/dist/metrics.js +23 -45
  25. package/dist/module-forwarding.d.ts +4 -0
  26. package/dist/module-forwarding.js +153 -0
  27. package/dist/module-graph.d.ts +34 -0
  28. package/dist/module-graph.js +122 -0
  29. package/dist/module-references.d.ts +8 -0
  30. package/dist/module-references.js +51 -0
  31. package/dist/module-resolution.d.ts +10 -0
  32. package/dist/module-resolution.js +76 -0
  33. package/dist/module-topology.d.ts +72 -0
  34. package/dist/module-topology.js +72 -0
  35. package/dist/react-probe.d.ts +6 -0
  36. package/dist/react-probe.js +60 -0
  37. package/dist/score-arithmetic.d.ts +12 -0
  38. package/dist/score-arithmetic.js +40 -0
  39. package/package.json +4 -3
@@ -0,0 +1,71 @@
1
+ import ts from "typescript";
2
+ import { isFunction } from "./function-nodes.js";
3
+ function ownedShape(root) {
4
+ // A concise function body has an implicit return, independent of formatting.
5
+ let statements = ts.isExpression(root) ? 1 : 0, dataLiteralEntries = 0;
6
+ function visit(node) {
7
+ if (ts.isExpressionWithTypeArguments(node)) {
8
+ visit(node.expression);
9
+ return;
10
+ }
11
+ if (isFunction(node) || ts.isTypeNode(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node) ||
12
+ ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node))
13
+ return;
14
+ if (ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node) || ts.isExportDeclaration(node))
15
+ return;
16
+ if (ts.isVariableStatement(node) && !node.declarationList.declarations.some(declaration => declaration.initializer))
17
+ return;
18
+ if (ts.isStatement(node) && !ts.isBlock(node) && !ts.isEmptyStatement(node) && !ts.isLabeledStatement(node))
19
+ statements++;
20
+ if (ts.isObjectLiteralExpression(node))
21
+ dataLiteralEntries += node.properties.length;
22
+ if (ts.isArrayLiteralExpression(node))
23
+ dataLiteralEntries += node.elements.filter(element => !ts.isOmittedExpression(element)).length;
24
+ ts.forEachChild(node, visit);
25
+ }
26
+ visit(root);
27
+ return { ownedStatements: statements, dataLiteralEntries };
28
+ }
29
+ export function decompositionModule(source, file, project, functions) {
30
+ const byStart = new Map(functions.map(item => [item.sourceSpanStart, item]));
31
+ const rows = [];
32
+ function visit(node) {
33
+ if (isFunction(node) && node.body) {
34
+ const item = byStart.get(node.getStart(source));
35
+ if (item)
36
+ rows.push({ id: item.id, member: item.member, line: item.line, ownedSourceLines: item.ownSourceLines, ...ownedShape(node.body) });
37
+ }
38
+ if (!ts.isTypeNode(node) || ts.isExpressionWithTypeArguments(node))
39
+ ts.forEachChild(node, visit);
40
+ }
41
+ visit(source);
42
+ return { file, project, implementedFunctions: rows.length, sumOwnedFunctionLines: rows.reduce((sum, row) => sum + row.ownedSourceLines, 0),
43
+ sumOwnedFunctionStatements: rows.reduce((sum, row) => sum + row.ownedStatements, 0),
44
+ initializer: ownedShape(source), directReExports: source.statements.filter(node => ts.isExportDeclaration(node) && node.moduleSpecifier).length,
45
+ typeDeclarations: source.statements.filter(node => ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)).length,
46
+ enums: source.statements.filter(ts.isEnumDeclaration).length, functions: rows };
47
+ }
48
+ export function decompositionEvidence(modules, incomplete) {
49
+ const ordered = [...modules].sort((a, b) => a.file.localeCompare(b.file, "en") || a.project.localeCompare(b.project, "en"));
50
+ const functions = ordered.flatMap(module => module.functions.map(row => ({ ...row, file: module.file, project: module.project })));
51
+ const sizes = functions.map(row => row.ownedSourceLines).sort((a, b) => a - b);
52
+ const sum = sizes.reduce((total, size) => total + size, 0);
53
+ const statements = functions.map(row => row.ownedStatements).sort((a, b) => a - b);
54
+ const statementSum = statements.reduce((total, size) => total + size, 0);
55
+ return { version: 2, status: incomplete ? "failed" : "unscored", score: null,
56
+ measurement: "owned-executable-statements-v2", primaryMeasure: "ownedStatements",
57
+ basis: "Descriptive size evidence; no responsibility inference, threshold, finding or score contribution.",
58
+ exclusions: ["nested-function-double-counting", "class-and-enum-initializers", "parameter-initializers", "semantic-responsibility"],
59
+ population: { modules: ordered.length, functions: functions.length, sumOwnedFunctionLines: sum,
60
+ sumOwnedFunctionStatements: statementSum,
61
+ medianFunctionStatements: statements.length ? statements[Math.ceil(statements.length * 0.5) - 1] : null,
62
+ p90FunctionStatements: statements.length ? statements[Math.ceil(statements.length * 0.9) - 1] : null,
63
+ medianFunctionLines: sizes.length ? sizes[Math.ceil(sizes.length * 0.5) - 1] : null,
64
+ p90FunctionLines: sizes.length ? sizes[Math.ceil(sizes.length * 0.9) - 1] : null }, modules: ordered,
65
+ largestFunctions: [...functions].sort((a, b) => b.ownedStatements - a.ownedStatements || a.id.localeCompare(b.id, "en")).slice(0, 10),
66
+ largestFunctionContainers: ordered.filter(module => module.sumOwnedFunctionStatements > 0)
67
+ .sort((a, b) => b.sumOwnedFunctionStatements - a.sumOwnedFunctionStatements || a.file.localeCompare(b.file, "en")).slice(0, 10)
68
+ .map(({ functions: _functions, ...module }) => ({ ...module,
69
+ shareOfOwnedFunctionStatements: statementSum ? module.sumOwnedFunctionStatements / statementSum : null,
70
+ shareOfOwnedFunctionLines: sum ? module.sumOwnedFunctionLines / sum : null })) };
71
+ }
@@ -0,0 +1,30 @@
1
+ import ts from "typescript";
2
+ import { type Dimension } from "./evidence.js";
3
+ import { HandlerResolver } from "./handler-resolution.js";
4
+ type UsageKind = "catchClause" | "promiseCatch" | "promiseThenRejection";
5
+ interface Handler {
6
+ id: string;
7
+ file: string;
8
+ project: string;
9
+ line: number;
10
+ kind: "catchClause" | "promiseRejection";
11
+ classification: "unexplainedEmpty" | "documentedEmpty" | "containsCode";
12
+ comments: string[];
13
+ uses: {
14
+ file: string;
15
+ line: number;
16
+ kind: UsageKind;
17
+ resolution: "inline" | "reference";
18
+ }[];
19
+ }
20
+ export declare function inspectErrorHandling(source: ts.SourceFile, checker: ts.TypeChecker, file: string, project: string, resolver: HandlerResolver): {
21
+ handlers: Handler[];
22
+ uninspectedCallbacks: {
23
+ file: string;
24
+ line: number;
25
+ kind: UsageKind;
26
+ reason: string;
27
+ }[];
28
+ };
29
+ export declare function errorHandlingEvidence(results: ReturnType<typeof inspectErrorHandling>[], incomplete: boolean): Dimension;
30
+ export {};
@@ -0,0 +1,114 @@
1
+ import ts from "typescript";
2
+ import { hash } from "./evidence.js";
3
+ import { unwrapHandler } from "./handler-resolution.js";
4
+ function undefinedValue(node, checker) {
5
+ node = unwrapHandler(node);
6
+ if (ts.isVoidExpression(node))
7
+ return ts.isNumericLiteral(node.expression);
8
+ if (!ts.isIdentifier(node) || node.text !== "undefined")
9
+ return false;
10
+ const declarations = checker.getSymbolAtLocation(node)?.declarations ?? [];
11
+ return declarations.length === 0 || declarations.every(declaration => declaration.getSourceFile().hasNoDefaultLib);
12
+ }
13
+ function emptyBody(body, checker) {
14
+ if (!ts.isBlock(body))
15
+ return undefinedValue(body, checker);
16
+ return body.statements.every(statement => ts.isEmptyStatement(statement) ||
17
+ (ts.isReturnStatement(statement) && (!statement.expression || undefinedValue(statement.expression, checker))));
18
+ }
19
+ function bodyComments(body, source) {
20
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, source.languageVariant, source.text.slice(body.pos, body.end));
21
+ const comments = [];
22
+ for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
23
+ if (token !== ts.SyntaxKind.SingleLineCommentTrivia && token !== ts.SyntaxKind.MultiLineCommentTrivia)
24
+ continue;
25
+ const text = scanner.getTokenText().replace(/^\/\/?\*?|\*\/$/g, "").trim();
26
+ if (/[\p{L}\p{N}]/u.test(text))
27
+ comments.push(text);
28
+ }
29
+ return comments;
30
+ }
31
+ function rejectionArgument(call, checker) {
32
+ const declaration = checker.getResolvedSignature(call)?.declaration;
33
+ if (!declaration || !ts.isMethodSignature(declaration) || !ts.isIdentifier(declaration.name) ||
34
+ !ts.isInterfaceDeclaration(declaration.parent) || !["Promise", "PromiseLike"].includes(declaration.parent.name.text))
35
+ return;
36
+ const source = declaration.getSourceFile();
37
+ if (!source.hasNoDefaultLib || !/(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/.test(source.fileName))
38
+ return;
39
+ if (declaration.name.text === "catch")
40
+ return { index: 0, kind: "promiseCatch" };
41
+ if (declaration.name.text === "then" && call.arguments.length > 1)
42
+ return { index: 1, kind: "promiseThenRejection" };
43
+ }
44
+ export function inspectErrorHandling(source, checker, file, project, resolver) {
45
+ const handlers = new Map();
46
+ const uninspectedCallbacks = [];
47
+ function add(body, kind, at, resolution) {
48
+ const identity = resolver.identity(body);
49
+ const existing = handlers.get(identity.id);
50
+ const use = { file, line: source.getLineAndCharacterOfPosition(at.getStart(source)).line + 1, kind, resolution };
51
+ if (existing) {
52
+ existing.uses.push(use);
53
+ return;
54
+ }
55
+ const empty = emptyBody(body, checker), comments = empty ? bodyComments(body, body.getSourceFile()) : [];
56
+ const classification = !empty ? "containsCode" : comments.length ? "documentedEmpty" : "unexplainedEmpty";
57
+ handlers.set(identity.id, { ...identity, kind: kind === "catchClause" ? "catchClause" : "promiseRejection", classification, comments, uses: [use] });
58
+ }
59
+ function visit(node) {
60
+ if (ts.isTypeNode(node) && !ts.isExpressionWithTypeArguments(node))
61
+ return;
62
+ if (ts.isCatchClause(node))
63
+ add(node.block, "catchClause", node, "inline");
64
+ if (ts.isCallExpression(node)) {
65
+ const rejection = rejectionArgument(node, checker);
66
+ if (rejection) {
67
+ const resolved = resolver.resolve(node.arguments[rejection.index]);
68
+ if ("body" in resolved)
69
+ add(resolved.body, rejection.kind, node, resolved.resolution);
70
+ else
71
+ uninspectedCallbacks.push({ file, line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
72
+ kind: rejection.kind, reason: resolved.reason });
73
+ }
74
+ }
75
+ ts.forEachChild(node, visit);
76
+ }
77
+ visit(source);
78
+ return { handlers: [...handlers.values()], uninspectedCallbacks };
79
+ }
80
+ function handlerFinding(handler) {
81
+ return { category: "unexplainedEmptyHandler", ruleId: "javascript-typescript/errorHandling/unexplainedEmptyHandler",
82
+ fingerprint: hash(`${handler.id}|unexplainedEmptyHandler`), file: handler.file, project: handler.project, line: handler.line,
83
+ severity: "info", confidence: "high",
84
+ message: "Handler contains only empty syntax or an undefined return, without a body comment. Review whether discarding the failure is intentional.",
85
+ observations: { handlerId: handler.id, kind: handler.kind, uses: handler.uses, classification: "reviewLead", scoreDisposition: "excludedUncalibrated" } };
86
+ }
87
+ export function errorHandlingEvidence(results, incomplete) {
88
+ const unique = new Map();
89
+ for (const row of results.flatMap(result => result.handlers)) {
90
+ const existing = unique.get(row.id);
91
+ if (existing)
92
+ existing.uses.push(...row.uses);
93
+ else
94
+ unique.set(row.id, { ...row, uses: [...row.uses] });
95
+ }
96
+ const handlers = [...unique.values()].sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
97
+ for (const handler of handlers)
98
+ handler.uses.sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line || a.kind.localeCompare(b.kind, "en"));
99
+ const uninspectedCallbacks = results.flatMap(result => result.uninspectedCallbacks)
100
+ .sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
101
+ const count = (classification) => handlers.filter(handler => handler.classification === classification).length;
102
+ const unexplained = count("unexplainedEmpty");
103
+ return { status: incomplete ? "failed" : "skipped", basis: incomplete ? "Incomplete source; handler observations are diagnostic only." :
104
+ "Handler syntax and documented intent are observed; error-handling scoring is not calibrated. Code presence is not proof of recovery.",
105
+ scope: { id: "javascript-typescript/errorHandling/handler-evidence-v2", coverage: "partial",
106
+ includes: ["catch-clauses", "inline-standard-promise-rejection-handlers", "body-comments", "selected-source-referenced-handlers"],
107
+ excludes: ["error-handling-score", "general-exception-flow", "arbitrary-callback-value-flow", "correctness-of-recovery", "semantic-rationale-judgment"] },
108
+ findings: handlers.filter(handler => handler.classification === "unexplainedEmpty").map(handlerFinding), handlerEvidence: { version: 2, countingUnit: "distinctHandlerBody",
109
+ totalHandlers: handlers.length, unexplainedEmptyHandlers: unexplained, documentedEmptyHandlers: count("documentedEmpty"),
110
+ handlersContainingCode: count("containsCode"), unexplainedEmptyPercent: handlers.length ? 100 * unexplained / handlers.length : null,
111
+ handlerUseSites: handlers.reduce((sum, handler) => sum + handler.uses.length, 0),
112
+ referencedCallbackUseSites: handlers.reduce((sum, handler) => sum + handler.uses.filter(use => use.resolution === "reference").length, 0),
113
+ uninspectedRejectionCallbacks: uninspectedCallbacks.length, uninspectedCallbacks, handlers } };
114
+ }
@@ -0,0 +1,17 @@
1
+ import ts from "typescript";
2
+ import { type FunctionNode } from "./function-nodes.js";
3
+ import { type ComplexityBreakdown } from "./complexity-breakdown.js";
4
+ export interface FunctionMeasurement {
5
+ id: string;
6
+ file: string;
7
+ project: string;
8
+ member: string;
9
+ line: number;
10
+ sourceSpanStart: number;
11
+ ownComplexity: number;
12
+ ownSourceLines: number;
13
+ ownHalsteadVolume: number;
14
+ ownMaintainabilityIndex: number;
15
+ complexityBreakdown?: ComplexityBreakdown;
16
+ }
17
+ export declare function measureFunction(node: FunctionNode, source: ts.SourceFile, file: string, project: string, member: string): FunctionMeasurement;
@@ -0,0 +1,85 @@
1
+ import ts from "typescript";
2
+ import { hash } from "./evidence.js";
3
+ import { isFunction } from "./function-nodes.js";
4
+ import { complexityBreakdown, decisionCategory, emptyDecisionCounts } from "./complexity-breakdown.js";
5
+ function excluded(node) {
6
+ return isFunction(node) || ts.isTypeNode(node) || ts.isInterfaceDeclaration(node) ||
7
+ ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isEnumDeclaration(node);
8
+ }
9
+ function assertedExpression(node) {
10
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node) ||
11
+ ts.isNonNullExpression(node) || ts.isExpressionWithTypeArguments(node))
12
+ return node.expression;
13
+ return undefined;
14
+ }
15
+ function runtimeChildren(node, source) {
16
+ const children = node.getChildren(source);
17
+ const typed = node;
18
+ const argumentsIndex = typed.typeArguments ? children.findIndex(child => child.kind === ts.SyntaxKind.SyntaxList &&
19
+ child.pos === typed.typeArguments.pos && child.end === typed.typeArguments.end) : -1;
20
+ return children.filter((child, index) => {
21
+ // Delimiters and commas around erased type arguments are not runtime tokens.
22
+ if (argumentsIndex >= 0 && index >= argumentsIndex - 1 && index <= argumentsIndex + 1)
23
+ return false;
24
+ if (typed.type && (child === typed.type || (child.kind === ts.SyntaxKind.ColonToken && children[index + 1] === typed.type)))
25
+ return false;
26
+ return child !== typed.exclamationToken;
27
+ });
28
+ }
29
+ // This scored measurement is deliberately separate from the historical CSV metric.
30
+ export function measureFunction(node, source, file, project, member) {
31
+ const body = node.body;
32
+ const decisionCounts = emptyDecisionCounts();
33
+ const operators = [], operands = [], lines = new Set();
34
+ function decisions(current) {
35
+ const expression = assertedExpression(current);
36
+ if (expression) {
37
+ decisions(expression);
38
+ return;
39
+ }
40
+ if (excluded(current))
41
+ return;
42
+ const category = decisionCategory(current);
43
+ if (category)
44
+ decisionCounts[category]++;
45
+ ts.forEachChild(current, decisions);
46
+ }
47
+ function tokens(current) {
48
+ const expression = assertedExpression(current);
49
+ if (expression) {
50
+ tokens(expression);
51
+ return;
52
+ }
53
+ if (excluded(current) || ts.isJsxText(current))
54
+ return;
55
+ const children = runtimeChildren(current, source);
56
+ if (children.length) {
57
+ children.forEach(tokens);
58
+ return;
59
+ }
60
+ if (current.kind === ts.SyntaxKind.EndOfFileToken || current.getWidth(source) === 0)
61
+ return;
62
+ const text = current.getText(source);
63
+ if (!["{", "}", ";"].includes(text)) {
64
+ const start = source.getLineAndCharacterOfPosition(current.getStart(source)).line;
65
+ const end = source.getLineAndCharacterOfPosition(current.end - 1).line;
66
+ for (let line = start; line <= end; line++)
67
+ lines.add(line);
68
+ }
69
+ if (ts.isIdentifier(current) || ts.isLiteralExpression(current))
70
+ operands.push(text);
71
+ else
72
+ operators.push(text);
73
+ }
74
+ decisions(body);
75
+ const breakdown = complexityBreakdown(decisionCounts);
76
+ const complexity = breakdown.total;
77
+ tokens(body);
78
+ const vocabulary = new Set(operators).size + new Set(operands).size;
79
+ const volume = vocabulary > 1 ? (operators.length + operands.length) * Math.log2(vocabulary) : 0;
80
+ const mi = lines.size === 0 ? 100 : Math.min(100, Math.max(0, (171 - 5.2 * Math.log(Math.max(volume, 1)) - 0.23 * complexity - 16.2 * Math.log(Math.max(lines.size, 1))) * 100 / 171));
81
+ const start = node.getStart(source);
82
+ return { id: hash(`${file}|${start}|${node.kind}`), file, project, member, sourceSpanStart: start,
83
+ line: source.getLineAndCharacterOfPosition(start).line + 1, ownComplexity: complexity,
84
+ ownSourceLines: lines.size, ownHalsteadVolume: volume, ownMaintainabilityIndex: mi, complexityBreakdown: breakdown };
85
+ }
@@ -0,0 +1,3 @@
1
+ import ts from "typescript";
2
+ export type FunctionNode = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration;
3
+ export declare function isFunction(node: ts.Node): node is FunctionNode;
@@ -0,0 +1,5 @@
1
+ import ts from "typescript";
2
+ export function isFunction(node) {
3
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) ||
4
+ ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
5
+ }
@@ -0,0 +1,5 @@
1
+ import { type Dimension, type Finding } from "./evidence.js";
2
+ import type { FunctionMeasurement } from "./function-measurements.js";
3
+ export declare const complexityAnchors: readonly [readonly [3, 10], readonly [5, 8], readonly [10, 6], readonly [20, 4], readonly [40, 0]];
4
+ export declare const maintainabilityAnchors: readonly [readonly [40, 0], readonly [52, 2], readonly [58, 4], readonly [65, 6], readonly [70, 8], readonly [75, 10]];
5
+ export declare function scoreFunctions(functions: FunctionMeasurement[], dimension: "codeQuality" | "maintainability", findings?: Finding[]): Dimension;
@@ -0,0 +1,50 @@
1
+ import { scored } from "./evidence.js";
2
+ import { add, decimal, fraction, interpolate, multiply, numeric, roundScore, subtract } from "./score-arithmetic.js";
3
+ export const complexityAnchors = [[3, 10], [5, 8], [10, 6], [20, 4], [40, 0]];
4
+ export const maintainabilityAnchors = [[40, 0], [52, 2], [58, 4], [65, 6], [70, 8], [75, 10]];
5
+ export function scoreFunctions(functions, dimension, findings = []) {
6
+ const complexity = dimension === "codeQuality";
7
+ const anchors = complexity ? complexityAnchors : maintainabilityAnchors;
8
+ const measured = (item) => complexity ? item.ownComplexity : item.ownMaintainabilityIndex;
9
+ const scope = { id: `javascript-typescript/${dimension}/v2`, coverage: "partial",
10
+ includes: [complexity ? "owned-function-cyclomatic-complexity" : "owned-function-maintainability-index"],
11
+ excludes: ["module-and-field-initializers", "parameter-initializers", "runtime-behavior", complexity ? "decomposition" : "change-cost"] };
12
+ if (!functions.length)
13
+ return { status: "skipped", basis: "No implemented functions; function score is unmeasured.", findings, scope };
14
+ const ordered = functions.map(item => ({ item, score: interpolate(measured(item), anchors) }))
15
+ .sort((a, b) => (complexity ? measured(b.item) - measured(a.item) : measured(a.item) - measured(b.item)) ||
16
+ (a.item.id < b.item.id ? -1 : a.item.id > b.item.id ? 1 : 0));
17
+ const weakCount = complexity ? 1 : Math.ceil(ordered.length / 5);
18
+ const restCount = ordered.length - weakCount;
19
+ const weakWeight = restCount ? fraction(2n, 5n * BigInt(weakCount)) : fraction(1n, BigInt(weakCount));
20
+ const restWeight = restCount ? fraction(3n, 5n * BigInt(restCount)) : decimal(0);
21
+ const losses = { weakest: decimal(0), remainder: decimal(0) };
22
+ const contributions = ordered.map(({ item, score }, index) => {
23
+ const group = index < weakCount ? "weakest" : "remainder";
24
+ const weight = index < weakCount ? weakWeight : restWeight;
25
+ const weightedLoss = multiply(subtract(decimal(10), score), weight);
26
+ losses[group] = add(losses[group], weightedLoss);
27
+ return { ...item, group, individualScore: numeric(score), weightNumerator: Number(weight.numerator),
28
+ weightDenominator: Number(weight.denominator), weightedScoreLoss: numeric(weightedLoss) };
29
+ });
30
+ const total = subtract(decimal(10), add(losses.weakest, losses.remainder));
31
+ const decision = { version: 1, policy: `javascript-typescript/${dimension}/owned-functions-v2`, operation: "deductions",
32
+ finalScore: roundScore(total), inputs: { baseline: 10, eligibleFunctions: ordered.length, weakCount, restCount,
33
+ anchors, aggregation: complexity ? "worst-40-remainder-60" : "weakest-quintile-40-remainder-60",
34
+ rounding: "decimal-rational-half-up-one-decimal", unroundedScore: numeric(total) },
35
+ steps: (restCount ? ["weakest", "remainder"] : ["weakest"]).map(group => ({ id: group, kind: "component", score: numeric(losses[group]),
36
+ disposition: "applied", inputs: { weightedScoreLoss: numeric(losses[group]), functionCount: group === "weakest" ? weakCount : restCount },
37
+ findingCategories: complexity ? ["highFunctionComplexity"] : [] })), findingEffects: [] };
38
+ const result = scored(decision, `${complexity ? "Method complexity: one worst function" : "Maintainability: weakest ceil(N/5) functions"} receives 40%; remaining functions receive 60%. Single-function scopes use their own score. JS/TS remains uncalibrated across ecosystems.`, findings, { ...decision.inputs, measurement: "owned-function-body-v2-type-erasure", functionContributions: contributions, topOffenders: contributions.slice(0, 5) });
39
+ // Findings identify population inputs, never additional independent penalties.
40
+ const byId = new Map(contributions.map(item => [item.id, item.group]));
41
+ decision.findingEffects = result.findings.map(finding => {
42
+ const group = byId.get(String(finding.observations.functionId));
43
+ return { fingerprint: finding.fingerprint, category: finding.category,
44
+ effect: group ? "policyInput" : "noAdditionalReduction", stepIds: group ? [group] : [] };
45
+ });
46
+ return { ...result, scope, ...(complexity ? { displayName: "Method Complexity", componentDetails: {
47
+ methodComplexity: { score: result.score, eligibleFunctions: ordered.length, topOffenders: contributions.slice(0, 5) },
48
+ decomposition: { score: null, status: "unsupported", description: "JS/TS decomposition is not measured by this policy." }
49
+ } } : {}) };
50
+ }
@@ -0,0 +1,26 @@
1
+ import ts from "typescript";
2
+ export declare function unwrapHandler(node: ts.Expression): ts.Expression;
3
+ type Resolution = {
4
+ body: ts.ConciseBody;
5
+ resolution: "inline" | "reference";
6
+ } | {
7
+ reason: string;
8
+ };
9
+ /** Bounded declaration lookup, not a runtime value-flow or mutation proof. */
10
+ export declare class HandlerResolver {
11
+ private readonly checker;
12
+ private readonly selected;
13
+ private readonly written;
14
+ private readonly identities;
15
+ constructor(checker: ts.TypeChecker, sources: ts.SourceFile[], root: string, project: string);
16
+ private recordWrite;
17
+ identity(body: ts.ConciseBody): {
18
+ id: string;
19
+ file: string;
20
+ project: string;
21
+ line: number;
22
+ };
23
+ resolve(argument: ts.Expression | undefined): Resolution;
24
+ private resolveReference;
25
+ }
26
+ export {};
@@ -0,0 +1,99 @@
1
+ import ts from "typescript";
2
+ import path from "node:path";
3
+ import { hash } from "./evidence.js";
4
+ import { isFunction } from "./function-nodes.js";
5
+ export function unwrapHandler(node) {
6
+ while (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) ||
7
+ ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node))
8
+ node = node.expression;
9
+ return node;
10
+ }
11
+ /** Bounded declaration lookup, not a runtime value-flow or mutation proof. */
12
+ export class HandlerResolver {
13
+ checker;
14
+ selected = new Set();
15
+ written = new Set();
16
+ identities = new Map();
17
+ constructor(checker, sources, root, project) {
18
+ this.checker = checker;
19
+ for (const source of sources) {
20
+ this.selected.add(source);
21
+ const file = path.relative(root, source.fileName).replaceAll("\\", "/");
22
+ let functions = 0, catches = 0;
23
+ const visit = (node) => {
24
+ if (ts.isTypeNode(node) && !ts.isExpressionWithTypeArguments(node))
25
+ return;
26
+ const body = isFunction(node) ? node.body : ts.isCatchClause(node) ? node.block : undefined;
27
+ if (body) {
28
+ const key = ts.isCatchClause(node) ? `catch:${catches++}` : `function:${functions++}`;
29
+ this.identities.set(body, { id: hash(`${project}|${file}|${key}`), file, project,
30
+ line: source.getLineAndCharacterOfPosition(body.getStart(source)).line + 1 });
31
+ }
32
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
33
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
34
+ this.recordWrite(node.left);
35
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
36
+ [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator))
37
+ this.recordWrite(node.operand);
38
+ if ((ts.isForInStatement(node) || ts.isForOfStatement(node)) && !ts.isVariableDeclarationList(node.initializer))
39
+ this.recordWrite(node.initializer);
40
+ ts.forEachChild(node, visit);
41
+ };
42
+ visit(source);
43
+ }
44
+ }
45
+ recordWrite(node) {
46
+ if (ts.isIdentifier(node)) {
47
+ const symbol = this.checker.getSymbolAtLocation(node);
48
+ if (symbol)
49
+ this.written.add(symbol);
50
+ }
51
+ else if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) {
52
+ // Destructuring and parenthesized assignments are conservatively included.
53
+ ts.forEachChild(node, child => this.recordWrite(child));
54
+ }
55
+ }
56
+ identity(body) { return this.identities.get(body); }
57
+ resolve(argument) {
58
+ if (!argument)
59
+ return { reason: "missingArgument" };
60
+ const direct = unwrapHandler(argument);
61
+ if (isFunction(direct) && direct.body)
62
+ return { body: direct.body, resolution: "inline" };
63
+ return this.resolveReference(direct, new Set());
64
+ }
65
+ resolveReference(node, seen) {
66
+ node = unwrapHandler(node);
67
+ if (!ts.isIdentifier(node))
68
+ return { reason: "unsupportedExpression" };
69
+ let symbol = this.checker.getSymbolAtLocation(node);
70
+ if (!symbol)
71
+ return { reason: "unresolvedSymbol" };
72
+ if (this.written.has(symbol))
73
+ return { reason: "reassignedBinding" };
74
+ if (symbol.flags & ts.SymbolFlags.Alias)
75
+ symbol = this.checker.getAliasedSymbol(symbol);
76
+ if (this.written.has(symbol))
77
+ return { reason: "reassignedBinding" };
78
+ if (seen.has(symbol) || seen.size >= 32)
79
+ return { reason: "aliasCycleOrDepth" };
80
+ seen.add(symbol);
81
+ const declarations = symbol.declarations ?? [];
82
+ if (!declarations.length || declarations.some(declaration => !this.selected.has(declaration.getSourceFile())))
83
+ return { reason: "outsideSelectedSource" };
84
+ const implementations = declarations.filter(declaration => ts.isFunctionDeclaration(declaration) && declaration.body);
85
+ if (implementations.length === 1)
86
+ return { body: implementations[0].body, resolution: "reference" };
87
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]))
88
+ return { reason: "unsupportedDeclaration" };
89
+ const declaration = declarations[0];
90
+ if (!ts.isVariableDeclarationList(declaration.parent) || !(declaration.parent.flags & ts.NodeFlags.Const))
91
+ return { reason: "mutableBinding" };
92
+ if (!declaration.initializer)
93
+ return { reason: "missingInitializer" };
94
+ const value = unwrapHandler(declaration.initializer);
95
+ if (isFunction(value) && value.body)
96
+ return { body: value.body, resolution: "reference" };
97
+ return this.resolveReference(value, seen);
98
+ }
99
+ }
package/dist/metrics.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { type Finding } from "./evidence.js";
3
+ import { type FunctionMeasurement } from "./function-measurements.js";
3
4
  export interface Metric {
4
5
  project: string;
5
6
  file: string;
@@ -18,9 +19,11 @@ export interface Metric {
18
19
  export declare function maintainability(volume: number, complexity: number, lines: number): number;
19
20
  export declare function analyzeFile(source: ts.SourceFile, checker: ts.TypeChecker, project: string, root: string): {
20
21
  metrics: Metric[];
22
+ functions: FunctionMeasurement[];
21
23
  findings: {
22
24
  dimension: "codeQuality" | "performanceAsync";
23
25
  finding: Finding;
24
26
  }[];
25
27
  reactSupported: boolean;
28
+ asyncSupported: boolean;
26
29
  };