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,72 @@
1
+ // Iterative SCC traversal avoids stack overflow on long import chains and emits
2
+ // one cyclic component, not an exponential list of individual cycle paths.
3
+ export function cyclicComponents(graph) {
4
+ const visited = new Set(), order = [];
5
+ for (const root of graph.keys()) {
6
+ const stack = [[root, false]];
7
+ while (stack.length) {
8
+ const [node, finishing] = stack.pop();
9
+ if (finishing) {
10
+ order.push(node);
11
+ continue;
12
+ }
13
+ if (visited.has(node))
14
+ continue;
15
+ visited.add(node);
16
+ stack.push([node, true]);
17
+ for (const target of graph.get(node) ?? [])
18
+ if (!visited.has(target))
19
+ stack.push([target, false]);
20
+ }
21
+ }
22
+ const reverse = new Map([...graph.keys()].map(node => [node, new Set()]));
23
+ for (const [node, targets] of graph)
24
+ for (const target of targets)
25
+ reverse.get(target).add(node);
26
+ visited.clear();
27
+ const components = [];
28
+ for (const root of order.reverse()) {
29
+ if (visited.has(root))
30
+ continue;
31
+ const component = [], stack = [root];
32
+ visited.add(root);
33
+ while (stack.length) {
34
+ const node = stack.pop();
35
+ component.push(node);
36
+ for (const target of reverse.get(node) ?? [])
37
+ if (!visited.has(target)) {
38
+ visited.add(target);
39
+ stack.push(target);
40
+ }
41
+ }
42
+ if (component.length > 1 || graph.get(root)?.has(root))
43
+ components.push(component.sort());
44
+ }
45
+ return components.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
46
+ }
47
+ function referenceView(ids, edges) {
48
+ const graph = new Map(ids.map(id => [id, new Set()]));
49
+ const incoming = new Map(ids.map(id => [id, new Set()]));
50
+ for (const edge of edges)
51
+ if (edge.resolution === "internal" && edge.usage !== "typeOnly" && edge.target) {
52
+ graph.get(edge.from).add(edge.target);
53
+ incoming.get(edge.target).add(edge.from);
54
+ }
55
+ const modules = ids.map(id => ({ id, internalValueFanOut: graph.get(id).size, internalValueFanIn: incoming.get(id).size }));
56
+ const rank = (key) => modules.filter(item => item[key] > 0)
57
+ .sort((a, b) => b[key] - a[key] || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).slice(0, 10);
58
+ return { graph, modules, internalValueEdgeCount: modules.reduce((sum, node) => sum + node.internalValueFanOut, 0),
59
+ highestFanOut: rank("internalValueFanOut"), highestFanIn: rank("internalValueFanIn") };
60
+ }
61
+ export function moduleTopology(ids, edges) {
62
+ const { graph, internalValueEdgeCount, ...overall } = referenceView(ids, edges);
63
+ const { graph: _imports, ...implementation } = referenceView(ids, edges.filter(edge => edge.roles.includes("implementation")));
64
+ const { graph: _exports, ...reExports } = referenceView(ids, edges.filter(edge => edge.roles.includes("reExport")));
65
+ return { ...overall, cycles: cyclicComponents(graph), dependencyViews: {
66
+ version: 2, classification: "explicit-forwarding-v2",
67
+ implementation: { basis: "Imports/loads with local uses or no recognized forwarding; unsupported alias flow remains here.", ...implementation },
68
+ reExports: { basis: "Direct re-exports and explicit forwarding through an unmodified local import/require binding or direct require call.", ...reExports },
69
+ totalInternalValueEdgeCount: internalValueEdgeCount,
70
+ sharedInternalValueEdgeCount: implementation.internalValueEdgeCount + reExports.internalValueEdgeCount - internalValueEdgeCount
71
+ } };
72
+ }
@@ -0,0 +1,6 @@
1
+ import ts from "typescript";
2
+ import type { Finding } from "./evidence.js";
3
+ type ReactObservation = Pick<Finding, "category" | "message" | "observations" | "confidence">;
4
+ export declare function importedHookName(call: ts.CallExpression, checker: ts.TypeChecker): string | undefined;
5
+ export declare function inspectReactCall(call: ts.CallExpression, body: ts.ConciseBody, checker: ts.TypeChecker): ReactObservation[];
6
+ export {};
@@ -0,0 +1,60 @@
1
+ import ts from "typescript";
2
+ import { isFunction } from "./function-nodes.js";
3
+ function isReactImport(declaration) {
4
+ if (ts.isImportSpecifier(declaration) && declaration.isTypeOnly)
5
+ return false;
6
+ let cursor = declaration;
7
+ while (cursor && !ts.isImportDeclaration(cursor))
8
+ cursor = cursor.parent;
9
+ return !!cursor && !cursor.importClause?.isTypeOnly && ts.isStringLiteral(cursor.moduleSpecifier) && cursor.moduleSpecifier.text === "react";
10
+ }
11
+ export function importedHookName(call, checker) {
12
+ const symbol = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression);
13
+ const declarations = symbol?.declarations ?? [];
14
+ const reactHook = declarations.some(isReactImport);
15
+ const hookName = declarations.find(ts.isImportSpecifier);
16
+ const calledName = hookName ? (hookName.propertyName ?? hookName.name).text : ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : "";
17
+ const namespaceSymbol = ts.isPropertyAccessExpression(call.expression) ? checker.getSymbolAtLocation(call.expression.expression) : undefined;
18
+ const reactNamespace = namespaceSymbol?.declarations?.some(isReactImport);
19
+ return (reactHook || reactNamespace) && /^use[A-Z]/.test(calledName) ? calledName : undefined;
20
+ }
21
+ function isConditionalOrLoop(node, child) {
22
+ if (ts.isIfStatement(node))
23
+ return child !== node.expression;
24
+ if (ts.isConditionalExpression(node))
25
+ return child !== node.condition;
26
+ if (ts.isBinaryExpression(node))
27
+ return child === node.right &&
28
+ [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(node.operatorToken.kind);
29
+ if (ts.isForStatement(node))
30
+ return child !== node.initializer;
31
+ if (ts.isForOfStatement(node) || ts.isForInStatement(node))
32
+ return child !== node.expression;
33
+ return ts.isWhileStatement(node) || ts.isDoStatement(node);
34
+ }
35
+ // The metric visitor supplies only calls owned by the current function. Keeping
36
+ // traversal and identity there preserves nested-function boundaries and ordering.
37
+ export function inspectReactCall(call, body, checker) {
38
+ const hook = importedHookName(call, checker);
39
+ if (!hook)
40
+ return [];
41
+ const findings = [];
42
+ let ancestor = call.parent;
43
+ let child = call;
44
+ while (ancestor && ancestor !== body) {
45
+ if (isConditionalOrLoop(ancestor, child)) {
46
+ findings.push({ category: "conditionalHook", message: `${hook} is called within a conditional or loop.`, observations: { hook }, confidence: "high" });
47
+ break;
48
+ }
49
+ child = ancestor;
50
+ ancestor = ancestor.parent;
51
+ }
52
+ if (["useEffect", "useLayoutEffect"].includes(hook)) {
53
+ const callback = call.arguments[0];
54
+ if (callback && isFunction(callback) && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword))
55
+ findings.push({ category: "asyncEffectCallback", message: "An effect callback is async and returns a Promise instead of cleanup or undefined.", observations: { hook }, confidence: "high" });
56
+ if (call.arguments.length < 2)
57
+ findings.push({ category: "effectWithoutDependencies", message: "Effect runs after every render; verify that this is intentional.", observations: { hook }, confidence: "low" });
58
+ }
59
+ return findings;
60
+ }
@@ -0,0 +1,12 @@
1
+ export interface Fraction {
2
+ numerator: bigint;
3
+ denominator: bigint;
4
+ }
5
+ export declare function fraction(numerator: bigint, denominator?: bigint): Fraction;
6
+ export declare function decimal(value: number): Fraction;
7
+ export declare const add: (a: Fraction, b: Fraction) => Fraction;
8
+ export declare const multiply: (a: Fraction, b: Fraction) => Fraction;
9
+ export declare const subtract: (a: Fraction, b: Fraction) => Fraction;
10
+ export declare const numeric: (a: Fraction) => number;
11
+ export declare function roundScore(a: Fraction): number;
12
+ export declare function interpolate(value: number, anchors: readonly (readonly [number, number])[]): Fraction;
@@ -0,0 +1,40 @@
1
+ export function fraction(numerator, denominator = 1n) {
2
+ if (denominator <= 0n)
3
+ throw new Error("Positive denominator required.");
4
+ let a = numerator < 0n ? -numerator : numerator, b = denominator;
5
+ while (b) {
6
+ const remainder = a % b;
7
+ a = b;
8
+ b = remainder;
9
+ }
10
+ return { numerator: numerator / a, denominator: denominator / a };
11
+ }
12
+ export function decimal(value) {
13
+ if (!Number.isFinite(value))
14
+ throw new Error("Finite score measurement required.");
15
+ const [mantissa, exponent = "0"] = String(value).toLowerCase().split("e");
16
+ const places = (mantissa.split(".")[1] ?? "").length - Number(exponent);
17
+ const numerator = BigInt(mantissa.replace(".", ""));
18
+ return places >= 0 ? fraction(numerator, 10n ** BigInt(places)) : fraction(numerator * 10n ** BigInt(-places));
19
+ }
20
+ export const add = (a, b) => fraction(a.numerator * b.denominator + b.numerator * a.denominator, a.denominator * b.denominator);
21
+ export const multiply = (a, b) => fraction(a.numerator * b.numerator, a.denominator * b.denominator);
22
+ export const subtract = (a, b) => add(a, { ...b, numerator: -b.numerator });
23
+ export const numeric = (a) => Number(a.numerator) / Number(a.denominator);
24
+ export function roundScore(a) {
25
+ if (a.numerator < 0n)
26
+ throw new Error("Score must be nonnegative.");
27
+ return Number((a.numerator * 20n + a.denominator) / (2n * a.denominator)) / 10;
28
+ }
29
+ export function interpolate(value, anchors) {
30
+ if (!Number.isFinite(value))
31
+ throw new Error("Finite score measurement required.");
32
+ if (value <= anchors[0][0])
33
+ return decimal(anchors[0][1]);
34
+ for (let index = 1; index < anchors.length; index++) {
35
+ const [x, y] = anchors[index], [previousX, previousY] = anchors[index - 1];
36
+ if (value <= x)
37
+ return add(decimal(previousY), multiply(subtract(decimal(value), decimal(previousX)), fraction(BigInt(y - previousY), BigInt(x - previousX))));
38
+ }
39
+ return decimal(anchors[anchors.length - 1][1]);
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemetrics-ai",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Deterministic code metrics and scorecard evidence for JavaScript, TypeScript, and React projects",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,8 +15,9 @@
15
15
  "scripts": {
16
16
  "build": "tsc -p tsconfig.json && node scripts/copy-schema.mjs",
17
17
  "prepack": "npm run build",
18
- "test": "npm run build && vitest run",
19
- "test:package": "node scripts/package-smoke.mjs"
18
+ "test": "npm run build && vitest run --maxWorkers=2",
19
+ "test:package": "node scripts/package-smoke.mjs",
20
+ "test:architecture": "npm run build && node scripts/architecture-corpus.mjs"
20
21
  },
21
22
  "engines": {
22
23
  "node": ">=20"