@titan-design/code-graph 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -1937,6 +1937,36 @@ function areLinked(a, b) {
1937
1937
  return false;
1938
1938
  }
1939
1939
 
1940
+ // src/symbol-metrics.ts
1941
+ var SYMBOL_METRICS = [
1942
+ { name: "symbol_cognitive", stat: "cognitive", unit: "count" },
1943
+ { name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
1944
+ { name: "symbol_loc", stat: "loc", unit: "lines" },
1945
+ { name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" }
1946
+ ];
1947
+ var SYMBOL_METRIC_NAMES = SYMBOL_METRICS.map((m) => m.name);
1948
+ function symbolMetrics(fileId2, stats, symbolNames) {
1949
+ if (symbolNames.size === 0) return [];
1950
+ const byName = /* @__PURE__ */ new Map();
1951
+ for (const s of stats) {
1952
+ if (!s.name || !symbolNames.has(s.name)) continue;
1953
+ const prev = byName.get(s.name);
1954
+ if (!prev) {
1955
+ byName.set(s.name, { ...s });
1956
+ continue;
1957
+ }
1958
+ for (const { stat } of SYMBOL_METRICS) prev[stat] = Math.max(prev[stat], s[stat]);
1959
+ }
1960
+ const out = [];
1961
+ for (const [name, values] of byName) {
1962
+ const nodeId = symbolId(fileId2, name);
1963
+ for (const m of SYMBOL_METRICS) {
1964
+ out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
1965
+ }
1966
+ }
1967
+ return out;
1968
+ }
1969
+
1940
1970
  // src/source-metrics.ts
1941
1971
  var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
1942
1972
  "function_declaration",
@@ -1986,12 +2016,11 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
1986
2016
  "max_nesting_depth",
1987
2017
  "class_count",
1988
2018
  "lcom4_max",
1989
- // Per-symbol complexity (C-58; C-64 extends it to non-exported symbols), keyed
2019
+ // Per-symbol complexity, loc and nesting (C-58, C-64, TP-317), keyed
1990
2020
  // to `symbol` node ids. Source-local like the file-level metrics above, so an
1991
2021
  // unchanged file carries them forward — but their nodeId is `<fileId>#<name>`,
1992
2022
  // so the reuse basis buckets them under the symbol's parent file (incremental.ts).
1993
- "symbol_cognitive",
1994
- "symbol_cyclomatic"
2023
+ ...SYMBOL_METRIC_NAMES
1995
2024
  ]);
1996
2025
  var EMPTY_NAMES = /* @__PURE__ */ new Set();
1997
2026
  function computeSourceMetrics(files, fileIdOf, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
@@ -2045,30 +2074,10 @@ function metricsForFile(nodeId, file, symbolNames) {
2045
2074
  unit: "count"
2046
2075
  });
2047
2076
  }
2048
- out.push(...symbolComplexityMetrics(nodeId, stats, symbolNames));
2077
+ out.push(...symbolMetrics(nodeId, stats, symbolNames));
2049
2078
  out.push(...computeLcomMetrics(file, nodeId));
2050
2079
  return out;
2051
2080
  }
2052
- function symbolComplexityMetrics(fileId2, stats, symbolNames) {
2053
- if (symbolNames.size === 0) return [];
2054
- const byName = /* @__PURE__ */ new Map();
2055
- for (const s of stats) {
2056
- if (!s.name || !symbolNames.has(s.name)) continue;
2057
- const prev = byName.get(s.name);
2058
- if (!prev) byName.set(s.name, { cognitive: s.cognitive, cyclomatic: s.cyclomatic });
2059
- else {
2060
- prev.cognitive = Math.max(prev.cognitive, s.cognitive);
2061
- prev.cyclomatic = Math.max(prev.cyclomatic, s.cyclomatic);
2062
- }
2063
- }
2064
- const out = [];
2065
- for (const [name, m] of byName) {
2066
- const sid = symbolId(fileId2, name);
2067
- out.push({ nodeId: sid, name: "symbol_cognitive", value: m.cognitive, unit: "count" });
2068
- out.push({ nodeId: sid, name: "symbol_cyclomatic", value: m.cyclomatic, unit: "count" });
2069
- }
2070
- return out;
2071
- }
2072
2081
  function countLoc(content) {
2073
2082
  return content.split("\n").filter((l) => l.trim() !== "").length;
2074
2083
  }
