codemetrics-ai 0.3.0 → 0.5.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 (45) hide show
  1. package/README.md +114 -8
  2. package/dist/analyzer.js +37 -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-scoring.d.ts +1 -0
  14. package/dist/error-handling-scoring.js +15 -0
  15. package/dist/error-handling.d.ts +33 -0
  16. package/dist/error-handling.js +147 -0
  17. package/dist/failure-contracts.d.ts +19 -0
  18. package/dist/failure-contracts.js +261 -0
  19. package/dist/function-measurements.d.ts +17 -0
  20. package/dist/function-measurements.js +85 -0
  21. package/dist/function-nodes.d.ts +3 -0
  22. package/dist/function-nodes.js +5 -0
  23. package/dist/function-scoring.d.ts +5 -0
  24. package/dist/function-scoring.js +50 -0
  25. package/dist/handler-disposition.d.ts +9 -0
  26. package/dist/handler-disposition.js +156 -0
  27. package/dist/handler-resolution.d.ts +26 -0
  28. package/dist/handler-resolution.js +99 -0
  29. package/dist/metrics.d.ts +3 -0
  30. package/dist/metrics.js +23 -45
  31. package/dist/module-forwarding.d.ts +4 -0
  32. package/dist/module-forwarding.js +153 -0
  33. package/dist/module-graph.d.ts +34 -0
  34. package/dist/module-graph.js +122 -0
  35. package/dist/module-references.d.ts +8 -0
  36. package/dist/module-references.js +51 -0
  37. package/dist/module-resolution.d.ts +10 -0
  38. package/dist/module-resolution.js +76 -0
  39. package/dist/module-topology.d.ts +72 -0
  40. package/dist/module-topology.js +72 -0
  41. package/dist/react-probe.d.ts +6 -0
  42. package/dist/react-probe.js +60 -0
  43. package/dist/score-arithmetic.d.ts +12 -0
  44. package/dist/score-arithmetic.js +40 -0
  45. package/package.json +4 -3
@@ -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.5.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"