code-auditor-mcp 3.6.0 → 3.8.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 (153) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +73 -0
  3. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  4. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +12 -1
  5. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  6. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  7. package/dist/analyzers/cross-language/SchemaValidator.js +122 -43
  8. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  9. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
  10. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +6 -7
  11. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  12. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  13. package/dist/analyzers/ruleRegistry.js +152 -20
  14. package/dist/analyzers/ruleRegistry.js.map +1 -1
  15. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +38 -1
  16. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  17. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +437 -48
  18. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  19. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +6 -0
  20. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  21. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +117 -35
  22. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  23. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +86 -15
  25. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  26. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +32 -7
  27. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +104 -23
  29. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  30. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts +55 -0
  31. package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -0
  32. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +318 -0
  33. package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -0
  34. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -8
  35. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  36. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +54 -45
  37. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  38. package/dist/analyzers/universal/functionConcerns.d.ts +57 -0
  39. package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -0
  40. package/dist/analyzers/universal/functionConcerns.js +239 -0
  41. package/dist/analyzers/universal/functionConcerns.js.map +1 -0
  42. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
  43. package/dist/analyzers/universal/schema/codeAnalysis.js +13 -7
  44. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
  45. package/dist/analyzers/universal/schema/jsonSchema.js +124 -11
  46. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
  47. package/dist/auditRouter.d.ts.map +1 -1
  48. package/dist/auditRouter.js +4 -6
  49. package/dist/auditRouter.js.map +1 -1
  50. package/dist/auditRunner.d.ts.map +1 -1
  51. package/dist/auditRunner.js +74 -48
  52. package/dist/auditRunner.js.map +1 -1
  53. package/dist/cli.js +321 -98
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config/configLoader.d.ts.map +1 -1
  56. package/dist/config/configLoader.js +23 -0
  57. package/dist/config/configLoader.js.map +1 -1
  58. package/dist/config/defaults.d.ts +6 -0
  59. package/dist/config/defaults.d.ts.map +1 -1
  60. package/dist/config/defaults.js +45 -10
  61. package/dist/config/defaults.js.map +1 -1
  62. package/dist/config/effectiveConfig.d.ts.map +1 -1
  63. package/dist/config/effectiveConfig.js +2 -0
  64. package/dist/config/effectiveConfig.js.map +1 -1
  65. package/dist/conventions/conventionMiner.d.ts +18 -3
  66. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  67. package/dist/conventions/conventionMiner.js +130 -12
  68. package/dist/conventions/conventionMiner.js.map +1 -1
  69. package/dist/daemon/core.d.ts +128 -0
  70. package/dist/daemon/core.d.ts.map +1 -0
  71. package/dist/daemon/core.js +700 -0
  72. package/dist/daemon/core.js.map +1 -0
  73. package/dist/daemon/lspServer.d.ts +51 -0
  74. package/dist/daemon/lspServer.d.ts.map +1 -0
  75. package/dist/daemon/lspServer.js +168 -0
  76. package/dist/daemon/lspServer.js.map +1 -0
  77. package/dist/daemon/main.d.ts +34 -0
  78. package/dist/daemon/main.d.ts.map +1 -0
  79. package/dist/daemon/main.js +147 -0
  80. package/dist/daemon/main.js.map +1 -0
  81. package/dist/daemon/resolve.d.ts +41 -0
  82. package/dist/daemon/resolve.d.ts.map +1 -0
  83. package/dist/daemon/resolve.js +90 -0
  84. package/dist/daemon/resolve.js.map +1 -0
  85. package/dist/daemon/socketClient.d.ts +21 -0
  86. package/dist/daemon/socketClient.d.ts.map +1 -0
  87. package/dist/daemon/socketClient.js +60 -0
  88. package/dist/daemon/socketClient.js.map +1 -0
  89. package/dist/daemon/socketServer.d.ts +33 -0
  90. package/dist/daemon/socketServer.d.ts.map +1 -0
  91. package/dist/daemon/socketServer.js +106 -0
  92. package/dist/daemon/socketServer.js.map +1 -0
  93. package/dist/daemon/types.d.ts +78 -0
  94. package/dist/daemon/types.d.ts.map +1 -0
  95. package/dist/daemon/types.js +10 -0
  96. package/dist/daemon/types.js.map +1 -0
  97. package/dist/dataPaths.d.ts +21 -0
  98. package/dist/dataPaths.d.ts.map +1 -1
  99. package/dist/dataPaths.js +27 -0
  100. package/dist/dataPaths.js.map +1 -1
  101. package/dist/graph/importGraph.d.ts +11 -0
  102. package/dist/graph/importGraph.d.ts.map +1 -1
  103. package/dist/graph/importGraph.js +11 -3
  104. package/dist/graph/importGraph.js.map +1 -1
  105. package/dist/hooks/core.d.ts.map +1 -1
  106. package/dist/hooks/core.js +21 -4
  107. package/dist/hooks/core.js.map +1 -1
  108. package/dist/installer.d.ts.map +1 -1
  109. package/dist/installer.js +2 -5
  110. package/dist/installer.js.map +1 -1
  111. package/dist/languages/RuntimeManager.js +10 -0
  112. package/dist/languages/RuntimeManager.js.map +1 -1
  113. package/dist/languages/go/analyzer +0 -0
  114. package/dist/languages/go/analyzer-src/analyzer.go +166 -35
  115. package/dist/languages/go/analyzer-src/solid.go +80 -172
  116. package/dist/languages/go/analyzer-src/types.go +18 -18
  117. package/dist/ledger.d.ts +1 -1
  118. package/dist/ledger.d.ts.map +1 -1
  119. package/dist/ledger.js +3 -3
  120. package/dist/ledger.js.map +1 -1
  121. package/dist/mcp.js +1 -1
  122. package/dist/mcp.js.map +1 -1
  123. package/dist/mcpAuditJobs.js +1 -1
  124. package/dist/mcpAuditJobs.js.map +1 -1
  125. package/dist/nextFileIncremental.d.ts +9 -3
  126. package/dist/nextFileIncremental.d.ts.map +1 -1
  127. package/dist/nextFileIncremental.js +32 -6
  128. package/dist/nextFileIncremental.js.map +1 -1
  129. package/dist/pipeline.d.ts +26 -0
  130. package/dist/pipeline.d.ts.map +1 -1
  131. package/dist/pipeline.js +86 -10
  132. package/dist/pipeline.js.map +1 -1
  133. package/dist/pipelineAdapters.d.ts +2 -1
  134. package/dist/pipelineAdapters.d.ts.map +1 -1
  135. package/dist/pipelineAdapters.js +228 -5
  136. package/dist/pipelineAdapters.js.map +1 -1
  137. package/dist/reporting/csvReportGenerator.js +1 -1
  138. package/dist/ruleAliases.d.ts.map +1 -1
  139. package/dist/ruleAliases.js +41 -2
  140. package/dist/ruleAliases.js.map +1 -1
  141. package/dist/styles/tailwindProbe.d.ts +8 -0
  142. package/dist/styles/tailwindProbe.d.ts.map +1 -1
  143. package/dist/styles/tailwindProbe.js +11 -1
  144. package/dist/styles/tailwindProbe.js.map +1 -1
  145. package/dist/types/crossLanguage.d.ts +1 -0
  146. package/dist/types/crossLanguage.d.ts.map +1 -1
  147. package/dist/types.d.ts +30 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/types.js.map +1 -1
  150. package/package.json +7 -3
  151. package/plugin/.claude-plugin/plugin.json +1 -1
  152. package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +4 -2
  153. package/plugin/skills/code-auditor/SKILL.md +19 -11