@@ -2082,7 +2091,8 @@ function analyzeFunctions(file) {
2082
2091
  name: fn.name === null ? null : qualify(scope, fn.name),
2083
2092
  cyclomatic: cyclomaticOf(fn.body, file.language),
2084
2093
  cognitive: cognitiveComplexityOf(fn.body, file.language),
2085
- nestingDepth: nestingDepthOf(fn.body, file.language, 0)
2094
+ nestingDepth: nestingDepthOf(fn.body, file.language, 0),
2095
+ loc: fn.node.endPosition.row - fn.node.startPosition.row + 1
2086
2096
  });
2087
2097
  });
2088
2098
  return stats;
@@ -2090,12 +2100,12 @@ function analyzeFunctions(file) {
2090
2100
  function functionAt(node, fnTypes) {
2091
2101
  if (fnTypes.has(node.type)) {
2092
2102
  const body = node.childForFieldName("body");
2093
- return body ? { name: node.childForFieldName("name")?.text ?? null, body } : null;
2103
+ return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
2094
2104
  }
2095
2105
  if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
2096
2106
  const body = node.childForFieldName("body");
2097
2107
  if (!body) return null;
2098
- return { name: node.parent.childForFieldName("name")?.text ?? null, body };
2108
+ return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
2099
2109
  }
2100
2110
  return null;
2101
2111
  }
@@ -2130,6 +2140,146 @@ function cyclomaticOf(body, language) {
2130
2140
  return complexity;
2131
2141
  }
2132
2142
 
