codemetrics-ai 0.4.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.
package/README.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # codemetrics-ai
2
2
 
3
- Deterministic source analysis for JavaScript, TypeScript, JSX and TSX. Version 0.4.0 implements source metrics and React hook/effect checks and emits schema-v3 evidence, including executed scoring decisions. Scores are **uncalibrated across ecosystems**; compare them only with compatible runs of this analyzer.
3
+ Deterministic source analysis for JavaScript, TypeScript, JSX and TSX. Version 0.5.0 implements source metrics and React hook/effect checks and emits schema-v3 evidence, including executed scoring decisions. Scores are **uncalibrated across ecosystems**; compare them only with compatible runs of this analyzer.
4
4
 
5
5
  Each scored dimension includes `scoringDecision`: policy inputs, executed steps, rounding and finding attribution. These are aggregate policy decisions, not independent finding deductions. See [the decision contract](../../shared/scorecard-schema/scoring-decisions.md).
6
6
 
7
- The 0.4.0 ruleset `javascript-typescript-2026-10-04-local-handlers` replaces the published 0.3.0 maximum-CC/median-MI ladders with owned-function population scores and extends React-only checks with bounded promise-usage analysis. It also corrects erased TypeScript syntax in owned-body measurements, scores async usage by affected eligible owners, and adds unscored module dependency, decomposition, and error-handler evidence. Generate new baselines when upgrading from 0.3.0. See the [0.4.0 release and migration notes](https://github.com/sean-m-cooper/CodeMetrics.AI/blob/master/docs/releases/javascript-typescript-0.4.0.md).
7
+ Version 0.5.0 uses `javascript-typescript-2026-10-09-handler-contracts` and adds
8
+ contextual Error Handling scoring. Adopting it requires a fresh baseline. See the
9
+ [0.5.0 release notes](../../docs/releases/javascript-typescript-0.5.0.md).
10
+
11
+ The earlier 0.4.0 ruleset `javascript-typescript-2026-10-04-local-handlers` replaces the published 0.3.0 maximum-CC/median-MI ladders with owned-function population scores and extends React-only checks with bounded promise-usage analysis. It also corrects erased TypeScript syntax in owned-body measurements, scores async usage by affected eligible owners, and adds unscored module dependency, decomposition, and error-handler evidence. Generate new baselines when upgrading from 0.3.0. See the [0.4.0 release and migration notes](https://github.com/sean-m-cooper/CodeMetrics.AI/blob/master/docs/releases/javascript-typescript-0.4.0.md).
8
12
 
9
13
  ```sh
10
14
  npx codemetrics-ai --project package.json
@@ -22,7 +26,7 @@ Default output paths are `.scorecard/javascript-typescript/metrics.csv` and `.sc
22
26
  | maintainability | 40% mean weakest ceil(N/5) function scores + 60% mean remaining |
23
27
  | performanceAsync (Async/Blocking Usage) | React hook/effect checks, standard Promise async executors, and informational async Array.forEach callbacks |
24
28
  | architecture | Module dependencies, explicit type-only separation, internal cycle groups and fan-in/out rankings; score remains unmeasured |
25
- | errorHandling | Catch clauses and inline/referenced standard Promise rejection handlers; unique bodies, linked uses, and documented intent; no score |
29
+ | errorHandling | Contextual failure dispositions for distinct catch/Promise handler bodies; scores unexplained swallowing only with a fully assessed population |
26
30
  | Other dimensions | Explicitly skipped |
27
31
 
28
32
  React checks run only when a source file imports React. Aliased named imports and namespace imports are recognized; local functions shadowing imports are excluded. Missing dependency arrays are advisory because running after every render can be intentional. The implementation does not claim exhaustive Rules of Hooks, dependency-array correctness, or general concurrency analysis. Source syntax errors make the run incomplete and source dimensions unscored. Semantic type errors are outside this AST-focused version's completeness check.
@@ -44,7 +48,9 @@ This is bounded source analysis, not promise-flow verification. Referenced execu
44
48
 
45
49
  `codeQuality.componentDetails.decomposition` version 2 uses owned executable statements as its primary measure. It ranks functions/modules by statements and reports statement totals, median/p90, and module concentration. Lines and initializer/literal/declaration/re-export observations remain context. Nested bodies are counted separately. Size is not responsibility: data tables, barrels, and orchestration roles receive no inferred defect or deduction. Code Quality remains a method-complexity score.
46
50
 
47
- `errorHandling.handlerEvidence` distinguishes unexplained empty syntax, documented empty bodies, and bodies containing code. Body comments acknowledge developer intent without judging the business rationale. Empty bodies produce informational review leads only; fallback behavior outside the handler may explain them. Referenced Promise callbacks resolve through function declarations, const aliases, and imports within selected source. Shared implementations count once, with call sites attached. Reassigned, mutable, unsupported, and out-of-scope references retain reasoned inspection gaps. No handlers means an unavailable percentage, not a perfect score. See the [evidence contract](../../shared/scorecard-schema/javascript-typescript-context-evidence.md).
51
+ `errorHandling.handlerEvidence` preserves empty/documented/nonempty syntax measurements and links each distinct implementation to its uses. Referenced Promise callbacks resolve through function declarations, const aliases, and imports within selected source. The new `dispositionEvidence` separates explicit outcomes, recognized fallback, documented intent, unexplained swallowing, and unknown paths. Nonempty code alone does not establish handling. Attached intent is honored without judging the business rationale.
52
+
53
+ When every observed body is assessed and no callback uses are unresolved, Error Handling scores `10 - 10 * unexplainedSwallowing / assessedHandlers`, capped at 9 if any issue exists, rounded half up to one decimal. Shared bodies count once. Unknown dispositions, unresolved callbacks, and absent populations yield no score. Scores describe bounded handling signals, not proven recovery or bugs. See the [policy and recognition limits](../../shared/scorecard-schema/javascript-typescript-error-handling-policy.md).
48
54
 
49
55
  ## Module architecture evidence
50
56
 
@@ -120,7 +126,7 @@ evidence. Overall dependencies and cycle findings remain intact, including mixed
120
126
  files and cycles crossing the views. Architecture is still unscored. See the
121
127
  [dependency view contract](../../shared/scorecard-schema/javascript-typescript-module-graph.md#implementation-reference-and-public-re-export-views).
122
128
 
123
- `metrics.ts` owns source traversal, raw member metrics and finding identities. `function-measurements.ts` collects the separate owned-body measurements; `function-scoring.ts` applies the two population policies with exact interpolation/rounding from `score-arithmetic.ts`. `react-probe.ts` checks calls owned by each function for React hook/effect observations. `async-probe.ts` checks each visited node once for bounded standard-library promise patterns, including module-level operations. Neither probe traverses nested bodies or calculates metrics. `async-population.ts` assigns distinct eligible owners; `async-scoring.ts` applies the population ladder. `decomposition-evidence.ts` records function size and module distribution without deductions. `handler-resolution.ts` performs bounded declaration lookup; `error-handling.ts` records distinct handler bodies, use sites, and body comments without a score. Regression fixtures preserve raw metrics, source locations and existing finding fingerprints; advisory severity/disposition changes are intentional.
129
+ `metrics.ts` owns source traversal, raw member metrics and finding identities. `function-measurements.ts` collects the separate owned-body measurements; `function-scoring.ts` applies the two population policies with exact interpolation/rounding from `score-arithmetic.ts`. `react-probe.ts` checks calls owned by each function for React hook/effect observations. `async-probe.ts` checks each visited node once for bounded standard-library promise patterns, including module-level operations. Neither probe traverses nested bodies or calculates metrics. `async-population.ts` assigns distinct eligible owners; `async-scoring.ts` applies the population ladder. `decomposition-evidence.ts` records function size and module distribution without deductions. `handler-resolution.ts` performs bounded declaration lookup; `handler-disposition.ts` classifies bounded failure paths; `failure-contracts.ts` resolves local logging, callback, and guarded-result contracts; `error-handling.ts` aggregates bodies, uses, context, and findings; `error-handling-scoring.ts` applies the population formula. Regression fixtures preserve raw metrics and source identities; the handler policy and ruleset change is intentional.
124
130
 
125
131
  ```sh
126
132
  npm ci
@@ -160,3 +166,13 @@ records results and limitations. Executable statements are the accepted primary
160
166
  and weights remain experimental; Code Quality continues to score method complexity
161
167
  only. The [statement calibration report](../../shared/scorecard-schema/calibration-runs/javascript-typescript-statements-2026-10-04.md)
162
168
  records the updated preview and counting contract.
169
+
170
+ The [completed context calibration](../../shared/scorecard-schema/javascript-typescript-context-policy-decision.md)
171
+ retained decomposition and error handling as unscored evidence at that stage. Across 4,062 functions,
172
+ the large-statement candidates overlapped existing CC/MI review groups; source review
173
+ also found explicit fallback and declared-intent counterexamples to body-only handler
174
+ deductions. The released workflow was verified on all six selections and a compatible
175
+ Uptime Kuma rerun. Numerical experiments remain outside the analyzer and are not
176
+ consumer scoring policies. The subsequent [contextual handler policy](../../shared/scorecard-schema/javascript-typescript-error-handling-policy.md)
177
+ enables the agreed population formula only where bounded classification establishes
178
+ an assessable population. Decomposition remains unscored.
package/dist/analyzer.js CHANGED
@@ -11,6 +11,7 @@ import { asyncPopulation } from "./async-population.js";
11
11
  import { decompositionModule, decompositionEvidence } from "./decomposition-evidence.js";
12
12
  import { HandlerResolver } from "./handler-resolution.js";
13
13
  import { inspectErrorHandling, errorHandlingEvidence } from "./error-handling.js";
14
+ import { FailureContracts } from "./failure-contracts.js";
14
15
  export function analyze(options, version) {
15
16
  const identity = invocationIds(options.runId, options.auditId);
16
17
  const discovery = discover(options.project, options.tsconfig);
@@ -30,6 +31,7 @@ export function analyze(options, version) {
30
31
  const handlerSources = pkg.files.map(filename => program.getSourceFile(filename))
31
32
  .filter((source) => !!source && !program.getSyntacticDiagnostics(source).length);
32
33
  const handlerResolver = new HandlerResolver(checker, handlerSources, discovery.repositoryRoot, pkg.name);
34
+ const failureContracts = new FailureContracts(checker, program.getSourceFiles(), handlerSources, discovery.repositoryRoot);
33
35
  const collectModules = moduleGraph.forPackage(pkg, program);
34
36
  for (const filename of pkg.files) {
35
37
  const source = program.getSourceFile(filename);
@@ -48,7 +50,7 @@ export function analyze(options, version) {
48
50
  const file = path.relative(discovery.repositoryRoot, filename).replaceAll("\\", "/");
49
51
  asyncOwners.push(...asyncPopulation(source, checker, file, pkg.name, result.functions, result.findings.filter(item => item.dimension === "performanceAsync").map(item => item.finding)));
50
52
  decompositionModules.push(decompositionModule(source, file, pkg.name, result.functions));
51
- errorHandling.push(inspectErrorHandling(source, checker, file, pkg.name, handlerResolver));
53
+ errorHandling.push(inspectErrorHandling(source, checker, file, pkg.name, handlerResolver, failureContracts));
52
54
  metrics.push(...result.metrics);
53
55
  collected.push(...result.findings);
54
56
  functions.push(...result.functions);
@@ -75,7 +77,7 @@ export function analyze(options, version) {
75
77
  filters: { totalUnits: discovery.packages.reduce((sum, pkg) => sum + pkg.files.length, 0) + discovery.skipped.length,
76
78
  analyzedUnits: analyzedFiles, skipped: discovery.skipped },
77
79
  population: { types: new Set(metrics.map(metric => `${metric.project}|${metric.file}|${metric.type}`)).size, members: metrics.length },
78
- dimensions, analysis: { ...identity, status: diagnostics.length ? "incomplete" : "complete", ruleset: "javascript-typescript-2026-10-04-local-handlers",
80
+ dimensions, analysis: { ...identity, status: diagnostics.length ? "incomplete" : "complete", ruleset: "javascript-typescript-2026-10-09-handler-contracts",
79
81
  calibration: "uncalibrated", configurationFingerprint: hash(JSON.stringify(canonicalConfiguration(discovery.packages.map(pkg => ({ name: pkg.name,
80
82
  options: pkg.options, selection: pkg.selection })), discovery.repositoryRoot))), diagnostics, suppressions: [] }
81
83
  };
@@ -0,0 +1 @@
1
+ export declare function errorHandlingDecision(total: number, unexplained: number, unknown: number, unresolvedUses: number): import("./scoring-decision.js").ScoringDecision | undefined;
@@ -0,0 +1,15 @@
1
+ import { firstMatch } from "./scoring-decision.js";
2
+ import { fraction, roundScore } from "./score-arithmetic.js";
3
+ export function errorHandlingDecision(total, unexplained, unknown, unresolvedUses) {
4
+ if (![total, unexplained, unknown, unresolvedUses].every(value => Number.isSafeInteger(value) && value >= 0) || unexplained + unknown > total)
5
+ throw new Error("Invalid handler population.");
6
+ if (!total || unknown || unresolvedUses)
7
+ return undefined;
8
+ const populationScore = roundScore(fraction(10n * BigInt(total - unexplained), BigInt(total)));
9
+ const cap = unexplained ? 9 : 10;
10
+ return firstMatch("javascript-typescript/errorHandling/failure-disposition-v2", { countingUnit: "distinctHandlerBody", totalHandlers: total, assessedHandlers: total, unexplainedSwallowing: unexplained,
11
+ unknownHandlers: unknown, unresolvedCallbackUses: unresolvedUses, populationScore, cap,
12
+ formula: "min(10 - 10 * unexplainedSwallowing / assessedHandlers, unexplainedSwallowing > 0 ? 9 : 10)", rounding: "half-up-one-decimal" }, [{ id: "issueCap", condition: "unexplainedSwallowing > 0 and populationScore > 9", matched: unexplained > 0 && populationScore > 9,
13
+ score: 9, categories: ["unexplainedSwallowing"] },
14
+ { id: "population", condition: "otherwise", matched: true, score: populationScore, categories: ["unexplainedSwallowing"] }]);
15
+ }
@@ -1,6 +1,8 @@
1
1
  import ts from "typescript";
2
2
  import { type Dimension } from "./evidence.js";
3
3
  import { HandlerResolver } from "./handler-resolution.js";
4
+ import { type HandlerDisposition } from "./handler-disposition.js";
5
+ import type { FailureContracts } from "./failure-contracts.js";
4
6
  type UsageKind = "catchClause" | "promiseCatch" | "promiseThenRejection";
5
7
  interface Handler {
6
8
  id: string;
@@ -10,6 +12,7 @@ interface Handler {
10
12
  kind: "catchClause" | "promiseRejection";
11
13
  classification: "unexplainedEmpty" | "documentedEmpty" | "containsCode";
12
14
  comments: string[];
15
+ disposition: HandlerDisposition;
13
16
  uses: {
14
17
  file: string;
15
18
  line: number;
@@ -17,7 +20,7 @@ interface Handler {
17
20
  resolution: "inline" | "reference";
18
21
  }[];
19
22
  }
20
- export declare function inspectErrorHandling(source: ts.SourceFile, checker: ts.TypeChecker, file: string, project: string, resolver: HandlerResolver): {
23
+ export declare function inspectErrorHandling(source: ts.SourceFile, checker: ts.TypeChecker, file: string, project: string, resolver: HandlerResolver, contracts: FailureContracts): {
21
24
  handlers: Handler[];
22
25
  uninspectedCallbacks: {
23
26
  file: string;
@@ -1,6 +1,8 @@
1
1
  import ts from "typescript";
2
- import { hash } from "./evidence.js";
2
+ import { hash, scored } from "./evidence.js";
3
3
  import { unwrapHandler } from "./handler-resolution.js";
4
+ import { classifyDisposition } from "./handler-disposition.js";
5
+ import { errorHandlingDecision } from "./error-handling-scoring.js";
4
6
  function undefinedValue(node, checker) {
5
7
  node = unwrapHandler(node);
6
8
  if (ts.isVoidExpression(node))
@@ -17,11 +19,22 @@ function emptyBody(body, checker) {
17
19
  (ts.isReturnStatement(statement) && (!statement.expression || undefinedValue(statement.expression, checker))));
18
20
  }
19
21
  function bodyComments(body, source) {
22
+ const nested = [];
23
+ function visit(node) {
24
+ if (node !== body && (ts.isFunctionLike(node) || ts.isLiteralExpression(node) || ts.isTemplateExpression(node))) {
25
+ nested.push({ start: ts.isFunctionLike(node) ? node.pos : node.getStart(source), end: node.end });
26
+ return;
27
+ }
28
+ ts.forEachChild(node, visit);
29
+ }
30
+ visit(body);
20
31
  const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, source.languageVariant, source.text.slice(body.pos, body.end));
21
32
  const comments = [];
22
33
  for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
23
34
  if (token !== ts.SyntaxKind.SingleLineCommentTrivia && token !== ts.SyntaxKind.MultiLineCommentTrivia)
24
35
  continue;
36
+ if (nested.some(range => body.pos + scanner.getTokenPos() >= range.start && body.pos + scanner.getTokenPos() < range.end))
37
+ continue;
25
38
  const text = scanner.getTokenText().replace(/^\/\/?\*?|\*\/$/g, "").trim();
26
39
  if (/[\p{L}\p{N}]/u.test(text))
27
40
  comments.push(text);
@@ -41,20 +54,24 @@ function rejectionArgument(call, checker) {
41
54
  if (declaration.name.text === "then" && call.arguments.length > 1)
42
55
  return { index: 1, kind: "promiseThenRejection" };
43
56
  }
44
- export function inspectErrorHandling(source, checker, file, project, resolver) {
57
+ export function inspectErrorHandling(source, checker, file, project, resolver, contracts) {
45
58
  const handlers = new Map();
46
59
  const uninspectedCallbacks = [];
47
60
  function add(body, kind, at, resolution) {
48
61
  const identity = resolver.identity(body);
49
62
  const existing = handlers.get(identity.id);
50
63
  const use = { file, line: source.getLineAndCharacterOfPosition(at.getStart(source)).line + 1, kind, resolution };
64
+ const empty = emptyBody(body, checker), comments = bodyComments(body, body.getSourceFile());
65
+ const disposition = classifyDisposition(body, checker, empty, comments, at, contracts);
51
66
  if (existing) {
52
67
  existing.uses.push(use);
68
+ if (existing.disposition.classification !== disposition.classification)
69
+ existing.disposition = { classification: "unknown", reason: "Uses of the shared handler have different failure contexts." };
53
70
  return;
54
71
  }
55
- const empty = emptyBody(body, checker), comments = empty ? bodyComments(body, body.getSourceFile()) : [];
56
72
  const classification = !empty ? "containsCode" : comments.length ? "documentedEmpty" : "unexplainedEmpty";
57
- handlers.set(identity.id, { ...identity, kind: kind === "catchClause" ? "catchClause" : "promiseRejection", classification, comments, uses: [use] });
73
+ handlers.set(identity.id, { ...identity, kind: kind === "catchClause" ? "catchClause" : "promiseRejection", classification, comments,
74
+ disposition, uses: [use] });
58
75
  }
59
76
  function visit(node) {
60
77
  if (ts.isTypeNode(node) && !ts.isExpressionWithTypeArguments(node))
@@ -77,19 +94,25 @@ export function inspectErrorHandling(source, checker, file, project, resolver) {
77
94
  visit(source);
78
95
  return { handlers: [...handlers.values()], uninspectedCallbacks };
79
96
  }
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" } };
97
+ function handlerFinding(handler, scoreAvailable) {
98
+ const actionable = handler.disposition.classification === "unexplainedSwallowing";
99
+ const category = actionable ? "unexplainedSwallowing" : "unknownHandlerDisposition";
100
+ return { category, ruleId: `javascript-typescript/errorHandling/${category}`,
101
+ fingerprint: hash(`${handler.id}|${category}`), file: handler.file, project: handler.project, line: handler.line,
102
+ severity: actionable && scoreAvailable ? "warning" : "info", confidence: actionable ? "high" : "low",
103
+ message: handler.disposition.reason,
104
+ observations: { handlerId: handler.id, kind: handler.kind, uses: handler.uses, classification: handler.disposition.classification,
105
+ scoreDisposition: !actionable ? "excludedUnknown" : scoreAvailable ? "policyInput" : "excludedUnavailablePopulation" } };
86
106
  }
87
107
  export function errorHandlingEvidence(results, incomplete) {
88
108
  const unique = new Map();
89
109
  for (const row of results.flatMap(result => result.handlers)) {
90
110
  const existing = unique.get(row.id);
91
- if (existing)
111
+ if (existing) {
92
112
  existing.uses.push(...row.uses);
113
+ if (existing.disposition.classification !== row.disposition.classification)
114
+ existing.disposition = { classification: "unknown", reason: "Selected project contexts disagree about the shared handler disposition." };
115
+ }
93
116
  else
94
117
  unique.set(row.id, { ...row, uses: [...row.uses] });
95
118
  }
@@ -100,12 +123,22 @@ export function errorHandlingEvidence(results, incomplete) {
100
123
  .sort((a, b) => a.file.localeCompare(b.file, "en") || a.line - b.line);
101
124
  const count = (classification) => handlers.filter(handler => handler.classification === classification).length;
102
125
  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",
126
+ const dispositions = Object.fromEntries(["propagated", "failureResult", "reported", "fallback", "documented", "unexplainedSwallowing", "unknown"]
127
+ .map(kind => [kind, handlers.filter(handler => handler.disposition.classification === kind).length]));
128
+ const decision = incomplete ? undefined : errorHandlingDecision(handlers.length, dispositions.unexplainedSwallowing, dispositions.unknown, uninspectedCallbacks.length);
129
+ const findings = handlers.filter(handler => ["unexplainedSwallowing", "unknown"].includes(handler.disposition.classification))
130
+ .map(handler => handlerFinding(handler, !!decision));
131
+ const scope = { id: "javascript-typescript/errorHandling/failure-disposition-v2", coverage: "partial",
132
+ includes: ["catch-clauses", "inline-standard-promise-rejection-handlers", "attached-intent", "selected-source-referenced-handlers", "bounded-failure-continuations"],
133
+ excludes: ["general-exception-flow", "arbitrary-callback-value-flow", "correctness-of-recovery", "semantic-rationale-judgment"] };
134
+ const dispositionEvidence = { version: 1, countingUnit: "distinctHandlerBody", totalHandlers: handlers.length,
135
+ assessedHandlers: handlers.length - dispositions.unknown, unknownHandlers: dispositions.unknown, counts: dispositions,
136
+ assessedPercent: handlers.length ? 100 * (handlers.length - dispositions.unknown) / handlers.length : null,
137
+ unresolvedCallbackUses: uninspectedCallbacks.length, populationComplete: !incomplete && !dispositions.unknown && !uninspectedCallbacks.length };
138
+ const dimension = decision ? scored(decision, "Partial Error Handling: unexplained swallowing among distinct assessed handler bodies. Documented intent is honored; failure outcomes are signals, not proof of correct recovery.", findings, dispositionEvidence) : { status: incomplete ? "failed" : "skipped", findings,
139
+ basis: incomplete ? "Incomplete source; handler observations are diagnostic only." : !handlers.length ? "No assessed handler population; error handling is unavailable." :
140
+ "Unknown handler dispositions or unresolved callback uses prevent a reliable population score." };
141
+ return { ...dimension, scope, dispositionEvidence, handlerEvidence: { version: 2, countingUnit: "distinctHandlerBody",
109
142
  totalHandlers: handlers.length, unexplainedEmptyHandlers: unexplained, documentedEmptyHandlers: count("documentedEmpty"),
110
143
  handlersContainingCode: count("containsCode"), unexplainedEmptyPercent: handlers.length ? 100 * unexplained / handlers.length : null,
111
144
  handlerUseSites: handlers.reduce((sum, handler) => sum + handler.uses.length, 0),
@@ -0,0 +1,19 @@
1
+ import ts from "typescript";
2
+ export declare function standardConsole(call: ts.CallExpression, checker: ts.TypeChecker): boolean;
3
+ /** Source-backed contracts, not name-based exemptions or a runtime data-flow proof. */
4
+ export declare class FailureContracts {
5
+ private readonly checker;
6
+ private readonly selected;
7
+ private readonly references;
8
+ private readonly writes;
9
+ private readonly local;
10
+ constructor(checker: ts.TypeChecker, sources: readonly ts.SourceFile[], selected: readonly ts.SourceFile[], root: string);
11
+ forwardsCaughtError(call: ts.CallExpression): boolean;
12
+ explicitResult(expression: ts.Expression | undefined, depth?: number): boolean;
13
+ reports(call: ts.CallExpression): boolean;
14
+ private reachesConsole;
15
+ guardedUndefinedResult(body: ts.ConciseBody): boolean;
16
+ private commonJsFile;
17
+ private guardedCallResult;
18
+ private truthyGuard;
19
+ }
@@ -0,0 +1,261 @@
1
+ import ts from "typescript";
2
+ import path from "node:path";
3
+ import { exclusion } from "./discovery.js";
4
+ import { unwrapHandler } from "./handler-resolution.js";
5
+ export function standardConsole(call, checker) {
6
+ const member = call.expression;
7
+ if (!ts.isPropertyAccessExpression(member) || !["error", "warn", "info", "log", "debug", "trace"].includes(member.name.text) ||
8
+ !ts.isIdentifier(member.expression) || member.expression.text !== "console" || !call.arguments.length || call.questionDotToken || member.questionDotToken)
9
+ return false;
10
+ const declarations = checker.getSymbolAtLocation(member.expression)?.declarations ?? [];
11
+ return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile &&
12
+ /(?:lib\.dom\.d\.ts|[/\\]@types[/\\]node[/\\](?:web-globals[/\\])?(?:console|globals)\.d\.ts)$/.test(declaration.getSourceFile().fileName));
13
+ }
14
+ const descendants = (node, match) => match(node) || (ts.forEachChild(node, child => descendants(child, match)) ?? false);
15
+ function enclosingFunction(node) {
16
+ for (let at = node.parent; at; at = at.parent)
17
+ if (ts.isFunctionLike(at))
18
+ return at;
19
+ }
20
+ function documentation(node) {
21
+ return (ts.getLeadingCommentRanges(node.getSourceFile().text, node.getFullStart()) ?? [])
22
+ .map(range => node.getSourceFile().text.slice(range.pos, range.end)).join("\n");
23
+ }
24
+ /** Source-backed contracts, not name-based exemptions or a runtime data-flow proof. */
25
+ export class FailureContracts {
26
+ checker;
27
+ selected;
28
+ references = new Map();
29
+ writes = new Set();
30
+ local = new Set();
31
+ constructor(checker, sources, selected, root) {
32
+ this.checker = checker;
33
+ this.selected = selected;
34
+ for (const source of sources) {
35
+ const relative = path.relative(root, source.fileName).replaceAll("\\", "/");
36
+ if (source.isDeclarationFile || relative.startsWith("../") || exclusion(relative))
37
+ continue;
38
+ this.local.add(source);
39
+ const visit = (node) => {
40
+ if (ts.isIdentifier(node)) {
41
+ const symbol = ts.isShorthandPropertyAssignment(node.parent)
42
+ ? checker.getShorthandAssignmentValueSymbol(node.parent) : checker.getSymbolAtLocation(node);
43
+ if (symbol) {
44
+ const refs = this.references.get(symbol) ?? [];
45
+ refs.push(node);
46
+ this.references.set(symbol, refs);
47
+ }
48
+ }
49
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
50
+ const record = (target) => {
51
+ if (ts.isIdentifier(target) || ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) {
52
+ const symbol = checker.getSymbolAtLocation(target);
53
+ if (symbol)
54
+ this.writes.add(symbol);
55
+ }
56
+ else
57
+ ts.forEachChild(target, record);
58
+ };
59
+ record(node.left);
60
+ }
61
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
62
+ [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
63
+ const symbol = checker.getSymbolAtLocation(node.operand);
64
+ if (symbol)
65
+ this.writes.add(symbol);
66
+ }
67
+ ts.forEachChild(node, visit);
68
+ };
69
+ visit(source);
70
+ }
71
+ }
72
+ forwardsCaughtError(call) {
73
+ if (call.questionDotToken || !ts.isIdentifier(call.expression) || !call.arguments.length)
74
+ return false;
75
+ const target = this.checker.getSymbolAtLocation(call.expression), error = unwrapHandler(call.arguments[0]);
76
+ const declaration = target?.declarations?.[0];
77
+ if (!target || this.writes.has(target) || !declaration || !ts.isParameter(declaration) || declaration.initializer ||
78
+ declaration.getSourceFile() !== call.getSourceFile() || call.pos < declaration.parent.pos || call.end > declaration.parent.end)
79
+ return false;
80
+ const caughtSymbol = (symbol) => {
81
+ const binding = symbol?.declarations?.[0];
82
+ return !!symbol && !!binding && ts.isVariableDeclaration(binding) && ts.isCatchClause(binding.parent) &&
83
+ !this.writes.has(symbol) && call.pos >= binding.parent.block.pos && call.end <= binding.parent.block.end;
84
+ };
85
+ // Only plain payload expressions, never an unrelated call taking the error.
86
+ const payload = (node) => {
87
+ if (ts.isIdentifier(node) && caughtSymbol(this.checker.getSymbolAtLocation(node)))
88
+ return true;
89
+ if (ts.isPropertyAccessExpression(node))
90
+ return payload(node.expression);
91
+ if (ts.isObjectLiteralExpression(node))
92
+ return node.properties.some(property => ts.isPropertyAssignment(property) ? payload(property.initializer) : ts.isShorthandPropertyAssignment(property) &&
93
+ caughtSymbol(this.checker.getShorthandAssignmentValueSymbol(property)));
94
+ return false;
95
+ };
96
+ return payload(error);
97
+ }
98
+ explicitResult(expression, depth = 0) {
99
+ if (!expression || depth > 4)
100
+ return false;
101
+ expression = unwrapHandler(expression);
102
+ if (ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression) || ts.isNewExpression(expression))
103
+ return true;
104
+ if (!ts.isIdentifier(expression))
105
+ return false;
106
+ const symbol = this.checker.getSymbolAtLocation(expression), declarations = symbol?.declarations ?? [];
107
+ if (!symbol || this.writes.has(symbol) || declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]))
108
+ return false;
109
+ const declaration = declarations[0];
110
+ if (!ts.isVariableDeclarationList(declaration.parent) || !(declaration.parent.flags & ts.NodeFlags.Const) || !declaration.initializer)
111
+ return false;
112
+ const value = unwrapHandler(declaration.initializer);
113
+ return ts.isLiteralExpression(value) || [ts.SyntaxKind.NullKeyword, ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(value.kind) || this.explicitResult(value, depth + 1);
114
+ }
115
+ reports(call) {
116
+ return this.reachesConsole(call, new Set(), 0);
117
+ }
118
+ reachesConsole(call, seen, depth, payloadPositions = new Set(call.arguments.map((_, index) => index))) {
119
+ if (call.questionDotToken || (ts.isPropertyAccessExpression(call.expression) && call.expression.questionDotToken))
120
+ return false;
121
+ if (standardConsole(call, this.checker))
122
+ return true;
123
+ const symbol = this.checker.getSymbolAtLocation(call.expression);
124
+ if (symbol && this.writes.has(symbol))
125
+ return false;
126
+ if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)) {
127
+ const receiver = this.checker.getSymbolAtLocation(call.expression.expression);
128
+ if (receiver && this.writes.has(receiver))
129
+ return false;
130
+ }
131
+ const declaration = this.checker.getResolvedSignature(call)?.declaration;
132
+ if (!declaration || depth >= 3 || seen.has(declaration) || !this.local.has(declaration.getSourceFile()) ||
133
+ !(ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isArrowFunction(declaration)) || !declaration.body)
134
+ return false;
135
+ seen = new Set(seen).add(declaration);
136
+ const parameters = new Set(declaration.parameters.filter((parameter, index) => payloadPositions.has(index) || parameter.dotDotDotToken && [...payloadPositions].some(position => position >= index))
137
+ .map(parameter => this.checker.getSymbolAtLocation(parameter.name)).filter(symbol => symbol && !this.writes.has(symbol)));
138
+ const declaredLogger = /\b(?:log|write)\b[^\n]*\b(?:message|exception|error|warn)\b/i.test(documentation(declaration));
139
+ // Ordinary wrappers require an unconditional forwarding call. A documented
140
+ // logger may have runtime level filters; prove its payload reaches a console sink.
141
+ const candidates = [];
142
+ const visit = (node) => {
143
+ if (node !== declaration.body && ts.isFunctionLike(node))
144
+ return;
145
+ if (ts.isCallExpression(node))
146
+ candidates.push(node);
147
+ ts.forEachChild(node, visit);
148
+ };
149
+ if (declaredLogger)
150
+ visit(declaration.body);
151
+ else if (ts.isBlock(declaration.body)) {
152
+ for (const statement of declaration.body.statements) {
153
+ if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
154
+ candidates.push(statement.expression);
155
+ else if (ts.isReturnStatement(statement) && statement.expression && ts.isCallExpression(statement.expression)) {
156
+ candidates.push(statement.expression);
157
+ break;
158
+ }
159
+ else if (!ts.isVariableStatement(statement) && !ts.isEmptyStatement(statement))
160
+ break;
161
+ }
162
+ }
163
+ else if (ts.isCallExpression(declaration.body))
164
+ candidates.push(declaration.body);
165
+ return candidates.some(candidate => {
166
+ const forwarded = new Set(candidate.arguments.flatMap((arg, index) => descendants(arg, node => ts.isIdentifier(node) && parameters.has(this.checker.getSymbolAtLocation(node))) ? [index] : []));
167
+ return forwarded.size > 0 && this.reachesConsole(candidate, seen, depth + 1, forwarded);
168
+ });
169
+ }
170
+ guardedUndefinedResult(body) {
171
+ const fn = enclosingFunction(body);
172
+ if (!fn || !ts.isFunctionDeclaration(fn) || !fn.name || ts.getCombinedModifierFlags(fn) & ts.ModifierFlags.Export)
173
+ return false;
174
+ const symbol = this.checker.getSymbolAtLocation(fn.name);
175
+ if (!symbol || this.writes.has(symbol))
176
+ return false;
177
+ const module = this.checker.getSymbolAtLocation(fn.getSourceFile());
178
+ if (!module && !this.commonJsFile(fn.getSourceFile()) && !enclosingFunction(fn))
179
+ return false;
180
+ if (module && this.checker.getExportsOfModule(module).some(exported => (exported.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(exported) : exported) === symbol))
181
+ return false;
182
+ const uses = (this.references.get(symbol) ?? []).filter(node => node !== fn.name);
183
+ return uses.length > 0 && uses.every(node => this.selected.includes(node.getSourceFile()) &&
184
+ ts.isCallExpression(node.parent) && node.parent.expression === node && this.guardedCallResult(node.parent));
185
+ }
186
+ commonJsFile(source) {
187
+ const exportRoot = (node) => {
188
+ if (!ts.isPropertyAccessExpression(node))
189
+ return;
190
+ if (ts.isIdentifier(node.expression)) {
191
+ const base = node.expression;
192
+ return base.text === "exports" || base.text === "module" && node.name.text === "exports" ? base : undefined;
193
+ }
194
+ return exportRoot(node.expression);
195
+ };
196
+ const inferredExport = (node) => {
197
+ // Depending on the assignments present, the JS binder uses either the
198
+ // assignment or its root identifier as the inferred module declaration.
199
+ while (ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)
200
+ node = node.parent;
201
+ if (ts.isBinaryExpression(node.parent) && node.parent.left === node)
202
+ node = node.parent;
203
+ return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && !!exportRoot(node.left);
204
+ };
205
+ return source.statements.some(statement => {
206
+ if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) ||
207
+ statement.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
208
+ return false;
209
+ const base = exportRoot(statement.expression.left);
210
+ if (!base)
211
+ return false;
212
+ const declarations = this.checker.getSymbolAtLocation(base)?.declarations ?? [];
213
+ // TypeScript binds bare CommonJS `module` to its export assignment when
214
+ // Node declarations are absent; that inferred declaration is not shadowing.
215
+ return declarations.every(declaration => inferredExport(declaration) ||
216
+ declaration.getSourceFile().isDeclarationFile && /[/\\]@types[/\\]node[/\\]/.test(declaration.getSourceFile().fileName));
217
+ });
218
+ }
219
+ guardedCallResult(call) {
220
+ const parent = call.parent;
221
+ let identifier, statement;
222
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
223
+ identifier = parent.name;
224
+ statement = parent.parent.parent;
225
+ }
226
+ else if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(parent.left)) {
227
+ identifier = parent.left;
228
+ statement = parent.parent;
229
+ }
230
+ else
231
+ return false;
232
+ if (!ts.isBlock(statement.parent))
233
+ return false;
234
+ const next = statement.parent.statements[statement.parent.statements.indexOf(statement) + 1];
235
+ const symbol = this.checker.getSymbolAtLocation(identifier);
236
+ if (!symbol || !next || !ts.isIfStatement(next) || !this.truthyGuard(next.expression, symbol))
237
+ return false;
238
+ return (this.references.get(symbol) ?? []).every(node => {
239
+ if (ts.isVariableDeclaration(node.parent) && node.parent.name === node)
240
+ return true;
241
+ if (ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)
242
+ return ts.isCallExpression(node.parent.right) && this.checker.getSymbolAtLocation(node.parent.right.expression) === this.checker.getSymbolAtLocation(call.expression) &&
243
+ node.parent.parent.parent === statement.parent;
244
+ if (ts.isPrefixUnaryExpression(node.parent) || ts.isPostfixUnaryExpression(node.parent))
245
+ return false;
246
+ for (let at = node; at.parent && !ts.isFunctionLike(at.parent); at = at.parent) {
247
+ const parent = at.parent;
248
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && this.truthyGuard(parent.left, symbol))
249
+ return true;
250
+ if (ts.isIfStatement(parent) && (parent.expression === at || parent.thenStatement === at) && this.truthyGuard(parent.expression, symbol))
251
+ return true;
252
+ }
253
+ return false;
254
+ });
255
+ }
256
+ truthyGuard(expression, symbol) {
257
+ expression = unwrapHandler(expression);
258
+ return ts.isIdentifier(expression) ? this.checker.getSymbolAtLocation(expression) === symbol :
259
+ ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && this.truthyGuard(expression.left, symbol);
260
+ }
261
+ }
@@ -0,0 +1,9 @@
1
+ import ts from "typescript";
2
+ import type { FailureContracts } from "./failure-contracts.js";
3
+ export type DispositionKind = "propagated" | "failureResult" | "reported" | "fallback" | "documented" | "unexplainedSwallowing" | "unknown";
4
+ export interface HandlerDisposition {
5
+ classification: DispositionKind;
6
+ reason: string;
7
+ comments?: string[];
8
+ }
9
+ export declare function classifyDisposition(body: ts.ConciseBody, checker: ts.TypeChecker, empty: boolean, comments: string[], use: ts.Node, contracts: FailureContracts): HandlerDisposition;
@@ -0,0 +1,156 @@
1
+ import ts from "typescript";
2
+ import { unwrapHandler } from "./handler-resolution.js";
3
+ const result = (classification, reason) => ({ classification, reason });
4
+ // These are declarations of intent, not a judgment about whether the rationale is good.
5
+ const intentional = /\b(?:ignor(?:e|ed|ing)|swallow(?:ed|ing)?|discard(?:ed|ing)?|optional|best[- ]effort|does nothing|no[- ]op|intention(?:al|ally))\b/i;
6
+ function leadingComments(node) {
7
+ const source = node.getSourceFile();
8
+ return (ts.getLeadingCommentRanges(source.text, node.getFullStart()) ?? [])
9
+ .map(range => source.text.slice(range.pos, range.end)).filter(text => intentional.test(text));
10
+ }
11
+ function declaredIntent(body, checker) {
12
+ const owner = body.parent;
13
+ if (ts.isCatchClause(owner))
14
+ return [...leadingComments(owner), ...leadingComments(owner.parent)];
15
+ if (ts.isFunctionDeclaration(owner)) {
16
+ const declarations = owner.name ? checker.getSymbolAtLocation(owner.name)?.declarations ?? [owner] : [owner];
17
+ return declarations.flatMap(leadingComments);
18
+ }
19
+ if ((ts.isArrowFunction(owner) || ts.isFunctionExpression(owner)) && ts.isVariableDeclaration(owner.parent)) {
20
+ const list = owner.parent.parent;
21
+ // A comment on a multi-binding declaration cannot identify one handler's intent.
22
+ if (ts.isVariableDeclarationList(list) && list.declarations.length === 1)
23
+ return leadingComments(list.parent);
24
+ }
25
+ return leadingComments(owner);
26
+ }
27
+ function explicitValue(expression) {
28
+ if (!expression)
29
+ return false;
30
+ const node = unwrapHandler(expression);
31
+ // No names, calls, spreads or property reads: their value/side effects need more flow analysis.
32
+ if (ts.isLiteralExpression(node) || [ts.SyntaxKind.NullKeyword, ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(node.kind))
33
+ return true;
34
+ if (ts.isPrefixUnaryExpression(node))
35
+ return ts.isNumericLiteral(node.operand);
36
+ if (ts.isArrayLiteralExpression(node))
37
+ return node.elements.every(element => explicitValue(element));
38
+ return ts.isObjectLiteralExpression(node) && node.properties.every(property => ts.isPropertyAssignment(property) &&
39
+ !ts.isComputedPropertyName(property.name) && explicitValue(property.initializer));
40
+ }
41
+ function statementOutcome(statement, checker, contracts) {
42
+ if (ts.isThrowStatement(statement))
43
+ return result("propagated", "Unconditional throw on the failure path.");
44
+ if (ts.isReturnStatement(statement))
45
+ return explicitValue(statement.expression) || contracts.explicitResult(statement.expression)
46
+ ? result("failureResult", "Returns an explicit value/container on the failure path; suitability and caller correctness are not judged.")
47
+ : result("unknown", "Return value or caller contract requires additional analysis.");
48
+ if (ts.isBlock(statement))
49
+ return blockOutcome(statement.statements, checker, contracts);
50
+ if (ts.isIfStatement(statement)) {
51
+ const yes = statementOutcome(statement.thenStatement, checker, contracts);
52
+ const no = statement.elseStatement && statementOutcome(statement.elseStatement, checker, contracts);
53
+ if (yes && no && ![yes.classification, no.classification].includes("unknown"))
54
+ return result(yes.classification === no.classification ? yes.classification : "failureResult", "Both branches have an explicit failure disposition.");
55
+ return result("unknown", "Conditional failure path is not fully resolved.");
56
+ }
57
+ if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
58
+ if (contracts.forwardsCaughtError(statement.expression))
59
+ return result("propagated", "Passes the caught error to a caller-supplied callback; consumer correctness is outside scope.");
60
+ if (contracts.reports(statement.expression))
61
+ return result("reported", "Standard console output or a resolved source-backed logging wrapper; runtime filtering is outside scope.");
62
+ }
63
+ if (ts.isExpressionStatement(statement) || ts.isVariableStatement(statement) || ts.isEmptyStatement(statement) ||
64
+ ts.isFunctionDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement))
65
+ return;
66
+ return result("unknown", "Control flow is outside the bounded handler classifier.");
67
+ }
68
+ function blockOutcome(statements, checker, contracts) {
69
+ for (const statement of statements) {
70
+ const outcome = statementOutcome(statement, checker, contracts);
71
+ if (outcome)
72
+ return outcome;
73
+ }
74
+ }
75
+ function containsReturn(node) {
76
+ if (ts.isReturnStatement(node))
77
+ return true;
78
+ if (ts.isFunctionLike(node))
79
+ return false;
80
+ return ts.forEachChild(node, containsReturn) ?? false;
81
+ }
82
+ function guardedFallback(statement, next, checker, contracts) {
83
+ if (!next || !ts.isIfStatement(next) || statement.tryBlock.statements.length !== 1)
84
+ return false;
85
+ const assignment = statement.tryBlock.statements[0];
86
+ if (!ts.isExpressionStatement(assignment) || !ts.isBinaryExpression(assignment.expression) ||
87
+ assignment.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assignment.expression.left))
88
+ return false;
89
+ const symbol = checker.getSymbolAtLocation(assignment.expression.left);
90
+ const declarations = symbol?.declarations ?? [];
91
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || declarations[0].initializer)
92
+ return false;
93
+ const refersToBinding = (node) => (ts.isIdentifier(node) && checker.getSymbolAtLocation(node) === symbol) ||
94
+ (ts.forEachChild(node, refersToBinding) ?? false);
95
+ if (refersToBinding(assignment.expression.right))
96
+ return false;
97
+ // Require the uninitialized binding immediately before the try: no earlier assigned state.
98
+ const parent = statement.parent;
99
+ if (!ts.isBlock(parent) && !ts.isSourceFile(parent))
100
+ return false;
101
+ const previous = parent.statements[parent.statements.indexOf(statement) - 1];
102
+ if (!previous || !ts.isVariableStatement(previous) || previous.declarationList.declarations.length !== 1 ||
103
+ !(previous.declarationList.flags & ts.NodeFlags.Let) || previous.declarationList.declarations[0] !== declarations[0])
104
+ return false;
105
+ const condition = unwrapHandler(next.expression);
106
+ if (!ts.isBinaryExpression(condition) || condition.operatorToken.kind !== ts.SyntaxKind.EqualsEqualsEqualsToken)
107
+ return false;
108
+ const left = unwrapHandler(condition.left), right = unwrapHandler(condition.right);
109
+ if (!ts.isIdentifier(left) || checker.getSymbolAtLocation(left) !== symbol || !ts.isIdentifier(right) || right.text !== "undefined")
110
+ return false;
111
+ const undefinedDeclarations = checker.getSymbolAtLocation(right)?.declarations ?? [];
112
+ if (undefinedDeclarations.some(declaration => !declaration.getSourceFile().hasNoDefaultLib))
113
+ return false;
114
+ const outcome = statementOutcome(next.thenStatement, checker, contracts);
115
+ return !!outcome && ["failureResult", "propagated", "reported"].includes(outcome.classification);
116
+ }
117
+ export function classifyDisposition(body, checker, empty, comments, use, contracts) {
118
+ const owner = body.parent;
119
+ // Finally may replace a throw/return, even when the catch itself is explicit.
120
+ if (ts.isCatchClause(owner)) {
121
+ for (let ancestor = owner.parent; ancestor && !ts.isFunctionLike(ancestor); ancestor = ancestor.parent)
122
+ if (ts.isTryStatement(ancestor) && ancestor.finallyBlock)
123
+ return result("unknown", "Finally may change the failure disposition.");
124
+ }
125
+ const intent = [...(empty ? comments : comments.filter(comment => intentional.test(comment))), ...declaredIntent(body, checker)];
126
+ if (intent.length)
127
+ return { ...result("documented", "Attached developer intent is honored without judging the business decision."), comments: intent };
128
+ if (!empty) {
129
+ const outcome = ts.isBlock(body) ? blockOutcome(body.statements, checker, contracts) : explicitValue(body) || contracts.explicitResult(body) ? result("failureResult", "Explicit value/container from rejection handler.") : undefined;
130
+ return outcome ?? result("unknown", "Code presence alone does not establish a failure disposition.");
131
+ }
132
+ if (!ts.isCatchClause(owner)) {
133
+ if (!ts.isExpressionStatement(use.parent))
134
+ return result("unknown", "The rejection handler's result is used by an enclosing expression or caller; its failure contract needs context.");
135
+ return result("unexplainedSwallowing", "Empty rejection handler resolves without a value or declared intent; its result is discarded.");
136
+ }
137
+ const statement = owner.parent, parent = statement.parent;
138
+ if (!ts.isBlock(parent) && !ts.isSourceFile(parent))
139
+ return result("unknown", "Enclosing control flow requires context.");
140
+ const following = parent.statements.slice(parent.statements.indexOf(statement) + 1);
141
+ if (guardedFallback(statement, following[0], checker, contracts))
142
+ return result("fallback", "Failed assignment is followed by an explicit undefined guard and failure outcome.");
143
+ if (following.length) {
144
+ const outcome = blockOutcome(following, checker, contracts);
145
+ if (outcome && outcome.classification !== "unknown")
146
+ return result("fallback", outcome.reason);
147
+ return result("unknown", "Continuation after the empty catch needs a failure-contract review.");
148
+ }
149
+ if (containsReturn(statement.tryBlock) && contracts.guardedUndefinedResult(body))
150
+ return result("fallback", "Every selected-source call tests the optional result before using it; no escaping function reference was found.");
151
+ if (containsReturn(statement.tryBlock))
152
+ return result("unknown", "Success returns a value; the implicit/undefined failure result may be a caller contract.");
153
+ if (!ts.isSourceFile(parent) && !ts.isFunctionLike(parent.parent))
154
+ return result("unknown", "Outer continuation or control flow requires context.");
155
+ return result("unexplainedSwallowing", "Terminal empty catch discards failure without an explicit result or attached intent.");
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemetrics-ai",
3
- "version": "0.4.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",