@@ -21,6 +21,7 @@ import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
21
21
  import { TYPESCRIPT_EXTENSIONS, JAVASCRIPT_EXTENSIONS, getLanguageFromPath } from './utils/fileDiscovery.js';
22
22
  import { walkAST, isExported, getLineAndColumn, calculateComplexity, getFunctionBody, } from './languages/adapterBridge.js';
23
23
  import { buildImportMap, extractFunctionCalls, } from './utils/dependencyExtractor.js';
24
+ import { resolveDependency, basenameNoExt } from './graph/importGraph.js';
24
25
  import { isReactComponent, detectComponentType, getComponentName, } from './utils/reactDetection.js';
25
26
  import { findTableReferences, checkNamingConventions, checkQueryPatterns, checkSQLInjection, getNearestTableSuggestions, } from './analyzers/universal/schema/codeAnalysis.js';
26
27
  import { passesFileGate, extractTablesFromRegistry, } from './analyzers/universal/schema/discovery.js';
@@ -125,6 +126,23 @@ export function createDocumentationVisitor() {
125
126
  category: 'style',
126
127
  };
127
128
  }
129
+ // ── Secrets visitor ──────────────────────────────────────────────────────────
130
+ export function createSecretsVisitor() {
131
+ const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSecretsAnalyzer.js').then((m) => new m.UniversalSecretsAnalyzer()));
132
+ return {
133
+ name: 'secrets',
134
+ stage: 'visitor',
135
+ getRuleIds: () => getRuleIdsFor('secrets'),
136
+ async visit(ast, adapter, context, sourceCode) {
137
+ const a = await getAnalyzer();
138
+ const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
139
+ return { violations, facts: {} };
140
+ },
141
+ defaultConfig: {},
142
+ description: 'Detects hardcoded credentials, API keys, and tokens',
143
+ category: 'security',
144
+ };
145
+ }
128
146
  // ── Function-Index visitor (infrastructure) ──────────────────────────────────