2143
+ // src/analysis/dead-code-python.ts
2144
+ var TERMINALS = /* @__PURE__ */ new Set([
2145
+ "return_statement",
2146
+ "raise_statement",
2147
+ "break_statement",
2148
+ "continue_statement"
2149
+ ]);
2150
+ var SCOPE_TYPES = /* @__PURE__ */ new Set(["function_definition", "lambda", "class_definition"]);
2151
+ var SPLAT_TYPES = /* @__PURE__ */ new Set(["list_splat_pattern", "dictionary_splat_pattern"]);
2152
+ var RECEIVERS = /* @__PURE__ */ new Set(["self", "cls"]);
2153
+ function pythonDeadCode(root) {
2154
+ const out = { unreachable: 0, locals: 0, params: 0 };
2155
+ const visit = (node) => {
2156
+ if (node.type === "block") out.unreachable += unreachableInBlock(node);
2157
+ if (node.type === "function_definition") {
2158
+ const r = analyzeFunction(node);
2159
+ out.locals += r.locals;
2160
+ out.params += r.params;
2161
+ }
2162
+ for (const child of node.namedChildren) {
2163
+ if (child) visit(child);
2164
+ }
2165
+ };
2166
+ visit(root);
2167
+ return out;
2168
+ }
2169
+ function unreachableInBlock(block) {
2170
+ let seenTerminal = false;
2171
+ let count = 0;
2172
+ for (const child of block.namedChildren) {
2173
+ if (!child) continue;
2174
+ if (seenTerminal && child.type !== "comment") count++;
2175
+ if (TERMINALS.has(child.type)) seenTerminal = true;
2176
+ }
2177
+ return count;
2178
+ }
2179
+ function analyzeFunction(fn) {
2180
+ const own = ownBindings(fn);
2181
+ const nested = nestedBindings(fn);
2182
+ const refs = countReferences(fn, own.declIds);
2183
+ const isUnused = (name) => !name.startsWith("_") && !nested.has(name) && !own.declared.has(name) && (refs.get(name) ?? 0) === 0;
2184
+ let params = 0;
2185
+ const paramNames = ownParamNames(fn);
2186
+ for (let i = isStub(fn) ? -1 : paramNames.length - 1; i >= 0; i--) {
2187
+ const name = paramNames[i] ?? null;
2188
+ if (name !== null && isUnused(name)) params++;
2189
+ else break;
2190
+ }
2191
+ const paramSet = new Set(paramNames);
2192
+ let locals = 0;
2193
+ for (const name of own.locals) {
2194
+ if (!paramSet.has(name) && isUnused(name)) locals++;
2195
+ }
2196
+ return { locals, params };
2197
+ }
2198
+ function isStub(fn) {
2199
+ const body = fn.childForFieldName("body");
2200
+ return (body?.namedChildren ?? []).every((stmt) => {
2201
+ if (!stmt || stmt.type === "comment" || stmt.type === "pass_statement") return true;
2202
+ if (stmt.type === "raise_statement") return /^raise NotImplementedError\b/.test(stmt.text);
2203
+ const expr = stmt.type === "expression_statement" ? stmt.namedChild(0) : null;
2204
+ return expr?.type === "ellipsis" || expr?.type === "string";
2205
+ });
2206
+ }
2207
+ function ownParamNames(fn) {
2208
+ const params = fn.childForFieldName("parameters");
2209
+ if (!params) return [];
2210
+ const out = [];
2211
+ for (const p of params.namedChildren) {
2212
+ if (!p || p.type === "keyword_separator" || p.type === "positional_separator") continue;
2213
+ const id = paramIdentifier(p);
2214
+ out.push(id && !RECEIVERS.has(id.text) ? id.text : null);
2215
+ }
2216
+ return out;
2217
+ }
2218
+ function paramIdentifier(p) {
2219
+ if (p.type === "identifier") return p;
2220
+ if (SPLAT_TYPES.has(p.type)) return null;
2221
+ const inner = p.childForFieldName("name") ?? p.namedChild(0);
2222
+ return inner?.type === "identifier" ? inner : null;
2223
+ }
2224
+ function assignedIdentifier(node) {
2225
+ if (node.type !== "assignment" || !node.childForFieldName("right")) return null;
2226
+ const left = node.childForFieldName("left");
2227
+ return left?.type === "identifier" ? left : null;
2228
+ }
2229
+ function ownBindings(fn) {
2230
+ const out = { locals: /* @__PURE__ */ new Set(), declared: /* @__PURE__ */ new Set(), declIds: /* @__PURE__ */ new Set() };
2231
+ for (const p of fn.childForFieldName("parameters")?.namedChildren ?? []) {
2232
+ const id = p ? paramIdentifier(p) : null;
2233
+ if (id) out.declIds.add(id.id);
2234
+ }
2235
+ const walk2 = (node) => {
2236
+ if (SCOPE_TYPES.has(node.type)) return;
2237
+ const id = assignedIdentifier(node);
2238
+ if (id) {
2239
+ out.locals.add(id.text);
2240
+ out.declIds.add(id.id);
2241
+ }
2242
+ if (node.type === "global_statement" || node.type === "nonlocal_statement") {
2243
+ for (const n of node.namedChildren) if (n) out.declared.add(n.text);
2244
+ }
2245
+ for (const child of node.namedChildren) if (child) walk2(child);
2246
+ };
2247
+ const body = fn.childForFieldName("body");
2248
+ if (body) walk2(body);
2249
+ return out;
2250
+ }
2251
+ function nestedBindings(fn) {
2252
+ const out = /* @__PURE__ */ new Set();
2253
+ const walk2 = (node, nested) => {
2254
+ const inner = nested || node !== fn && SCOPE_TYPES.has(node.type);
2255
+ if (inner) {
2256
+ const id = assignedIdentifier(node);
2257
+ if (id) out.add(id.text);
2258
+ if (node.type === "parameters" || node.type === "lambda_parameters") {
2259
+ for (const p of node.namedChildren) {
2260
+ const pid = p ? paramIdentifier(p) : null;
2261
+ if (pid) out.add(pid.text);
2262
+ }
2263
+ }
2264
+ }
2265
+ for (const child of node.namedChildren) if (child) walk2(child, inner);
2266
+ };
2267
+ walk2(fn, false);
2268
+ return out;
2269
+ }
2270
+ function countReferences(fn, declIds) {
2271
+ const out = /* @__PURE__ */ new Map();
2272
+ const walk2 = (node) => {
2273
+ if (node.type === "identifier" && !declIds.has(node.id)) {
2274
+ out.set(node.text, (out.get(node.text) ?? 0) + 1);
2275
+ }
2276
+ for (const child of node.namedChildren) if (child) walk2(child);
2277
+ };
2278
+ const body = fn.childForFieldName("body");
2279
+ if (body) walk2(body);
2280
+ return out;
2281
+ }
2282
+
2133
2283
  // src/analysis/dead-code.ts
2134
2284
  var DEAD_CODE_METRIC_NAMES = /* @__PURE__ */ new Set([
2135
2285
  "unreachable_statements",
@@ -2145,7 +2295,7 @@ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
2145
2295
  "generator_function"
2146
2296
  ]);
2147
2297
  var PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
2148
- var TERMINALS = /* @__PURE__ */ new Set([
2298
+ var TERMINALS2 = /* @__PURE__ */ new Set([
2149
2299
  "return_statement",
2150
2300
  "throw_statement",
2151
2301
  "break_statement",
@@ -2154,13 +2304,13 @@ var TERMINALS = /* @__PURE__ */ new Set([
2154
2304
  function computeDeadCodeMetrics(files, fileIdOf) {
2155
2305
  const out = [];
2156
2306
  for (const file of files) {
2157
- if (file.language !== "typescript" && file.language !== "tsx") continue;
2307
+ const counts = deadCodeOf(file);
2308
+ if (!counts) continue;
2158
2309
  const id = fileIdOf(file.filePath);
2159
- const unreachable = countUnreachable(file.tree.rootNode);
2310
+ const { unreachable, locals, params } = counts;
2160
2311
  if (unreachable > 0) {
2161
2312
  out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
2162
2313
  }
2163
- const { locals, params } = countUnusedBindings(file.tree.rootNode);
2164
2314
  if (locals > 0) {
2165
2315
  out.push({ nodeId: id, name: "unused_locals", value: locals, unit: "count" });
2166
2316
  }
@@ -2170,10 +2320,16 @@ function computeDeadCodeMetrics(files, fileIdOf) {
2170
2320
  }
2171
2321
  return out;
2172
2322
  }
2323
+ function deadCodeOf(file) {
2324
+ const root = file.tree.rootNode;
2325
+ if (file.language === "python") return pythonDeadCode(root);
2326
+ if (file.language !== "typescript" && file.language !== "tsx") return null;
2327
+ return { unreachable: countUnreachable(root), ...countUnusedBindings(root) };
2328
+ }
2173
2329
  function countUnreachable(root) {
2174
2330
  let count = 0;
2175
2331
  const visit = (node) => {
2176
- if (node.type === "statement_block") count += unreachableInBlock(node);
2332
+ if (node.type === "statement_block") count += unreachableInBlock2(node);
2177
2333
  for (const child of node.namedChildren) {
2178
2334
  if (child) visit(child);
2179
2335
  }
@@ -2181,7 +2337,7 @@ function countUnreachable(root) {
2181
2337
  visit(root);
2182
2338
  return count;
2183
2339
  }
2184
- function unreachableInBlock(block) {
2340
+ function unreachableInBlock2(block) {
2185
2341
  let seenTerminal = false;
2186
2342
  let count = 0;
2187
2343
  for (const child of block.namedChildren) {
@@ -2189,7 +2345,7 @@ function unreachableInBlock(block) {
2189
2345
  if (seenTerminal && child.type !== "comment" && child.type !== "function_declaration") {
2190
2346
  count++;
2191
2347
  }
2192
- if (TERMINALS.has(child.type)) seenTerminal = true;
2348
+ if (TERMINALS2.has(child.type)) seenTerminal = true;
2193
2349
  }
2194
2350
  return count;
2195
2351
  }
@@ -2198,7 +2354,7 @@ function countUnusedBindings(root) {
2198
2354
  let params = 0;
2199
2355
  const visit = (node) => {
2200
2356
  if (FUNCTION_TYPES.has(node.type)) {
2201
- const r = analyzeFunction(node);
2357
+ const r = analyzeFunction2(node);
2202
2358
  locals += r.locals;
2203
2359
  params += r.params;
2204
2360
  }
@@ -2209,14 +2365,14 @@ function countUnusedBindings(root) {
2209
2365
  visit(root);
2210
2366
  return { locals, params };
2211
2367
  }
2212
- function analyzeFunction(fn) {
2368
+ function analyzeFunction2(fn) {
2213
2369
  const declIds = /* @__PURE__ */ new Set();
2214
2370
  const nameCount = /* @__PURE__ */ new Map();
2215
2371
  collectBindingDecls(fn, declIds, nameCount);
2216
2372
  const refs = /* @__PURE__ */ new Map();
2217
- countReferences(fn, declIds, refs);
2373
+ countReferences2(fn, declIds, refs);
2218
2374
  const isUnused = (name) => !name.startsWith("_") && (nameCount.get(name) ?? 0) <= 1 && (refs.get(name) ?? 0) === 0;
2219
- const paramNames = ownParamNames(fn);
2375
+ const paramNames = ownParamNames2(fn);
2220
2376
  let params = 0;
2221
2377
  for (let i = paramNames.length - 1; i >= 0; i--) {
2222
2378
  const name = paramNames[i] ?? null;
@@ -2229,7 +2385,7 @@ function analyzeFunction(fn) {
2229
2385
  }
2230
2386
  return { locals, params };
2231
2387
  }
2232
- function ownParamNames(fn) {
2388
+ function ownParamNames2(fn) {
2233
2389
  const params = fn.childForFieldName("parameters");
2234
2390
  if (!params) return [];
2235
2391
  const out = [];
@@ -2272,7 +2428,7 @@ function collectBindingDecls(fn, ids, nameCount) {
2272
2428
  };
2273
2429
  walk2(fn);
2274
2430
  }
2275
- function countReferences(fn, declIds, out) {
2431
+ function countReferences2(fn, declIds, out) {
2276
2432
  const walk2 = (node) => {
2277
2433
  if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
2278
2434
  out.set(node.text, (out.get(node.text) ?? 0) + 1);
@@ -2962,7 +3118,7 @@ function qualifiedByName(file) {
2962
3118
  }
2963
3119
 
2964
3120
  // src/indexer.ts
2965
- var INDEX_VERSION = "0.15.0";
3121
+ var INDEX_VERSION = "0.16.0";
2966
3122
  var LANGUAGES = ["typescript", "python"];
2967
3123
  function canonicalizePath(p) {
2968
3124
  try {
@@ -3930,6 +4086,26 @@ var SOURCE = [
3930
4086
  source: "source-metrics",
3931
4087
  description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
3932
4088
  },
4089
+ {
4090
+ name: "symbol_loc",
4091
+ unit: "lines",
4092
+ appliesTo: SYMBOL,
4093
+ rollup: "max",
4094
+ direction: "higher-worse",
4095
+ absent: "exclude",
4096
+ source: "source-metrics",
4097
+ description: "Lines spanned by the function a symbol names, signature included; the max when several share the name."
4098
+ },
4099
+ {
4100
+ name: "symbol_max_nesting",
4101
+ unit: "count",
4102
+ appliesTo: SYMBOL,
4103
+ rollup: "max",
4104
+ direction: "higher-worse",
4105
+ absent: "exclude",
4106
+ source: "source-metrics",
4107
+ description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
4108
+ },
3933
4109
  {
3934
4110
  name: "class_count",
3935
4111
  unit: "count",
@@ -4177,11 +4353,13 @@ function describeMetrics(names) {
4177
4353
 
4178
4354
  // src/check/context.ts
4179
4355
  function buildRuleContext(store, snapshotId) {
4180
- const nodes = store.listNodes(snapshotId);
4356
+ const all = store.listNodes(snapshotId, { includeSymbols: true });
4357
+ const nodes = all.filter((n) => n.kind !== "symbol");
4358
+ const symbolNodes2 = all.filter((n) => n.kind === "symbol");
4181
4359
  const edges = store.listEdges(snapshotId);
4182
4360
  const nodesById = /* @__PURE__ */ new Map();
4183
4361
  for (const n of nodes) nodesById.set(n.id, n);
4184
- return { nodes, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
4362
+ return { nodes, symbolNodes: symbolNodes2, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
4185
4363
  }
4186
4364
  function indexMetrics(metrics) {
4187
4365
  const metricsByNode = /* @__PURE__ */ new Map();
@@ -4339,6 +4517,18 @@ function barrelMessage(barrelPkg, importerCount) {
4339
4517
  return importerCount === 0 ? `barrel in package "${barrelPkg}" has no importers` : `barrel in package "${barrelPkg}" has ${importerCount} internal importer(s) but zero external`;
4340
4518
  }
4341
4519
 
4520
+ // src/check/violation-location.ts
4521
+ var CODE_GRAPH_TOOL = "code-graph";
4522
+ function locateNode(node) {
4523
+ if (node.kind !== "symbol") return { path: node.id };
4524
+ const location = { path: node.parentId ?? node.id, symbol: node.name };
4525
+ const startLine = node.attrs?.startLine;
4526
+ const endLine = node.attrs?.endLine;
4527
+ if (typeof startLine === "number") location.lineStart = startLine;
4528
+ if (typeof endLine === "number") location.lineEnd = endLine;
4529
+ return location;
4530
+ }
4531
+
4342
4532
  // src/check/metric-rules.ts
4343
4533
  function nodeFilter(rule) {
4344
4534
  return {
@@ -4353,71 +4543,76 @@ function passesFilter(node, filter) {
4353
4543
  if (node.role && filter.excludedRoles.has(node.role)) return false;
4354
4544
  return true;
4355
4545
  }
4356
- function runMetricMaxRule(rule, ctx) {
4546
+ function candidateNodes(rule, ctx) {
4357
4547
  const filter = nodeFilter(rule);
4548
+ const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
4549
+ return pool.filter((node) => passesFilter(node, filter));
4550
+ }
4551
+ function toViolation(rule, breach) {
4552
+ return {
4553
+ ruleId: rule.id,
4554
+ severity: severityOf(rule),
4555
+ nodeId: breach.node.id,
4556
+ metric: breach.metric,
4557
+ value: breach.value,
4558
+ threshold: breach.threshold,
4559
+ message: breach.message,
4560
+ ...locateNode(breach.node),
4561
+ evidence: breach.evidence,
4562
+ tool: CODE_GRAPH_TOOL
4563
+ };
4564
+ }
4565
+ function thresholdBreach(node, metric, value, bound, threshold) {
4566
+ const op = bound === "max" ? ">" : "<";
4567
+ return {
4568
+ node,
4569
+ metric,
4570
+ value,
4571
+ threshold,
4572
+ message: `${metric}=${formatNumber(value)} ${op} ${formatNumber(threshold)}`,
4573
+ evidence: `${metric}=${formatNumber(value)} (${bound} ${formatNumber(threshold)})`
4574
+ };
4575
+ }
4576
+ function runMetricMaxRule(rule, ctx) {
4358
4577
  const out = [];
4359
- for (const node of ctx.nodes) {
4360
- if (!passesFilter(node, filter)) continue;
4578
+ for (const node of candidateNodes(rule, ctx)) {
4361
4579
  const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
4362
- if (value === void 0) continue;
4363
- if (value <= rule.max) continue;
4364
- out.push({
4365
- ruleId: rule.id,
4366
- severity: severityOf(rule),
4367
- nodeId: node.id,
4368
- metric: rule.metric,
4369
- value,
4370
- threshold: rule.max,
4371
- message: `${rule.metric}=${formatNumber(value)} > ${formatNumber(rule.max)}`
4372
- });
4580
+ if (value === void 0 || value <= rule.max) continue;
4581
+ out.push(toViolation(rule, thresholdBreach(node, rule.metric, value, "max", rule.max)));
4373
4582
  }
4374
4583
  return out;
4375
4584
  }
4376
4585
  function runMetricMinRule(rule, ctx) {
4377
- const filter = nodeFilter(rule);
4378
4586
  const out = [];
4379
- for (const node of ctx.nodes) {
4380
- if (!passesFilter(node, filter)) continue;
4587
+ for (const node of candidateNodes(rule, ctx)) {
4381
4588
  const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
4382
- if (value === void 0) continue;
4383
- if (value >= rule.min) continue;
4384
- out.push({
4385
- ruleId: rule.id,
4386
- severity: severityOf(rule),
4387
- nodeId: node.id,
4388
- metric: rule.metric,
4389
- value,
4390
- threshold: rule.min,
4391
- message: `${rule.metric}=${formatNumber(value)} < ${formatNumber(rule.min)}`
4392
- });
4589
+ if (value === void 0 || value >= rule.min) continue;
4590
+ out.push(toViolation(rule, thresholdBreach(node, rule.metric, value, "min", rule.min)));
4393
4591
  }
4394
4592
  return out;
4395
4593
  }
4396
4594
  function runMetricProductMaxRule(rule, ctx) {
4397
- const filter = nodeFilter(rule);
4398
4595
  const out = [];
4399
- for (const node of ctx.nodes) {
4400
- if (!passesFilter(node, filter)) continue;
4596
+ for (const node of candidateNodes(rule, ctx)) {
4401
4597
  const inner = ctx.metricsByNode.get(node.id);
4402
4598
  if (!inner) continue;
4403
4599
  const components = collectComponents(rule.metrics, inner);
4404
4600
  if (!components) continue;
4405
4601
  const product = components.reduce((a, b) => a * b, 1);
4406
4602
  if (product <= rule.max) continue;
4407
- out.push(productViolation(rule, node.id, components, product));
4603
+ out.push(toViolation(rule, productBreach(rule, node, components, product)));
4408
4604
  }
4409
4605
  return out;
4410
4606
  }
4411
- function productViolation(rule, nodeId, components, product) {
4607
+ function productBreach(rule, node, components, product) {
4412
4608
  const detail = rule.metrics.map((m, i) => `${m}=${formatNumber(components[i])}`).join(" * ");
4413
4609
  return {
4414
- ruleId: rule.id,
4415
- severity: severityOf(rule),
4416
- nodeId,
4610
+ node,
4417
4611
  metric: rule.metrics.join(" * "),
4418
4612
  value: product,
4419
4613
  threshold: rule.max,
4420
- message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`
4614
+ message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`,
4615
+ evidence: `${detail} = ${formatNumber(product)} (max ${formatNumber(rule.max)})`
4421
4616
  };
4422
4617
  }
4423
4618
  function collectComponents(metrics, values) {
@@ -4507,6 +4702,43 @@ function countByOriginAndSeverity(violations) {
4507
4702
  return counts;
4508
4703
  }
4509
4704
 
4705
+ // src/check/findings.ts
4706
+ function toFindings(result) {
4707
+ return result.violations.map(violationToFinding);
4708
+ }
4709
+ function violationToFinding(v) {
4710
+ const tool = v.tool ?? CODE_GRAPH_TOOL;
4711
+ const target = v.destinationId ? `${v.nodeId}->${v.destinationId}` : v.nodeId;
4712
+ return withoutUndefined({
4713
+ id: `${tool}:${v.ruleId}:${target}`,
4714
+ path: v.path ?? v.nodeId,
4715
+ lineStart: v.lineStart,
4716
+ lineEnd: v.lineEnd,
4717
+ symbol: v.symbol,
4718
+ signal: v.ruleId,
4719
+ value: v.value,
4720
+ threshold: v.threshold,
4721
+ severity: v.severity,
4722
+ evidence: v.evidence ?? v.message,
4723
+ tool
4724
+ });
4725
+ }
4726
+ function externalToFinding(input) {
4727
+ return withoutUndefined({
4728
+ id: `${input.tool}:${input.rule}:${input.file}:${input.line}`,
4729
+ path: input.file,
4730
+ lineStart: input.line,
4731
+ lineEnd: input.endLine,
4732
+ signal: input.rule,
4733
+ severity: input.severity,
4734
+ evidence: input.message,
4735
+ tool: input.tool
4736
+ });
4737
+ }
4738
+ function withoutUndefined(finding) {
4739
+ return Object.fromEntries(Object.entries(finding).filter(([, v]) => v !== void 0));
4740
+ }
4741
+
4510
4742
  // src/check/validate.ts
4511
4743
  function validateRules(input, options = {}) {
4512
4744
  if (!input || typeof input !== "object") {
@@ -5414,6 +5646,7 @@ export {
5414
5646
  embedSnapshot,
5415
5647
  embedTextsCached,
5416
5648
  externalId,
5649
+ externalToFinding,
5417
5650
  fileId,
5418
5651
  findConventions,
5419
5652
  findSimilarCapability,
@@ -5472,6 +5705,7 @@ export {
5472
5705
  summarizeConventions,
5473
5706
  symbolId,
5474
5707
  testCoverageCountMetrics,
5708
+ toFindings,
5475
5709
  tryEmbedSnapshot,
5476
5710
  validateRules,
5477
5711
  violationKey,