129
147
  // Always-on Stage 2 visitor that populates the `functions` table with
130
148
  // per-file definitions so the conventions and cross-domain reducers have data
@@ -861,7 +879,7 @@ export function createConventionsReducer() {
861
879
  // ── Cross-Domain reducer (stage 4) ───────────────────────────────────────────
862
880
  /**
863
881
  * Cross-domain reducer — runs written-never-read, read-never-written,
864
- * transaction-boundary risk, validation-bypass, and uncovered-risk detectors.
882
+ * transaction-boundary risk, no-validator-reachable, and uncovered-risk detectors.
865
883
  * Uses analyzer.analyze([]) with rawDb injected.
866
884
  */
867
885
  export function createCrossDomainReducer() {
@@ -980,7 +998,7 @@ function clCollectCallees(raw) {
980
998
  walk(raw);
981
999
  return [...names];
982
1000
  }
983
- function clMakeFunction(filePath, lang, name, line, signature, purpose, params, callees, exported, complexity) {
1001
+ function clMakeFunction(filePath, lang, name, line, signature, purpose, params, callees, exported, complexity, isMethod = false) {
984
1002
  return {
985
1003
  id: clEntityId(filePath, 'function', name, line),
986
1004
  name,
@@ -997,9 +1015,49 @@ function clMakeFunction(filePath, lang, name, line, signature, purpose, params,
997
1015
  purpose,
998
1016
  context: '',
999
1017
  searchTokens: [name.toLowerCase()],
1000
- metadata: { callees },
1018
+ metadata: { callees, isMethod },
1001
1019
  };
1002
1020
  }
1021
+ /**
1022
+ * Collect a file's import specifiers and whether it declares any exports.
1023
+ *
1024
+ * Feeds file-level reachability: an `import_statement` (TS/JS) or
1025
+ * `import_declaration` (Go) records a dependency; an `export_statement`
1026
+ * (TS/JS) records that the file exposes symbols to importers. Go exports are
1027
+ * computed separately from the extracted entities (capitalized top-level
1028
+ * names), since Go has no `export` keyword.
1029
+ */
1030
+ function clCollectFileInfo(root, lang) {
1031
+ const imports = new Set();
1032
+ let hasExports = false;
1033
+ walkAST(root, (node) => {
1034
+ const raw = node.raw;
1035
+ if (node.type === 'import_statement') {
1036
+ const source = clRawField(raw, 'source');
1037
+ if (source?.text)
1038
+ imports.add(source.text.replace(/^['"]|['"]$/g, ''));
1039
+ }
1040
+ else if (node.type === 'import_declaration') {
1041
+ for (const spec of raw?.namedChildren ?? []) {
1042
+ if (spec.type === 'import_spec') {
1043
+ const p = clRawField(spec, 'path');
1044
+ if (p?.text)
1045
+ imports.add(p.text.replace(/^['"]|['"]$/g, ''));
1046
+ }
1047
+ }
1048
+ }
1049
+ else if (node.type === 'export_statement') {
1050
+ hasExports = true;
1051
+ // Re-exports (`export { x } from './y'`, `export * from './y'`) are import
1052
+ // edges for reachability: a barrel re-exporting a module marks it live,
1053
+ // not dead. The `source` field is present only on the `from` form.
1054
+ const source = clRawField(raw, 'source');
1055
+ if (source?.text)
1056
+ imports.add(source.text.replace(/^['"]|['"]$/g, ''));
1057
+ }
1058
+ });
1059
+ return { imports: [...imports], hasExports };
1060
+ }
1003
1061
  // ── TS/JS extraction ─────────────────────────────────────────────────────────
1004
1062
  function clExtractTSParams(raw) {
1005
1063
  const params = [];
@@ -1055,7 +1113,7 @@ function clExtractTSEntities(root, filePath, sourceCode, lang, out) {
1055
1113
  }
1056
1114
  }
1057
1115
  const line = (raw?.startPosition?.row ?? 0) + 1;
1058
- out.push(clMakeFunction(filePath, lang, displayName, line, clSignatureText(raw, sourceCode) || raw?.text || '', clBodyText(raw, sourceCode), clExtractTSParams(raw), clCollectCallees(raw), isExported(node), calculateComplexity(node)));
1116
+ out.push(clMakeFunction(filePath, lang, displayName, line, clSignatureText(raw, sourceCode) || raw?.text || '', clBodyText(raw, sourceCode), clExtractTSParams(raw), clCollectCallees(raw), isExported(node), calculateComplexity(node), node.type === 'method_definition'));
1059
1117
  return;
1060
1118
  }
1061
1119
  if (node.type === 'variable_declarator') {
@@ -1222,9 +1280,15 @@ export function createCrossLanguageEntityVisitor() {
1222
1280
  else {
1223
1281
  clExtractTSEntities(root, filePath, sourceCode, lang, entities);
1224
1282
  }
1283
+ const fileInfo = clCollectFileInfo(root, lang);
1284
+ // Go has no `export` keyword — an exported symbol is a capitalized
1285
+ // top-level name, which the entity extractor already records as public.
1286
+ const hasExports = lang === 'go'
1287
+ ? entities.some((e) => e.visibility === 'public')
1288
+ : fileInfo.hasExports;
1225
1289
  return {
1226
1290
  violations: [],
1227
- facts: { [filePath]: { entities } },
1291
+ facts: { [filePath]: { entities, imports: fileInfo.imports, hasExports } },
1228
1292
  };
1229
1293
  },
1230
1294
  defaultConfig: {},
@@ -1244,6 +1308,86 @@ function clFlattenEntities(allFacts) {
1244
1308
  }
1245
1309
  return entities;
1246
1310
  }
1311
+ /** Extract per-file `{ imports, hasExports }` from the cross-language facts. */
1312
+ function clFileFacts(allFacts) {
1313
+ const facts = allFacts['cross-language-entities'];
1314
+ const map = new Map();
1315
+ if (!facts)
1316
+ return map;
1317
+ for (const [filePath, data] of Object.entries(facts)) {
1318
+ if (!data)
1319
+ continue;
1320
+ map.set(filePath, { imports: data.imports ?? [], hasExports: data.hasExports ?? false });
1321
+ }
1322
+ return map;
1323
+ }
1324
+ function clIsTestFile(fp) {
1325
+ const lower = fp.toLowerCase();
1326
+ return lower.includes('.test.') || lower.includes('.spec.') ||
1327
+ lower.includes('__tests__') || lower.includes('/test/') || lower.includes('/tests/') ||
1328
+ lower.endsWith('_test.go');
1329
+ }
1330
+ // Files the framework loads directly rather than via an import from sibling
1331
+ // code. Skipped when flagging unreferenced modules — a route/page/entry file
1332
+ // that nothing imports is an entry point, not dead code.
1333
+ const CL_ENTRY_BASENAMES = new Set([
1334
+ 'route', 'page', 'layout', 'loading', 'error', 'not-found', 'template', 'default',
1335
+ 'middleware', 'instrumentation', 'server', 'client', 'cli', 'main', 'app', 'index', 'worker',
1336
+ 'setup', 'seed',
1337
+ ]);
1338
+ function clIsEntryPointFile(fp) {
1339
+ const segments = fp.replace(/\\/g, '/').split('/').filter(Boolean);
1340
+ const base = segments[segments.length - 1] ?? '';
1341
+ const stem = base.replace(/\.[^.]+$/, '');
1342
+ if (CL_ENTRY_BASENAMES.has(stem))
1343
+ return true;
1344
+ if (stem.endsWith('.config'))
1345
+ return true; // next.config, vite.config, tailwind.config, …
1346
+ const joined = '/' + segments.join('/') + '/';
1347
+ if (joined.includes('/app/api/') || joined.includes('/pages/api/'))
1348
+ return true;
1349
+ if (joined.includes('/scripts/') || joined.includes('/bin/') || joined.includes('/cmd/'))
1350
+ return true;
1351
+ return false;
1352
+ }
1353
+ /**
1354
+ * Compute file-level reachability from per-file imports.
1355
+ *
1356
+ * A file is a framework entry point (live → 1.0), imported by another file
1357
+ * (live → 0.5), or referenced by nothing (dead → 0.0). The resolver mirrors
1358
+ * importGraph.ts and handles relative paths, npm/alias basenames, and fuzzy
1359
+ * path-segment matches.
1360
+ */
1361
+ function clComputeReachability(fileFacts) {
1362
+ const filePaths = new Set(fileFacts.keys());
1363
+ const moduleToFile = new Map();
1364
+ for (const fp of filePaths) {
1365
+ const bn = basenameNoExt(fp);
1366
+ if (!moduleToFile.has(bn))
1367
+ moduleToFile.set(bn, new Set());
1368
+ moduleToFile.get(bn).add(fp);
1369
+ }
1370
+ const importersOf = new Map();
1371
+ for (const [fp, info] of fileFacts) {
1372
+ for (const dep of info.imports) {
1373
+ const targets = resolveDependency(dep, filePaths, moduleToFile, fp);
1374
+ for (const t of targets) {
1375
+ if (t === fp)
1376
+ continue;
1377
+ if (!importersOf.has(t))
1378
+ importersOf.set(t, new Set());
1379
+ importersOf.get(t).add(fp);
1380
+ }
1381
+ }
1382
+ }
1383
+ const reachability = new Map();
1384
+ for (const fp of filePaths) {
1385
+ const entry = clIsEntryPointFile(fp);
1386
+ const imported = (importersOf.get(fp)?.size ?? 0) > 0;
1387
+ reachability.set(fp, entry ? 1 : imported ? 0.5 : 0);
1388
+ }
1389
+ return { reachability, importersOf };
1390
+ }
1247
1391
  /**
1248
1392
  * Build `calls` cross-references by resolving each entity's extracted callee
1249
1393
  * names against the corpus entity name index. Library calls (fetch, map, …)
@@ -1407,6 +1551,30 @@ export function createDependencyGraphReducer() {
1407
1551
  const idToEntity = new Map(entities.map((e) => [e.id, e]));
1408
1552
  const violations = [];
1409
1553
  for (const issue of health.issues) {
1554
+ if (issue.type === 'orphaned-nodes') {
1555
+ // Emit one violation per orphan so each is attributed to its own
1556
+ // file/line. The aggregated form anchored every orphan to the first
1557
+ // node's location while the message listed all of them — foreign
1558
+ // symbols leaked into the wrong file.
1559
+ for (const id of issue.affectedNodes) {
1560
+ const orphan = idToEntity.get(id);
1561
+ if (!orphan)
1562
+ continue;
1563
+ violations.push({
1564
+ file: orphan.file,
1565
+ line: orphan.startLine,
1566
+ severity: issue.severity,
1567
+ message: `Orphaned node "${orphan.name}" has no connections.`,
1568
+ rule: issue.type,
1569
+ type: issue.type, // dependency-graph rules match on field: 'type'
1570
+ analyzer: 'dependency-graph',
1571
+ category: 'cross-language-dependency',
1572
+ functionName: orphan.name,
1573
+ details: { orphanId: id },
1574
+ });
1575
+ }
1576
+ continue;
1577
+ }
1410
1578
  const anchor = issue.affectedNodes.map((id) => idToEntity.get(id)).find(Boolean);
1411
1579
  violations.push({
1412
1580
  file: anchor?.file ?? '(unknown)',
@@ -1420,6 +1588,61 @@ export function createDependencyGraphReducer() {
1420
1588
  details: issue.details,
1421
1589
  });
1422
1590
  }
1591
+ // ── File-level reachability: unreferenced modules + ranking axis ──
1592
+ const fileFacts = clFileFacts(allFacts);
1593
+ if (fileFacts.size > 0) {
1594
+ const { reachability, importersOf } = clComputeReachability(fileFacts);
1595
+ // Persist reachability to graph_cache so the post-pipeline reorder
1596
+ // (auditRunner hotspot block) can rank every finding by live/dead.
1597
+ if (context.indexHandle?.rawDb) {
1598
+ try {
1599
+ const db = context.indexHandle.rawDb;
1600
+ const del = db.prepare("DELETE FROM graph_cache WHERE graph_type = 'reachability'");
1601
+ const ins = db.prepare("INSERT OR REPLACE INTO graph_cache (graph_type, node_key, neighbor_key, weight) VALUES ('reachability', ?, '', ?)");
1602
+ const tx = db.transaction(() => {
1603
+ del.run();
1604
+ for (const [fp, score] of reachability) {
1605
+ ins.run(fp, score);
1606
+ }
1607
+ });
1608
+ tx();
1609
+ }
1610
+ catch {
1611
+ // Reachability persistence is advisory — ranking falls back to a
1612
+ // neutral default without it.
1613
+ }
1614
+ }
1615
+ // Flag files that export symbols yet are imported by nothing and are
1616
+ // not framework entry points — a dead module. This is the signal the
1617
+ // orphaned-nodes check missed: a whole unreferenced file whose every
1618
+ // internal symbol still forms a connected component.
1619
+ for (const [fp, info] of fileFacts) {
1620
+ if (!info.hasExports)
1621
+ continue;
1622
+ if (clIsTestFile(fp))
1623
+ continue;
1624
+ if (clIsEntryPointFile(fp))
1625
+ continue;
1626
+ if ((importersOf.get(fp)?.size ?? 0) > 0)
1627
+ continue;
1628
+ violations.push({
1629
+ file: fp,
1630
+ line: 1,
1631
+ // Promoted suggestion → warning (Spec 11 R5, one tier). The
1632
+ // method-dispatch fix removed the class-prefixed / isMethod false
1633
+ // positives, so a module that exports and is imported by nothing
1634
+ // is a genuine dead-module signal — a defect that should block,
1635
+ // not a suggestion to weigh. Validated across four corpora.
1636
+ severity: 'warning',
1637
+ message: 'Module is not imported by any other file and is not a framework entry point — dead code candidate.',
1638
+ rule: 'unreferenced-module',
1639
+ type: 'unreferenced-module',
1640
+ analyzer: 'dependency-graph',
1641
+ category: 'cross-language-dependency',
1642
+ details: { imports: info.imports },
1643
+ });
1644
+ }
1645
+ }
1423
1646
  for (const s of health.suggestions) {
1424
1647
  violations.push({
1425
1648
  file: '(multiple)',