code-auditor-mcp 3.1.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +665 -1
  3. package/README.md +85 -11
  4. package/dist/__test_tmp__/spec18_test.d.ts +2 -0
  5. package/dist/__test_tmp__/spec18_test.d.ts.map +1 -0
  6. package/dist/__test_tmp__/spec18_test.js +5 -0
  7. package/dist/__test_tmp__/spec18_test.js.map +1 -0
  8. package/dist/__tests__/fixtures/spec-20/scripts/tool.d.ts +6 -0
  9. package/dist/__tests__/fixtures/spec-20/scripts/tool.d.ts.map +1 -0
  10. package/dist/__tests__/fixtures/spec-20/scripts/tool.js +12 -0
  11. package/dist/__tests__/fixtures/spec-20/scripts/tool.js.map +1 -0
  12. package/dist/__tests__/fixtures/spec-20/src/demo-module.d.ts +7 -0
  13. package/dist/__tests__/fixtures/spec-20/src/demo-module.d.ts.map +1 -0
  14. package/dist/__tests__/fixtures/spec-20/src/demo-module.js +13 -0
  15. package/dist/__tests__/fixtures/spec-20/src/demo-module.js.map +1 -0
  16. package/dist/analyzers/analyzerUtils.js +2 -2
  17. package/dist/analyzers/analyzerUtils.js.map +1 -1
  18. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +1 -1
  19. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  20. package/dist/analyzers/cross-language/APIContractAnalyzer.js +2 -2
  21. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  22. package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts +3 -0
  23. package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js +3 -3
  25. package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js.map +1 -1
  26. package/dist/analyzers/cross-language/SchemaValidator.d.ts +1 -1
  27. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  28. package/dist/analyzers/cross-language/SchemaValidator.js +2 -2
  29. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  30. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +83 -0
  31. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -0
  32. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +611 -0
  33. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -0
  34. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  35. package/dist/analyzers/documentationAnalyzer.js +8 -4
  36. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  37. package/dist/analyzers/orm/adapterRegistry.d.ts +41 -0
  38. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -0
  39. package/dist/analyzers/orm/adapterRegistry.js +73 -0
  40. package/dist/analyzers/orm/adapterRegistry.js.map +1 -0
  41. package/dist/analyzers/orm/drizzleAdapter.d.ts +42 -0
  42. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -0
  43. package/dist/analyzers/orm/drizzleAdapter.js +166 -0
  44. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -0
  45. package/dist/analyzers/orm/index.d.ts +14 -0
  46. package/dist/analyzers/orm/index.d.ts.map +1 -0
  47. package/dist/analyzers/orm/index.js +24 -0
  48. package/dist/analyzers/orm/index.js.map +1 -0
  49. package/dist/analyzers/orm/prismaAdapter.d.ts +45 -0
  50. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -0
  51. package/dist/analyzers/orm/prismaAdapter.js +170 -0
  52. package/dist/analyzers/orm/prismaAdapter.js.map +1 -0
  53. package/dist/analyzers/orm/types.d.ts +63 -0
  54. package/dist/analyzers/orm/types.d.ts.map +1 -0
  55. package/dist/analyzers/orm/types.js +10 -0
  56. package/dist/analyzers/orm/types.js.map +1 -0
  57. package/dist/analyzers/provenance.d.ts +133 -0
  58. package/dist/analyzers/provenance.d.ts.map +1 -0
  59. package/dist/analyzers/provenance.js +1041 -0
  60. package/dist/analyzers/provenance.js.map +1 -0
  61. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  62. package/dist/analyzers/reactAnalyzer.js +117 -1
  63. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  64. package/dist/analyzers/ruleRegistry.d.ts +38 -0
  65. package/dist/analyzers/ruleRegistry.d.ts.map +1 -0
  66. package/dist/analyzers/ruleRegistry.js +123 -0
  67. package/dist/analyzers/ruleRegistry.js.map +1 -0
  68. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +58 -0
  69. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -0
  70. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +445 -0
  71. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -0
  72. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +52 -0
  73. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  74. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +108 -26
  75. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  76. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +64 -0
  77. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  78. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +259 -53
  79. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  80. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  81. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +5 -15
  82. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  83. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  84. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +10 -10
  85. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  86. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +25 -0
  87. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  88. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +250 -59
  89. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  90. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +103 -0
  91. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -0
  92. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +750 -0
  93. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -0
  94. package/dist/auditRunner.d.ts.map +1 -1
  95. package/dist/auditRunner.js +483 -3
  96. package/dist/auditRunner.js.map +1 -1
  97. package/dist/baseline.d.ts +98 -0
  98. package/dist/baseline.d.ts.map +1 -0
  99. package/dist/baseline.js +184 -0
  100. package/dist/baseline.js.map +1 -0
  101. package/dist/churn/churnExtractor.d.ts +32 -0
  102. package/dist/churn/churnExtractor.d.ts.map +1 -0
  103. package/dist/churn/churnExtractor.js +261 -0
  104. package/dist/churn/churnExtractor.js.map +1 -0
  105. package/dist/cli.js +1215 -11
  106. package/dist/cli.js.map +1 -1
  107. package/dist/codeIndexDB-enhanced.js +1 -1
  108. package/dist/codeIndexDB-enhanced.js.map +1 -1
  109. package/dist/codeIndexDB.d.ts +101 -0
  110. package/dist/codeIndexDB.d.ts.map +1 -1
  111. package/dist/codeIndexDB.js +661 -5
  112. package/dist/codeIndexDB.js.map +1 -1
  113. package/dist/config/configLoader.d.ts.map +1 -1
  114. package/dist/config/configLoader.js +95 -1
  115. package/dist/config/configLoader.js.map +1 -1
  116. package/dist/config/defaults.d.ts +72 -1
  117. package/dist/config/defaults.d.ts.map +1 -1
  118. package/dist/config/defaults.js +146 -15
  119. package/dist/config/defaults.js.map +1 -1
  120. package/dist/config/pathProfiles.d.ts +38 -0
  121. package/dist/config/pathProfiles.d.ts.map +1 -0
  122. package/dist/config/pathProfiles.js +47 -0
  123. package/dist/config/pathProfiles.js.map +1 -0
  124. package/dist/conventions/conventionMiner.d.ts +63 -0
  125. package/dist/conventions/conventionMiner.d.ts.map +1 -0
  126. package/dist/conventions/conventionMiner.js +696 -0
  127. package/dist/conventions/conventionMiner.js.map +1 -0
  128. package/dist/coverage/coverageService.d.ts +49 -0
  129. package/dist/coverage/coverageService.d.ts.map +1 -0
  130. package/dist/coverage/coverageService.js +117 -0
  131. package/dist/coverage/coverageService.js.map +1 -0
  132. package/dist/coverage/istanbulParser.d.ts +84 -0
  133. package/dist/coverage/istanbulParser.d.ts.map +1 -0
  134. package/dist/coverage/istanbulParser.js +120 -0
  135. package/dist/coverage/istanbulParser.js.map +1 -0
  136. package/dist/coverage/lcovParser.d.ts +40 -0
  137. package/dist/coverage/lcovParser.d.ts.map +1 -0
  138. package/dist/coverage/lcovParser.js +168 -0
  139. package/dist/coverage/lcovParser.js.map +1 -0
  140. package/dist/fingerprint.d.ts +17 -0
  141. package/dist/fingerprint.d.ts.map +1 -1
  142. package/dist/fingerprint.js +42 -0
  143. package/dist/fingerprint.js.map +1 -1
  144. package/dist/grammars/tree-sitter-css.wasm +0 -0
  145. package/dist/graph/blastRadius.d.ts +27 -0
  146. package/dist/graph/blastRadius.d.ts.map +1 -0
  147. package/dist/graph/blastRadius.js +150 -0
  148. package/dist/graph/blastRadius.js.map +1 -0
  149. package/dist/graph/callGraph.d.ts +78 -0
  150. package/dist/graph/callGraph.d.ts.map +1 -0
  151. package/dist/graph/callGraph.js +484 -0
  152. package/dist/graph/callGraph.js.map +1 -0
  153. package/dist/graph/importGraph.d.ts +91 -0
  154. package/dist/graph/importGraph.d.ts.map +1 -0
  155. package/dist/graph/importGraph.js +602 -0
  156. package/dist/graph/importGraph.js.map +1 -0
  157. package/dist/graph/outputFormatter.d.ts +55 -0
  158. package/dist/graph/outputFormatter.d.ts.map +1 -0
  159. package/dist/graph/outputFormatter.js +283 -0
  160. package/dist/graph/outputFormatter.js.map +1 -0
  161. package/dist/hooks/codex.d.ts +36 -1
  162. package/dist/hooks/codex.d.ts.map +1 -1
  163. package/dist/hooks/codex.js +88 -54
  164. package/dist/hooks/codex.js.map +1 -1
  165. package/dist/hotspots/hotspotScorer.d.ts +20 -0
  166. package/dist/hotspots/hotspotScorer.d.ts.map +1 -0
  167. package/dist/hotspots/hotspotScorer.js +202 -0
  168. package/dist/hotspots/hotspotScorer.js.map +1 -0
  169. package/dist/invariants/invariant-rules.schema.json +42 -1
  170. package/dist/invariants/ruleEngine.d.ts +4 -1
  171. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  172. package/dist/invariants/ruleEngine.js +141 -5
  173. package/dist/invariants/ruleEngine.js.map +1 -1
  174. package/dist/invariants/ruleValidator.d.ts.map +1 -1
  175. package/dist/invariants/ruleValidator.js +62 -1
  176. package/dist/invariants/ruleValidator.js.map +1 -1
  177. package/dist/invariants/types.d.ts +23 -2
  178. package/dist/invariants/types.d.ts.map +1 -1
  179. package/dist/languages/UniversalAnalyzer.d.ts +1 -1
  180. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  181. package/dist/languages/UniversalAnalyzer.js +61 -4
  182. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  183. package/dist/languages/index.d.ts +1 -0
  184. package/dist/languages/index.d.ts.map +1 -1
  185. package/dist/languages/index.js +3 -0
  186. package/dist/languages/index.js.map +1 -1
  187. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +59 -0
  188. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -0
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +236 -0
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -0
  191. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  192. package/dist/languages/tree-sitter/parser.js +2 -0
  193. package/dist/languages/tree-sitter/parser.js.map +1 -1
  194. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  195. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +61 -10
  196. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  197. package/dist/ledger.d.ts +128 -0
  198. package/dist/ledger.d.ts.map +1 -0
  199. package/dist/ledger.js +345 -0
  200. package/dist/ledger.js.map +1 -0
  201. package/dist/mcp-tools/projectTasks.d.ts.map +1 -1
  202. package/dist/mcp-tools/projectTasks.js +9 -14
  203. package/dist/mcp-tools/projectTasks.js.map +1 -1
  204. package/dist/mcp.js +0 -0
  205. package/dist/mcpAuditJobs.d.ts.map +1 -1
  206. package/dist/mcpAuditJobs.js +3 -0
  207. package/dist/mcpAuditJobs.js.map +1 -1
  208. package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
  209. package/dist/reporting/csvReportGenerator.js +7 -1
  210. package/dist/reporting/csvReportGenerator.js.map +1 -1
  211. package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
  212. package/dist/reporting/htmlReportGenerator.js +5 -1
  213. package/dist/reporting/htmlReportGenerator.js.map +1 -1
  214. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  215. package/dist/reporting/jsonReportGenerator.js +10 -2
  216. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  217. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  218. package/dist/reporting/sarifReportGenerator.js +17 -17
  219. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  220. package/dist/scripts/runBench.d.ts +94 -0
  221. package/dist/scripts/runBench.d.ts.map +1 -0
  222. package/dist/scripts/runBench.js +1559 -0
  223. package/dist/scripts/runBench.js.map +1 -0
  224. package/dist/search/QueryParser.d.ts.map +1 -1
  225. package/dist/search/QueryParser.js +51 -4
  226. package/dist/search/QueryParser.js.map +1 -1
  227. package/dist/services/CodeMapGenerator.d.ts +33 -0
  228. package/dist/services/CodeMapGenerator.d.ts.map +1 -1
  229. package/dist/services/CodeMapGenerator.js +404 -0
  230. package/dist/services/CodeMapGenerator.js.map +1 -1
  231. package/dist/styles/normalizer.d.ts +37 -0
  232. package/dist/styles/normalizer.d.ts.map +1 -0
  233. package/dist/styles/normalizer.js +301 -0
  234. package/dist/styles/normalizer.js.map +1 -0
  235. package/dist/styles/styleExtractor.d.ts +38 -0
  236. package/dist/styles/styleExtractor.d.ts.map +1 -0
  237. package/dist/styles/styleExtractor.js +630 -0
  238. package/dist/styles/styleExtractor.js.map +1 -0
  239. package/dist/styles/styleIndexer.d.ts +35 -0
  240. package/dist/styles/styleIndexer.d.ts.map +1 -0
  241. package/dist/styles/styleIndexer.js +266 -0
  242. package/dist/styles/styleIndexer.js.map +1 -0
  243. package/dist/styles/tailwindConfigLoader.d.ts +38 -0
  244. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -0
  245. package/dist/styles/tailwindConfigLoader.js +374 -0
  246. package/dist/styles/tailwindConfigLoader.js.map +1 -0
  247. package/dist/styles/tailwindExpander.d.ts +38 -0
  248. package/dist/styles/tailwindExpander.d.ts.map +1 -0
  249. package/dist/styles/tailwindExpander.js +637 -0
  250. package/dist/styles/tailwindExpander.js.map +1 -0
  251. package/dist/styles/types.d.ts +92 -0
  252. package/dist/styles/types.d.ts.map +1 -0
  253. package/dist/styles/types.js +10 -0
  254. package/dist/styles/types.js.map +1 -0
  255. package/dist/symbols.d.ts +21 -0
  256. package/dist/symbols.d.ts.map +1 -0
  257. package/dist/symbols.js +31 -0
  258. package/dist/symbols.js.map +1 -0
  259. package/dist/types.d.ts +326 -3
  260. package/dist/types.d.ts.map +1 -1
  261. package/dist/types.js.map +1 -1
  262. package/package.json +9 -5
  263. package/plugin/.claude-plugin/plugin.json +1 -1
  264. package/plugin/scripts/hook-audit.sh +5 -1
  265. package/plugin/skills/code-auditor/SKILL.md +51 -0
  266. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts +0 -21
  267. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts.map +0 -1
  268. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js +0 -21
  269. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js.map +0 -1
  270. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts +0 -13
  271. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts.map +0 -1
  272. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js +0 -12
  273. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js.map +0 -1
  274. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts +0 -11
  275. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts.map +0 -1
  276. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js +0 -22
  277. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js.map +0 -1
  278. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts +0 -9
  279. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts.map +0 -1
  280. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js +0 -13
  281. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js.map +0 -1
  282. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts +0 -13
  283. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts.map +0 -1
  284. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js +0 -18
  285. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js.map +0 -1
  286. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts +0 -10
  287. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts.map +0 -1
  288. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js +0 -16
  289. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js.map +0 -1
  290. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts +0 -14
  291. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts.map +0 -1
  292. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js +0 -18
  293. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js.map +0 -1
  294. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts +0 -13
  295. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts.map +0 -1
  296. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js +0 -22
  297. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js.map +0 -1
  298. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts +0 -10
  299. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts.map +0 -1
  300. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js +0 -20
  301. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js.map +0 -1
  302. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts +0 -18
  303. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts.map +0 -1
  304. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js +0 -28
  305. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js.map +0 -1
  306. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts +0 -11
  307. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts.map +0 -1
  308. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js +0 -31
  309. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js.map +0 -1
  310. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts +0 -11
  311. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts.map +0 -1
  312. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js +0 -18
  313. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js.map +0 -1
  314. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts +0 -13
  315. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts.map +0 -1
  316. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js +0 -61
  317. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js.map +0 -1
  318. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts +0 -10
  319. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts.map +0 -1
  320. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js +0 -15
  321. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js.map +0 -1
  322. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts +0 -11
  323. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts.map +0 -1
  324. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js +0 -49
  325. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js.map +0 -1
  326. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts +0 -10
  327. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts.map +0 -1
  328. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js +0 -19
  329. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js.map +0 -1
  330. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts +0 -37
  331. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts.map +0 -1
  332. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js +0 -73
  333. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js.map +0 -1
  334. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts +0 -12
  335. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts.map +0 -1
  336. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js +0 -82
  337. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js.map +0 -1
  338. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts +0 -10
  339. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts.map +0 -1
  340. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js +0 -15
  341. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js.map +0 -1
  342. package/dist/analyzers/dataAccessAnalyzer.d.ts +0 -46
  343. package/dist/analyzers/dataAccessAnalyzer.d.ts.map +0 -1
  344. package/dist/analyzers/dataAccessAnalyzer.js +0 -474
  345. package/dist/analyzers/dataAccessAnalyzer.js.map +0 -1
  346. package/dist/analyzers/dryAnalyzer.d.ts +0 -31
  347. package/dist/analyzers/dryAnalyzer.d.ts.map +0 -1
  348. package/dist/analyzers/dryAnalyzer.js +0 -734
  349. package/dist/analyzers/dryAnalyzer.js.map +0 -1
  350. package/dist/analyzers/schemaAnalyzer.d.ts +0 -23
  351. package/dist/analyzers/schemaAnalyzer.d.ts.map +0 -1
  352. package/dist/analyzers/schemaAnalyzer.js +0 -564
  353. package/dist/analyzers/schemaAnalyzer.js.map +0 -1
  354. package/dist/analyzers/solidAnalyzer.d.ts +0 -33
  355. package/dist/analyzers/solidAnalyzer.d.ts.map +0 -1
  356. package/dist/analyzers/solidAnalyzer.js +0 -847
  357. package/dist/analyzers/solidAnalyzer.js.map +0 -1
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { CodeMapGenerator } from './services/CodeMapGenerator.js';
18
18
  import { initParsers } from './languages/index.js';
19
19
  import { queryParser } from './search/QueryParser.js';
20
20
  import { CodeIndexDB } from './codeIndexDB.js';
21
+ import { createBaselineFromFindings, saveBaseline, loadBaseline, diffBaselines } from './baseline.js';
21
22
  // Get package.json for version info
22
23
  const __filename = fileURLToPath(import.meta.url);
23
24
  const __dirname = dirname(__filename);
@@ -37,6 +38,9 @@ program
37
38
  .option('-o, --output <dir>', 'Output directory for reports')
38
39
  .option('-f, --format <format>', 'Report format: html, json, csv, or sarif')
39
40
  .option('--fail-on <severity>', 'Exit code 2 when violations at or above this severity exist')
41
+ .option('--full', 'Show full violation inventory (overrides default delta view when baseline exists)')
42
+ .option('--include-baseline', 'Evaluate baseline-known violations in --fail-on checks')
43
+ .option('--fail-on-regression', 'Exit code 2 when total advisory debt exceeds the baseline snapshot')
40
44
  .action(async (options) => {
41
45
  console.log(chalk.blue('šŸ” Code Quality Audit Tool'));
42
46
  console.log(chalk.gray('══════════════════════════════════════════════════'));
@@ -55,10 +59,88 @@ program
55
59
  outputDirectory: options.output
56
60
  });
57
61
  const result = await runner.run();
58
- console.log(`\nFound ${result.summary.totalViolations} violations`);
59
- console.log(`Critical: ${result.summary.criticalIssues}`);
60
- console.log(`Warnings: ${result.summary.warnings}`);
61
- console.log(`Suggestions: ${result.summary.suggestions}`);
62
+ const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
63
+ const baseline = result.metadata?.baseline;
64
+ // ── Delta output (Spec 18 R2) ─────────────────────────────────
65
+ if (baseline && !options.full) {
66
+ const newViolations = violations.filter((v) => v.new === true);
67
+ const knownCount = baseline.knownCount ?? 0;
68
+ const fixedCount = baseline.fixedCount ?? 0;
69
+ const previousKnown = baseline.previousKnownCount ?? 0;
70
+ const currentDebt = newViolations.length + knownCount;
71
+ const debtDelta = currentDebt - previousKnown;
72
+ const trendIcon = debtDelta > 0 ? '↑' : debtDelta < 0 ? '↓' : '→';
73
+ const trendLabel = debtDelta > 0
74
+ ? `(debt increased since last baseline)`
75
+ : debtDelta < 0
76
+ ? `(debt decreased since last baseline)`
77
+ : '(unchanged)';
78
+ console.log(`\nšŸ“Š Delta: +${newViolations.length} new Ā· āˆ’${fixedCount} fixed Ā· ${knownCount} known ${trendIcon} ${trendLabel}`);
79
+ if (newViolations.length > 0) {
80
+ console.log(chalk.gray(`\n── New Findings (${newViolations.length}) ──────────────────────────`));
81
+ for (const v of newViolations) {
82
+ const icon = v.severity === 'critical' ? 'šŸ”“' :
83
+ v.severity === 'warning' ? '🟔' : 'šŸ”µ';
84
+ console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}`);
85
+ }
86
+ }
87
+ else {
88
+ console.log(chalk.green('\nāœ“ No new findings since last baseline.'));
89
+ }
90
+ // Debt by analyzer
91
+ console.log(chalk.gray(`\n── Debt by Analyzer ──────────────────────────`));
92
+ const analyzerCounts = {};
93
+ for (const v of violations) {
94
+ const a = v.analyzer || 'unknown';
95
+ if (!analyzerCounts[a])
96
+ analyzerCounts[a] = { known: 0, new: 0 };
97
+ if (v.new === false)
98
+ analyzerCounts[a].known++;
99
+ else if (v.new === true)
100
+ analyzerCounts[a].new++;
101
+ }
102
+ for (const [analyzer, counts] of Object.entries(analyzerCounts).sort()) {
103
+ const newPart = counts.new > 0 ? ` (+${counts.new})` : '';
104
+ console.log(`${analyzer}: ${counts.known.toLocaleString()} known${newPart}`);
105
+ }
106
+ // Top files
107
+ console.log(chalk.gray(`\n── Top Files ─────────────────────────────────`));
108
+ const fileCounts = new Map();
109
+ for (const v of violations) {
110
+ const f = v.file || '';
111
+ fileCounts.set(f, (fileCounts.get(f) || 0) + 1);
112
+ }
113
+ const topFiles = [...fileCounts.entries()]
114
+ .sort((a, b) => b[1] - a[1])
115
+ .slice(0, 5);
116
+ for (const [file, count] of topFiles) {
117
+ console.log(`${file} — ${count} finding${count !== 1 ? 's' : ''}`);
118
+ }
119
+ console.log(chalk.gray(`\nšŸ’” Run ${chalk.cyan('code-audit --full')} to see all ${currentDebt.toLocaleString()} findings.`));
120
+ }
121
+ else if (!baseline) {
122
+ // No baseline: current behavior + hint
123
+ console.log(`\nFound ${result.summary.totalViolations} violations`);
124
+ console.log(`Critical: ${result.summary.criticalIssues}`);
125
+ console.log(`Warnings: ${result.summary.warnings}`);
126
+ console.log(`Suggestions: ${result.summary.suggestions}`);
127
+ console.log(chalk.gray(`\nšŸ’” Run ${chalk.cyan('code-audit baseline')} to adopt the ratchet and track changes over time.`));
128
+ }
129
+ else {
130
+ // --full with baseline: full itemized inventory (current behavior)
131
+ console.log(`\nFound ${result.summary.totalViolations} violations`);
132
+ console.log(`Critical: ${result.summary.criticalIssues}`);
133
+ console.log(`Warnings: ${result.summary.warnings}`);
134
+ console.log(`Suggestions: ${result.summary.suggestions}`);
135
+ }
136
+ // Spec-20 R4: built-in profile visibility — silent behavior changes
137
+ // are never acceptable. Notify when scripts-and-tests capped findings.
138
+ const builtinCapped = Object.values(result.analyzerResults)
139
+ .reduce((count, ar) => count + ar.violations.filter(v => v.profile === 'scripts-and-tests').length, 0);
140
+ if (builtinCapped > 0) {
141
+ console.log(chalk.blue(`\nā„¹ļø ${builtinCapped.toLocaleString()} findings capped by built-in profile "scripts-and-tests" (scripts/tests/fixtures → suggestion).`));
142
+ console.log(chalk.gray(` Set ${chalk.cyan('"builtin": false')} in .codeauditor.json to disable.`));
143
+ }
62
144
  // Generate formatted report if --format is specified
63
145
  if (options.format) {
64
146
  const validFormats = ['html', 'json', 'csv', 'sarif'];
@@ -74,12 +156,24 @@ program
74
156
  await fs.writeFile(reportPath, report, 'utf-8');
75
157
  console.log(chalk.green(`\nReport written to ${reportPath}`));
76
158
  }
77
- // Exit code based on --fail-on
159
+ // ── Fail-on logic (Spec 18 R3) ───────────────────────────────
160
+ // --fail-on-regression: compare total advisory debt to baseline snapshot
161
+ if (baseline && options.failOnRegression) {
162
+ const currentDebt = violations.filter((v) => v.new || v.new === false).length;
163
+ const snapshotDebt = baseline.previousKnownCount ?? 0;
164
+ if (currentDebt > snapshotDebt) {
165
+ console.error(chalk.red(`Debt regression: ${currentDebt - snapshotDebt} findings added without re-baselining.`));
166
+ process.exit(2);
167
+ }
168
+ }
169
+ // --fail-on: evaluate new + invariant findings only (unless --include-baseline)
78
170
  if (failOnSeverity) {
79
- const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
171
+ const evaluableViolations = (baseline && !options.includeBaseline)
172
+ ? violations.filter((v) => v.new || v.analyzer === 'invariants')
173
+ : violations;
80
174
  const severityOrder = ['critical', 'warning', 'suggestion'];
81
175
  const failIndex = severityOrder.indexOf(failOnSeverity);
82
- const hasAtOrAbove = violations.some((v) => {
176
+ const hasAtOrAbove = evaluableViolations.some((v) => {
83
177
  const vIndex = severityOrder.indexOf(v.severity);
84
178
  return vIndex >= 0 && vIndex <= failIndex;
85
179
  });
@@ -101,6 +195,7 @@ program
101
195
  .option('-f, --format <format>', 'Report format: json, or sarif (overrides --json)')
102
196
  .option('--quiet', 'Suppress output when zero violations')
103
197
  .option('--fail-on <severity>', 'Exit code 2 when violations at or above this severity exist', 'critical')
198
+ .option('--include-baseline', 'Evaluate baseline-known violations in --fail-on checks')
104
199
  .option('--stdin', 'Read file paths from stdin (one per line)')
105
200
  .option('-p, --path <projectPath>', 'Project root path', process.cwd())
106
201
  .action(async (paths, options) => {
@@ -147,6 +242,11 @@ program
147
242
  const result = await runner.run();
148
243
  // Collect all violations
149
244
  const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
245
+ // Baseline classification (Spec 18 R4 — hook path)
246
+ const baseline = result.metadata?.baseline;
247
+ const knownViolations = violations.filter((v) => v.new === false);
248
+ const newViolations = violations.filter((v) => v.new === true || v.new === undefined);
249
+ const knownCount = knownViolations.length;
150
250
  // JSON output
151
251
  if (options.format === 'sarif') {
152
252
  const { generateSARIFReport } = await import('./reporting/sarifReportGenerator.js');
@@ -179,7 +279,8 @@ program
179
279
  endColumn: v.end?.column,
180
280
  enclosingSymbol: v.symbol || v.enclosingFunction || '',
181
281
  suggestion: v.suggestion || '',
182
- details: v.details || ''
282
+ details: v.details || '',
283
+ ...(v.new !== undefined && { new: v.new })
183
284
  };
184
285
  });
185
286
  process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
@@ -193,20 +294,30 @@ program
193
294
  console.log(`Critical: ${result.summary.criticalIssues}`);
194
295
  console.log(`Warnings: ${result.summary.warnings}`);
195
296
  console.log(`Suggestions: ${result.summary.suggestions}`);
297
+ // Known-finding informational line (Spec 18 R4)
298
+ if (baseline && knownCount > 0) {
299
+ console.log(chalk.gray(`\nā„¹ļø ${knownCount} pre-existing finding${knownCount !== 1 ? 's' : ''} in files you touched (not blocking).`));
300
+ }
196
301
  if (violations.length > 0) {
197
302
  console.log(chalk.gray('\n── Violations ────────────────────────────────────'));
198
303
  for (const v of violations) {
199
304
  const icon = v.severity === 'critical' ? 'šŸ”“' :
200
305
  v.severity === 'warning' ? '🟔' : 'šŸ”µ';
201
- console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}`);
306
+ const statusTag = v.new === false
307
+ ? chalk.dim(' [known]')
308
+ : '';
309
+ console.log(`${icon} ${chalk.bold(v.file)}${v.line ? `:${v.line}` : ''} [${v.severity}] ${v.message}${statusTag}`);
202
310
  }
203
311
  }
204
312
  }
205
- // Exit code based on --fail-on
313
+ // Exit code based on --fail-on (Spec 18 R3/R4)
206
314
  if (failOnSeverity) {
315
+ const evaluableViolations = (baseline && !options.includeBaseline)
316
+ ? violations.filter((v) => v.new || v.analyzer === 'invariants')
317
+ : violations;
207
318
  const severityOrder = ['critical', 'warning', 'suggestion'];
208
319
  const failIndex = severityOrder.indexOf(failOnSeverity);
209
- const hasAtOrAbove = violations.some((v) => {
320
+ const hasAtOrAbove = evaluableViolations.some((v) => {
210
321
  const vIndex = severityOrder.indexOf(v.severity);
211
322
  return vIndex >= 0 && vIndex <= failIndex;
212
323
  });
@@ -220,6 +331,72 @@ program
220
331
  process.exit(1);
221
332
  }
222
333
  });
334
+ // Baseline command (Spec 18 R1)
335
+ program
336
+ .command('baseline')
337
+ .description('Snapshot current advisory findings as the baseline (excludes invariants)')
338
+ .option('-p, --path <path>', 'Project path', process.cwd())
339
+ .option('--json', 'Output as JSON')
340
+ .action(async (options) => {
341
+ try {
342
+ await initParsers();
343
+ const runner = createAuditRunner({
344
+ projectRoot: options.path,
345
+ });
346
+ const result = await runner.run();
347
+ // Collect all violations from analyzer results
348
+ const allViolations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
349
+ // Compute corpus stats
350
+ const corpusStats = {
351
+ files: result.metadata.filesAnalyzed,
352
+ functions: result.metadata.collectedFunctions?.length ?? 0,
353
+ };
354
+ // Build per-analyzer counts
355
+ const analyzerCounts = {};
356
+ for (const v of allViolations) {
357
+ if (v.analyzer === 'invariants')
358
+ continue;
359
+ const a = v.analyzer || 'unknown';
360
+ analyzerCounts[a] = (analyzerCounts[a] || 0) + 1;
361
+ }
362
+ // Create new baseline
363
+ const newBaseline = createBaselineFromFindings(allViolations, {
364
+ toolVersion: packageJson.version,
365
+ totalFindings: allViolations.filter((v) => v.analyzer !== 'invariants').length,
366
+ analyzerCounts,
367
+ corpusStats,
368
+ });
369
+ // Load existing baseline for diff
370
+ const projectRoot = resolve(options.path || process.cwd());
371
+ const existing = loadBaseline(projectRoot);
372
+ const diff = existing ? diffBaselines(existing, newBaseline) : null;
373
+ // Save
374
+ saveBaseline(projectRoot, newBaseline);
375
+ if (options.json) {
376
+ process.stdout.write(JSON.stringify({
377
+ success: true,
378
+ absorbed: diff?.absorbed ?? newBaseline.entries.length,
379
+ fixed: diff?.fixed ?? 0,
380
+ totalKnown: newBaseline.entries.length,
381
+ invariantsExcluded: allViolations.filter((v) => v.analyzer === 'invariants').length,
382
+ }, null, 2) + '\n');
383
+ }
384
+ else {
385
+ const absorbed = diff?.absorbed ?? newBaseline.entries.length;
386
+ const fixed = diff?.fixed ?? 0;
387
+ const totalKnown = newBaseline.entries.length;
388
+ console.log(chalk.green(`\nāœ“ Baseline updated: ${absorbed} finding${absorbed !== 1 ? 's' : ''} absorbed, ${fixed} fixed, ${totalKnown} total known.`));
389
+ console.log(chalk.gray('Invariants excluded (they always enforce).'));
390
+ if (totalKnown > 0) {
391
+ console.log(chalk.gray(`\nRun ${chalk.cyan('code-audit')} to see your delta view.`));
392
+ }
393
+ }
394
+ }
395
+ catch (error) {
396
+ console.error(chalk.red('Error:'), error);
397
+ process.exit(1);
398
+ }
399
+ });
223
400
  // Generate configuration command
224
401
  program
225
402
  .command('generate-config')
@@ -351,6 +528,75 @@ indexCmd
351
528
  process.exit(1);
352
529
  }
353
530
  });
531
+ // ── Index status command (Spec 14 R1) ──────────────────────────
532
+ indexCmd
533
+ .command('status')
534
+ .description('Show index health and graph statistics')
535
+ .option('--json', 'Output as JSON')
536
+ .action(async (options) => {
537
+ try {
538
+ const db = CodeIndexDB.getInstance();
539
+ await db.initialize();
540
+ const stats = await db.getStats();
541
+ // Graph stats — non-fatal if graph cache is empty
542
+ let graphStats = null;
543
+ try {
544
+ graphStats = db.getGraphStats();
545
+ }
546
+ catch {
547
+ // No graph data yet — ok
548
+ }
549
+ if (options.json) {
550
+ process.stdout.write(JSON.stringify({
551
+ totalFunctions: stats.totalFunctions,
552
+ totalFiles: stats.filesIndexed,
553
+ languageBreakdown: stats.languages,
554
+ graphStats: graphStats ? {
555
+ callNodes: graphStats.callNodes,
556
+ callEdges: graphStats.callEdges,
557
+ unresolvedCalls: graphStats.unresolvedCalls,
558
+ unresolvedShare: graphStats.unresolvedShare,
559
+ importNodes: graphStats.importNodes,
560
+ importEdges: graphStats.importEdges,
561
+ } : null,
562
+ }, null, 2) + '\n');
563
+ }
564
+ else {
565
+ console.log(chalk.blue('šŸ“Š Index Status'));
566
+ console.log(chalk.gray('══════════════════════════════════'));
567
+ console.log(`${chalk.bold('Total functions:')} ${(stats.totalFunctions ?? 0).toLocaleString()}`);
568
+ console.log(`${chalk.bold('Total files:')} ${(stats.filesIndexed ?? 0).toLocaleString()}`);
569
+ if (stats.languages && Object.keys(stats.languages).length > 0) {
570
+ console.log(chalk.bold('\nLanguage breakdown:'));
571
+ for (const [lang, count] of Object.entries(stats.languages)) {
572
+ console.log(` ${lang}: ${count.toLocaleString()}`);
573
+ }
574
+ }
575
+ if (graphStats) {
576
+ console.log(chalk.bold('\nCall Graph:'));
577
+ console.log(` ${chalk.bold('Nodes:')} ${graphStats.callNodes.toLocaleString()}`);
578
+ console.log(` ${chalk.bold('Edges:')} ${graphStats.callEdges.toLocaleString()}`);
579
+ const pct = (graphStats.unresolvedShare * 100).toFixed(1);
580
+ if (graphStats.unresolvedShare > 0.3) {
581
+ console.log(` ${chalk.yellow(`⚠ Unresolved: ${graphStats.unresolvedCalls.toLocaleString()} (${pct}%) — graph metrics may be incomplete`)}`);
582
+ }
583
+ else {
584
+ console.log(` ${chalk.bold('Unresolved:')} ${graphStats.unresolvedCalls.toLocaleString()} (${pct}%)`);
585
+ }
586
+ console.log(chalk.bold('\nImport Graph:'));
587
+ console.log(` ${chalk.bold('Nodes:')} ${graphStats.importNodes.toLocaleString()}`);
588
+ console.log(` ${chalk.bold('Edges:')} ${graphStats.importEdges.toLocaleString()}`);
589
+ }
590
+ else {
591
+ console.log(chalk.gray('\nNo graph data available. Run a full sync (code-audit index sync) first.'));
592
+ }
593
+ }
594
+ }
595
+ catch (error) {
596
+ console.error(chalk.red('Error:'), error);
597
+ process.exit(1);
598
+ }
599
+ });
354
600
  // Config command
355
601
  const configCmd = program
356
602
  .command('config')
@@ -359,6 +605,8 @@ const configCmd = program
359
605
  console.log(chalk.yellow('Use a config subcommand:'));
360
606
  console.log(chalk.gray(' code-audit config rules-list List invariant rules from .codeauditor.json'));
361
607
  console.log(chalk.gray(' code-audit config rules-check Validate .codeauditor.json rules'));
608
+ console.log(chalk.gray(' code-audit config profiles Show active path profiles'));
609
+ console.log(chalk.gray(' code-audit config detection Show DB/validator provenance and inferred receivers'));
362
610
  });
363
611
  configCmd
364
612
  .command('rules-list')
@@ -456,6 +704,282 @@ configCmd
456
704
  process.exit(1);
457
705
  }
458
706
  });
707
+ // Config profiles subcommand — debug surface for path profiles (Spec-20 R5)
708
+ configCmd
709
+ .command('profiles')
710
+ .description('Show active path profiles and resolve per-file matching')
711
+ .option('-p, --path <path>', 'Project path', process.cwd())
712
+ .option('--file <file>', 'Resolve profiles for a specific file')
713
+ .option('--json', 'Output as JSON')
714
+ .action(async (options) => {
715
+ try {
716
+ const fs = await import('fs/promises');
717
+ const pathModule = await import('path');
718
+ const { loadConfig } = await import('./config/configLoader.js');
719
+ const { resolvePathProfile } = await import('./config/pathProfiles.js');
720
+ const projectRoot = pathModule.resolve(options.path);
721
+ const configPath = pathModule.join(projectRoot, '.codeauditor.json');
722
+ let profiles = [];
723
+ try {
724
+ await fs.access(configPath);
725
+ const config = await loadConfig({ configPath });
726
+ profiles = config.pathProfiles || [];
727
+ }
728
+ catch {
729
+ // No config file — use defaults only
730
+ const { mergePathProfiles } = await import('./config/defaults.js');
731
+ profiles = mergePathProfiles(undefined, undefined) || [];
732
+ }
733
+ if (options.file) {
734
+ // Resolve profiles for a specific file
735
+ const filePath = pathModule.resolve(options.file);
736
+ const resolved = resolvePathProfile(filePath, projectRoot, profiles);
737
+ if (options.json) {
738
+ process.stdout.write(JSON.stringify({
739
+ file: options.file,
740
+ resolvedFilePath: filePath,
741
+ matchedProfileNames: resolved.matchedProfileNames,
742
+ severityCap: resolved.severityCap || null,
743
+ overrides: resolved.overrides,
744
+ }, null, 2) + '\n');
745
+ }
746
+ else {
747
+ if (resolved.matchedProfileNames.length === 0) {
748
+ console.log(chalk.gray(`No profiles match: ${options.file}`));
749
+ }
750
+ else {
751
+ console.log(chalk.bold(`Profiles matching: ${options.file}`));
752
+ for (const name of resolved.matchedProfileNames) {
753
+ const profile = profiles.find(p => p.name === name);
754
+ const builtinTag = profile?.builtin !== false && profiles.some(p => p.name === name) ? '' : '';
755
+ console.log(` ${chalk.cyan(name)}${builtinTag ? chalk.gray(' (built-in)') : ''}`);
756
+ }
757
+ if (resolved.severityCap) {
758
+ console.log(chalk.yellow(` severityCap: ${resolved.severityCap}`));
759
+ }
760
+ if (Object.keys(resolved.overrides).length > 0) {
761
+ console.log(chalk.gray(' merged overrides:'));
762
+ for (const [key, value] of Object.entries(resolved.overrides)) {
763
+ console.log(chalk.gray(` ${key}: ${JSON.stringify(value)}`));
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }
769
+ else {
770
+ // List all active profiles
771
+ if (options.json) {
772
+ process.stdout.write(JSON.stringify({
773
+ projectRoot,
774
+ profiles: profiles.map(p => ({
775
+ name: p.name,
776
+ paths: p.paths,
777
+ overrides: p.overrides,
778
+ builtin: p.builtin !== false,
779
+ })),
780
+ }, null, 2) + '\n');
781
+ }
782
+ else {
783
+ if (profiles.length === 0) {
784
+ console.log(chalk.gray('No path profiles active.'));
785
+ }
786
+ else {
787
+ console.log(chalk.bold('Active path profiles:'));
788
+ for (const p of profiles) {
789
+ const tag = p.builtin !== false ? chalk.gray(' [built-in]') : '';
790
+ console.log(` ${chalk.cyan(p.name)}${tag}`);
791
+ console.log(chalk.gray(` paths: ${p.paths.join(', ')}`));
792
+ if (p.overrides && Object.keys(p.overrides).length > 0) {
793
+ for (const [key, value] of Object.entries(p.overrides)) {
794
+ console.log(chalk.gray(` ${key}: ${JSON.stringify(value)}`));
795
+ }
796
+ }
797
+ }
798
+ }
799
+ }
800
+ }
801
+ }
802
+ catch (error) {
803
+ console.error(chalk.red('Error:'), error);
804
+ process.exit(1);
805
+ }
806
+ });
807
+ // Config detection subcommand — provenance debug surface (Spec-21 R2)
808
+ configCmd
809
+ .command('detection')
810
+ .description('Show DB-provenanced and inferred receiver identifiers with per-entry evidence')
811
+ .option('-p, --path <path>', 'Project path', process.cwd())
812
+ .option('--json', 'Output as JSON')
813
+ .option('--mode <mode>', 'Detection mode filter (hybrid, provenance, names)')
814
+ .action(async (options) => {
815
+ try {
816
+ const pathModule = await import('path');
817
+ const fsPromises = await import('fs/promises');
818
+ const { initParsers } = await import('./languages/tree-sitter/parser.js');
819
+ const { initializeLanguages } = await import('./languages/index.js');
820
+ const { LanguageRegistry } = await import('./languages/LanguageRegistry.js');
821
+ const { loadConfig } = await import('./config/configLoader.js');
822
+ const provenanceMod = await import('./analyzers/provenance.js');
823
+ const { buildProvenanceContext, inferReceivers, } = provenanceMod;
824
+ initializeLanguages();
825
+ await initParsers();
826
+ const registry = LanguageRegistry.getInstance();
827
+ const projectRoot = pathModule.resolve(options.path);
828
+ // Load config to get detection mode and analyzer settings
829
+ let config = {};
830
+ try {
831
+ const configPath = pathModule.join(projectRoot, '.codeauditor.json');
832
+ await fsPromises.access(configPath);
833
+ config = await loadConfig({ configPath });
834
+ }
835
+ catch {
836
+ // No config — use defaults
837
+ }
838
+ const detectionMode = options.mode ||
839
+ config?.analyzerOptions?.schema?.detection?.mode ||
840
+ config?.analyzerOptions?.['data-access']?.detection?.mode ||
841
+ 'hybrid';
842
+ // Gather files
843
+ const { findFiles } = await import('./utils/fileDiscovery.js');
844
+ const files = await findFiles(projectRoot);
845
+ const provenanced = [];
846
+ const validatorIdentifiers = [];
847
+ const inferred = [];
848
+ // Per-file scan
849
+ for (const filePath of files.slice(0, 500)) {
850
+ const relPath = pathModule.relative(projectRoot, filePath);
851
+ const adapter = registry.getAdapterForFile(filePath);
852
+ if (!adapter)
853
+ continue;
854
+ try {
855
+ const sourceCode = await fsPromises.readFile(filePath, 'utf-8');
856
+ const ast = await adapter.parse(filePath, sourceCode);
857
+ if (!ast?.root)
858
+ continue;
859
+ const schemaCfg = config?.analyzerOptions?.schema ?? {};
860
+ const daCfg = config?.analyzerOptions?.['data-access'] ?? {};
861
+ // Build provenance context using schema analyzer config (most complete)
862
+ const ctx = buildProvenanceContext(ast, adapter, sourceCode, {
863
+ mode: detectionMode,
864
+ dbReceiverNames: schemaCfg.dbReceiverNames ?? daCfg.dbReceiverNames,
865
+ dbBindingNames: schemaCfg.dbBindingNames ?? daCfg.dbBindingNames,
866
+ dbCallMethods: schemaCfg.dbCallMethods ?? daCfg.dbCallMethods,
867
+ });
868
+ // Collect DB-provenanced
869
+ for (const [id, evidence] of ctx.dbProvenanced) {
870
+ provenanced.push({
871
+ identifier: id,
872
+ file: relPath,
873
+ reason: evidence.reason,
874
+ source: evidence.source,
875
+ chain: evidence.chain.length > 0 ? evidence.chain : undefined,
876
+ });
877
+ }
878
+ // Collect validator-provenanced
879
+ for (const [id, evidence] of ctx.validatorProvenanced) {
880
+ validatorIdentifiers.push({
881
+ identifier: id,
882
+ file: relPath,
883
+ reason: evidence.reason,
884
+ source: evidence.source,
885
+ chain: evidence.chain.length > 0 ? evidence.chain : undefined,
886
+ });
887
+ }
888
+ // Run inference
889
+ if (ctx.dbProvenanced.size > 0) {
890
+ const inf = inferReceivers(ctx.dbProvenanced, ast, adapter, sourceCode);
891
+ for (const ev of inf.evidence) {
892
+ inferred.push({
893
+ identifier: ev.identifier,
894
+ file: relPath,
895
+ reason: ev.reason,
896
+ });
897
+ }
898
+ }
899
+ }
900
+ catch {
901
+ // Skip unparseable files
902
+ }
903
+ }
904
+ // Persist provenance to code index for cross-file consumption (Spec-21 R2)
905
+ try {
906
+ const db = CodeIndexDB.getInstance();
907
+ await db.initialize();
908
+ // Store per-file provenance
909
+ const byFile = new Map();
910
+ for (const p of provenanced) {
911
+ const entry = byFile.get(p.file) || (byFile.set(p.file, { dbProvenanced: [], validatorProvenanced: [] }), byFile.get(p.file));
912
+ entry.dbProvenanced.push(p);
913
+ }
914
+ for (const v of validatorIdentifiers) {
915
+ const entry = byFile.get(v.file) || (byFile.set(v.file, { dbProvenanced: [], validatorProvenanced: [] }), byFile.get(v.file));
916
+ entry.validatorProvenanced.push(v);
917
+ }
918
+ for (const [file, data] of byFile) {
919
+ db.storeFileProvenance(file, data);
920
+ }
921
+ // Store inferred receivers
922
+ db.storeInferredReceivers(inferred);
923
+ }
924
+ catch {
925
+ // Index not available — skip persistence
926
+ }
927
+ if (options.json) {
928
+ process.stdout.write(JSON.stringify({
929
+ mode: detectionMode,
930
+ projectRoot,
931
+ filesScanned: files.slice(0, 500).length,
932
+ dbProvenanced: provenanced,
933
+ validatorProvenanced: validatorIdentifiers,
934
+ inferred,
935
+ }, null, 2) + '\n');
936
+ }
937
+ else {
938
+ console.log(chalk.bold(`Detection mode: ${chalk.cyan(detectionMode)}`));
939
+ console.log(chalk.gray(`Scanned ${files.slice(0, 500).length} files`));
940
+ console.log();
941
+ if (provenanced.length > 0) {
942
+ console.log(chalk.bold('DB-Provenanced Identifiers:'));
943
+ for (const p of provenanced) {
944
+ const reasonColor = p.reason === 'fallback'
945
+ ? chalk.yellow
946
+ : p.reason === 'package'
947
+ ? chalk.green
948
+ : chalk.cyan;
949
+ console.log(` ${chalk.white(p.identifier)} ${chalk.gray(`(${p.file})`)}`);
950
+ console.log(` ${reasonColor(`reason: ${p.reason}`)} — ${chalk.gray(p.source)}`);
951
+ if (p.chain) {
952
+ console.log(` ${chalk.gray(`chain: ${p.chain.join(' → ')}`)}`);
953
+ }
954
+ }
955
+ console.log();
956
+ }
957
+ if (inferred.length > 0) {
958
+ console.log(chalk.bold('Inferred Receivers:'));
959
+ for (const i of inferred) {
960
+ console.log(` ${chalk.white(i.identifier)} ${chalk.gray(`(${i.file})`)}`);
961
+ console.log(` ${chalk.magenta(i.reason)}`);
962
+ }
963
+ console.log();
964
+ }
965
+ if (validatorIdentifiers.length > 0) {
966
+ console.log(chalk.bold('Validator-Provenanced Identifiers:'));
967
+ for (const v of validatorIdentifiers) {
968
+ console.log(` ${chalk.white(v.identifier)} ${chalk.gray(`(${v.file})`)}`);
969
+ console.log(` ${chalk.green(`reason: ${v.reason}`)} — ${chalk.gray(v.source)}`);
970
+ }
971
+ console.log();
972
+ }
973
+ if (provenanced.length === 0 && inferred.length === 0 && validatorIdentifiers.length === 0) {
974
+ console.log(chalk.gray('No DB-provenanced, inferred, or validator-provenanced identifiers found.'));
975
+ }
976
+ }
977
+ }
978
+ catch (error) {
979
+ console.error(chalk.red('Error:'), error);
980
+ process.exit(1);
981
+ }
982
+ });
459
983
  // Search command
460
984
  program
461
985
  .command('search <query>')
@@ -788,6 +1312,456 @@ tasksCmd
788
1312
  process.exit(1);
789
1313
  }
790
1314
  });
1315
+ // ── Ledger command — Spec 11 R1 ──────────────────────────────────────────
1316
+ // ── Hotspots command (Spec 13 R2/R3) ─────────────────────────────────────────
1317
+ program
1318
+ .command('hotspots')
1319
+ .description('Rank files and functions by hotspot score (churn Ɨ complexity)')
1320
+ .option('--limit <n>', 'Max entries to show')
1321
+ .option('--path <dir>', 'Target directory (triggers spot churn extraction if index is empty)')
1322
+ .option('--json', 'Output as JSON')
1323
+ .action(async (options) => {
1324
+ try {
1325
+ const db = CodeIndexDB.getInstance();
1326
+ await db.initialize();
1327
+ const rawDb = db.rawDb;
1328
+ // Trigger a fast churn extraction if no hotspot scores exist yet
1329
+ const existing = rawDb.prepare('SELECT COUNT(*) AS cnt FROM hotspot_scores').get();
1330
+ if (existing.cnt === 0 && options.path) {
1331
+ const { extractChurn } = await import('./churn/churnExtractor.js');
1332
+ const { computeHotspots } = await import('./hotspots/hotspotScorer.js');
1333
+ try {
1334
+ extractChurn(rawDb, options.path, { churnWindowMonths: 12 });
1335
+ computeHotspots(rawDb);
1336
+ }
1337
+ catch {
1338
+ // Graceful: no git repo or extraction failure
1339
+ }
1340
+ }
1341
+ const rows = rawDb.prepare(`
1342
+ SELECT target, type, score, churn_pct, complexity_pct, commit_count,
1343
+ distinct_authors, dominant_author, dominant_author_share,
1344
+ bus_factor_risk, complexity
1345
+ FROM hotspot_scores
1346
+ ORDER BY score DESC
1347
+ ${options.limit ? 'LIMIT ?' : ''}
1348
+ `).all(...(options.limit ? [parseInt(options.limit, 10)] : []));
1349
+ if (options.json) {
1350
+ const entries = rows.map((r) => ({
1351
+ target: r.target,
1352
+ type: r.type,
1353
+ score: Math.round(r.score * 1000) / 1000,
1354
+ churnPercentile: Math.round(r.churn_pct * 1000) / 1000,
1355
+ complexityPercentile: Math.round(r.complexity_pct * 1000) / 1000,
1356
+ commitCount: r.commit_count,
1357
+ distinctAuthors: r.distinct_authors,
1358
+ dominantAuthor: r.dominant_author,
1359
+ dominantAuthorShare: Math.round((r.dominant_author_share ?? 0) * 1000) / 1000,
1360
+ busFactorRisk: !!r.bus_factor_risk,
1361
+ complexity: r.complexity,
1362
+ }));
1363
+ process.stdout.write(JSON.stringify(entries, null, 2) + '\n');
1364
+ }
1365
+ else if (rows.length === 0) {
1366
+ console.log(chalk.gray('No hotspot data found. Run a full sync with --path in a git repo first.'));
1367
+ }
1368
+ else {
1369
+ console.log(chalk.blue(`Hotspots (${rows.length}):\n`));
1370
+ for (const r of rows) {
1371
+ const icon = r.bus_factor_risk ? chalk.red(' ⚠ BUS-FACTOR') : '';
1372
+ const typeColor = r.type === 'function' ? chalk.cyan : chalk.yellow;
1373
+ const scorePct = Math.round(r.score * 100);
1374
+ const bar = scorePct > 0 ? 'ā–ˆ'.repeat(Math.max(1, Math.round(scorePct / 5))) : '';
1375
+ console.log(` ${typeColor(`[${r.type}]`)} ${r.target}` +
1376
+ `\n score: ${chalk.bold(`${scorePct}%`)} ${chalk.gray(bar)}` +
1377
+ ` churn: ${r.commit_count} commits (${r.distinct_authors} authors)` +
1378
+ ` complexity: ${r.complexity}` +
1379
+ `${r.dominant_author ? ` owner: ${r.dominant_author} (${Math.round(r.dominant_author_share * 100)}%)` : ''}` +
1380
+ `${icon}`);
1381
+ }
1382
+ }
1383
+ }
1384
+ catch (error) {
1385
+ console.error(chalk.red('Error:'), error);
1386
+ process.exit(1);
1387
+ }
1388
+ });
1389
+ // ── Risk command — Spec 14 R2 ───────────────────────────────────────
1390
+ program
1391
+ .command('risk')
1392
+ .description('Rank functions by architectural risk (PageRank Ɨ betweenness Ɨ complexity Ɨ untested)')
1393
+ .option('--limit <n>', 'Max entries to show', '20')
1394
+ .option('--path <dir>', 'Project path', process.cwd())
1395
+ .option('--json', 'Output as JSON')
1396
+ .option('--format <format>', 'Output format: dot (call-graph neighborhood of top functions)')
1397
+ .action(async (options) => {
1398
+ try {
1399
+ const db = CodeIndexDB.getInstance();
1400
+ await db.initialize();
1401
+ const rawDb = db.rawDb;
1402
+ const { computeRisk, buildCallGraphFromCache } = await import('./graph/callGraph.js');
1403
+ const { graph: callGraph } = buildCallGraphFromCache(rawDb);
1404
+ const riskEntries = computeRisk(rawDb, callGraph.adjacency, callGraph.nodeIds, callGraph.nodeNames, callGraph.nodePaths);
1405
+ const limit = parseInt(options.limit, 10) || 20;
1406
+ const top = riskEntries.slice(0, limit);
1407
+ if (options.format === 'dot') {
1408
+ // DOT output: call-graph neighborhood of top-N risk functions
1409
+ const { callGraphToDot } = await import('./graph/outputFormatter.js');
1410
+ // Collect IDs for top risk functions
1411
+ const topIds = new Set();
1412
+ for (const e of top) {
1413
+ const rows = rawDb.prepare('SELECT id FROM functions WHERE name = ? AND file_path = ?').all(e.functionName, e.filePath);
1414
+ for (const r of rows)
1415
+ topIds.add(r.id);
1416
+ }
1417
+ const dot = callGraphToDot(callGraph, {
1418
+ label: `Call Graph — Top ${top.length} Risk Functions`,
1419
+ maxNodes: 60,
1420
+ rankdir: 'LR',
1421
+ showWeights: true,
1422
+ });
1423
+ process.stdout.write(dot);
1424
+ }
1425
+ else if (options.json) {
1426
+ process.stdout.write(JSON.stringify(top.map(e => ({
1427
+ functionName: e.functionName,
1428
+ filePath: e.filePath,
1429
+ pageRankPercentile: Math.round(e.pageRankPercentile * 1000) / 1000,
1430
+ betweennessPercentile: Math.round(e.betweennessPercentile * 1000) / 1000,
1431
+ complexityPercentile: Math.round(e.complexityPercentile * 1000) / 1000,
1432
+ untested: e.untested,
1433
+ riskScore: Math.round(e.riskScore * 1000) / 1000,
1434
+ })), null, 2) + '\n');
1435
+ }
1436
+ else if (top.length === 0) {
1437
+ console.log(chalk.gray('No risk data available. Run a full sync (code-audit index sync) first.'));
1438
+ }
1439
+ else {
1440
+ console.log(chalk.blue(`Risk Rankings (${top.length} of ${riskEntries.length}):\n`));
1441
+ console.log(chalk.gray('Rank Function File PR% BW% CX% Untested Risk'));
1442
+ console.log(chalk.gray('──── ──────── ──── ─── ─── ─── ──────── ────'));
1443
+ for (let i = 0; i < top.length; i++) {
1444
+ const e = top[i];
1445
+ const fnName = e.functionName.length > 36 ? e.functionName.slice(0, 33) + '...' : e.functionName.padEnd(36);
1446
+ const fp = (e.filePath || '').length > 36 ? '...' + (e.filePath || '').slice(-33) : (e.filePath || '').padEnd(36);
1447
+ const prPct = String(Math.round(e.pageRankPercentile * 100)).padStart(4);
1448
+ const bwPct = String(Math.round(e.betweennessPercentile * 100)).padStart(4);
1449
+ const cxPct = String(Math.round(e.complexityPercentile * 100)).padStart(4);
1450
+ const untested = e.untested ? chalk.red(' āœ— ') : chalk.green(' āœ“ ');
1451
+ const riskScore = String((Math.round(e.riskScore * 1000) / 1000).toFixed(3)).padStart(6);
1452
+ console.log(` ${chalk.dim(String(i + 1).padStart(3))} ${chalk.bold(fnName)} ${chalk.green(fp)} ${prPct} ${bwPct} ${cxPct} ${untested} ${chalk.yellow(riskScore)}`);
1453
+ }
1454
+ console.log(chalk.gray('\nPR% = PageRank percentile, BW% = Betweenness percentile, CX% = Complexity percentile'));
1455
+ console.log(chalk.gray('Risk = max(PR%, BW%) Ɨ CX% Ɨ (1 + untested)'));
1456
+ }
1457
+ }
1458
+ catch (error) {
1459
+ console.error(chalk.red('Error:'), error);
1460
+ process.exit(1);
1461
+ }
1462
+ });
1463
+ const ledgerCmd = program
1464
+ .command('ledger')
1465
+ .description('View and manage the findings ledger (append-only audit history)');
1466
+ ledgerCmd
1467
+ .command('list')
1468
+ .description('List recent audit runs')
1469
+ .option('--limit <n>', 'Max runs to show', '20')
1470
+ .option('--json', 'Output as JSON')
1471
+ .action(async (options) => {
1472
+ try {
1473
+ const { listRuns } = await import('./ledger.js');
1474
+ const db = CodeIndexDB.getInstance();
1475
+ await db.initialize();
1476
+ const runs = listRuns(db.rawDb);
1477
+ const limit = parseInt(options.limit, 10) || 20;
1478
+ const limited = runs.slice(0, limit);
1479
+ if (options.json) {
1480
+ process.stdout.write(JSON.stringify(limited, null, 2) + '\n');
1481
+ }
1482
+ else if (limited.length === 0) {
1483
+ console.log(chalk.gray('No audit runs in the ledger yet. Run an audit first.'));
1484
+ }
1485
+ else {
1486
+ console.log(chalk.blue(`Ledger runs (${limited.length} of ${runs.length}):`));
1487
+ for (const r of limited) {
1488
+ const dirty = r.gitDirty ? chalk.yellow(' [dirty]') : '';
1489
+ console.log(` ${chalk.bold(r.runId.slice(0, 8))} ${r.timestamp} ${r.surface}/${r.scope} ${r.findingCount} findings ${r.durationMs}ms exit=${r.exitStatus}${dirty}`);
1490
+ }
1491
+ }
1492
+ }
1493
+ catch (error) {
1494
+ console.error(chalk.red('Error:'), error);
1495
+ process.exit(1);
1496
+ }
1497
+ });
1498
+ ledgerCmd
1499
+ .command('export')
1500
+ .description('Export ledger as JSON')
1501
+ .option('--since <iso>', 'Only runs from this ISO timestamp')
1502
+ .option('--json', 'Output as JSON (always on for export)')
1503
+ .action(async (options) => {
1504
+ try {
1505
+ const { exportLedger } = await import('./ledger.js');
1506
+ const db = CodeIndexDB.getInstance();
1507
+ await db.initialize();
1508
+ const data = exportLedger(db.rawDb, options.since);
1509
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
1510
+ }
1511
+ catch (error) {
1512
+ console.error(chalk.red('Error:'), error);
1513
+ process.exit(1);
1514
+ }
1515
+ });
1516
+ ledgerCmd
1517
+ .command('stats')
1518
+ .description('Show aggregated ledger statistics')
1519
+ .option('--json', 'Output as JSON')
1520
+ .action(async (options) => {
1521
+ try {
1522
+ const { getLedgerStats } = await import('./ledger.js');
1523
+ const db = CodeIndexDB.getInstance();
1524
+ await db.initialize();
1525
+ const stats = getLedgerStats(db.rawDb);
1526
+ if (options.json) {
1527
+ process.stdout.write(JSON.stringify(stats, null, 2) + '\n');
1528
+ }
1529
+ else {
1530
+ console.log(chalk.blue(`Total runs: ${stats.totalRuns}`));
1531
+ for (const [analyzer, a] of Object.entries(stats.perAnalyzer)) {
1532
+ console.log(chalk.bold(`\n${analyzer} (${a.total} findings):`));
1533
+ for (const [rule, count] of Object.entries(a.perRule)) {
1534
+ console.log(` ${rule}: ${count}`);
1535
+ }
1536
+ const sev = a.severityDistribution;
1537
+ if (Object.keys(sev).length > 0) {
1538
+ console.log(` severity: ${Object.entries(sev).map(([k, v]) => `${k}=${v}`).join(', ')}`);
1539
+ }
1540
+ }
1541
+ }
1542
+ }
1543
+ catch (error) {
1544
+ console.error(chalk.red('Error:'), error);
1545
+ process.exit(1);
1546
+ }
1547
+ });
1548
+ ledgerCmd
1549
+ .command('trends')
1550
+ .description('Compare consecutive full-audit runs and report new vs fixed findings per rule')
1551
+ .option('--since <runId>', 'Only consider runs after this run ID')
1552
+ .option('--json', 'Output as JSON')
1553
+ .action(async (options) => {
1554
+ try {
1555
+ const { getTrends } = await import('./ledger.js');
1556
+ const db = CodeIndexDB.getInstance();
1557
+ await db.initialize();
1558
+ const trends = getTrends(db.rawDb, options.since);
1559
+ if (options.json) {
1560
+ process.stdout.write(JSON.stringify(trends, null, 2) + '\n');
1561
+ }
1562
+ else if (!trends) {
1563
+ console.log(chalk.gray('Need at least 2 full-audit runs of the same target for trend analysis.'));
1564
+ }
1565
+ else {
1566
+ console.log(chalk.blue(`Trends for ${chalk.bold(trends.target)}`));
1567
+ console.log(chalk.gray(` ${trends.runPairs.length} run pairs from ${trends.timeRange.start} to ${trends.timeRange.end}\n`));
1568
+ // Sort rules by |net| descending
1569
+ const rules = Object.values(trends.perRule).sort((a, b) => Math.abs(b.net) - Math.abs(a.net));
1570
+ if (rules.length === 0) {
1571
+ console.log(chalk.gray('No trend changes detected — findings are stable across runs.'));
1572
+ }
1573
+ else {
1574
+ for (const r of rules) {
1575
+ const netIcon = r.net > 0 ? chalk.green(`↓${r.net}`) : r.net < 0 ? chalk.red(`↑${-r.net}`) : chalk.gray('→0');
1576
+ console.log(` ${chalk.bold(r.rule)}` +
1577
+ ` new: ${chalk.red(r.newCount)}` +
1578
+ ` fixed: ${chalk.green(r.fixedCount)}` +
1579
+ ` net: ${netIcon}`);
1580
+ }
1581
+ }
1582
+ // Comparison basis
1583
+ console.log(chalk.gray(`\nComparison basis: target=${trends.target}, ${trends.runPairs.length} consecutive full-audit pairs`));
1584
+ if (options.since) {
1585
+ console.log(chalk.gray(`Filter: since run ${options.since.slice(0, 8)}`));
1586
+ }
1587
+ }
1588
+ }
1589
+ catch (error) {
1590
+ console.error(chalk.red('Error:'), error);
1591
+ process.exit(1);
1592
+ }
1593
+ });
1594
+ ledgerCmd
1595
+ .command('import')
1596
+ .description('Import D1 interim ledger files from a directory')
1597
+ .requiredOption('--dir <path>', 'Directory of JSON run files')
1598
+ .action(async (options) => {
1599
+ try {
1600
+ const { importLedgerFromDir } = await import('./ledger.js');
1601
+ const db = CodeIndexDB.getInstance();
1602
+ await db.initialize();
1603
+ const result = importLedgerFromDir(db.rawDb, options.dir);
1604
+ console.log(chalk.green(`āœ“ Imported ${result.imported} runs, skipped ${result.skipped}`));
1605
+ }
1606
+ catch (error) {
1607
+ console.error(chalk.red('Error:'), error);
1608
+ process.exit(1);
1609
+ }
1610
+ });
1611
+ // ── Conventions command — Spec 12 R3 ────────────────────────────────────────
1612
+ const conventionsCmd = program
1613
+ .command('conventions')
1614
+ .description('List mined codebase conventions and propose enforceable rules');
1615
+ conventionsCmd
1616
+ .command('list')
1617
+ .description('List mined conventions from the code index')
1618
+ .option('--domain <domain>', 'Filter by domain: usage-pair, import-form, error-handling, export-shape, naming')
1619
+ .option('--json', 'Output as JSON')
1620
+ .action(async (options) => {
1621
+ try {
1622
+ const db = CodeIndexDB.getInstance();
1623
+ await db.initialize();
1624
+ const rawDb = db.rawDb;
1625
+ let sql = 'SELECT * FROM conventions';
1626
+ let params = [];
1627
+ if (options.domain) {
1628
+ sql += ' WHERE domain = ?';
1629
+ params = [options.domain];
1630
+ }
1631
+ sql += ' ORDER BY domain, directory, confidence DESC';
1632
+ const conventions = rawDb.prepare(sql).all(...params);
1633
+ if (options.json) {
1634
+ process.stdout.write(JSON.stringify(conventions, null, 2) + '\n');
1635
+ }
1636
+ else if (conventions.length === 0) {
1637
+ console.log(chalk.gray('No conventions mined yet. Run a full audit or index sync first.'));
1638
+ }
1639
+ else {
1640
+ // Group by domain for display
1641
+ const byDomain = new Map();
1642
+ for (const c of conventions) {
1643
+ const list = byDomain.get(c.domain) ?? [];
1644
+ list.push(c);
1645
+ byDomain.set(c.domain, list);
1646
+ }
1647
+ console.log(chalk.blue(`Mined conventions (${conventions.length} total):\n`));
1648
+ for (const [domain, list] of byDomain) {
1649
+ console.log(chalk.bold(` ${domain} (${list.length})`));
1650
+ for (const c of list.slice(0, 5)) {
1651
+ const exemplar = c.exemplar_file
1652
+ ? ` (exemplar: ${c.exemplar_file}${c.exemplar_line ? `:${c.exemplar_line}` : ''})`
1653
+ : '';
1654
+ const pct = Math.round(c.confidence * 100);
1655
+ const desc = domain === 'usage-pair'
1656
+ ? `${c.antecedent} → ${c.consequent}`
1657
+ : domain === 'import-form'
1658
+ ? `${c.antecedent} → ${c.consequent}`
1659
+ : domain === 'error-handling'
1660
+ ? `shape: ${c.pattern}`
1661
+ : domain === 'export-shape'
1662
+ ? `form: ${c.pattern}`
1663
+ : `case: ${c.pattern}`; // naming
1664
+ const dir = c.directory ? ` [${c.directory}]` : '';
1665
+ console.log(` ${desc} ${pct}% (${c.support}/${c.total_cases})${dir}${exemplar}`);
1666
+ }
1667
+ if (list.length > 5) {
1668
+ console.log(chalk.gray(` ... and ${list.length - 5} more`));
1669
+ }
1670
+ console.log('');
1671
+ }
1672
+ }
1673
+ }
1674
+ catch (error) {
1675
+ console.error(chalk.red('Error:'), error);
1676
+ process.exit(1);
1677
+ }
1678
+ });
1679
+ conventionsCmd
1680
+ .command('propose')
1681
+ .description('Emit ready-to-paste .codeauditor.json rules from mined conventions')
1682
+ .option('--domain <domain>', 'Filter by domain: naming or import-form (other domains do not map to rule kinds)')
1683
+ .option('--json', 'Output as JSON')
1684
+ .action(async (options) => {
1685
+ try {
1686
+ const db = CodeIndexDB.getInstance();
1687
+ await db.initialize();
1688
+ const rawDb = db.rawDb;
1689
+ let sql = 'SELECT * FROM conventions';
1690
+ let params = [];
1691
+ if (options.domain) {
1692
+ sql += ' WHERE domain = ?';
1693
+ params = [options.domain];
1694
+ }
1695
+ sql += ' ORDER BY domain, directory, confidence DESC';
1696
+ const conventions = rawDb.prepare(sql).all(...params);
1697
+ const proposals = [];
1698
+ for (const c of conventions) {
1699
+ switch (c.domain) {
1700
+ case 'naming': {
1701
+ if (!c.directory || !c.pattern)
1702
+ continue;
1703
+ proposals.push({
1704
+ kind: 'naming',
1705
+ message: `Use ${c.pattern} naming in ${c.directory}/ (${Math.round(c.confidence * 100)}% convention)`,
1706
+ description: `Exported symbols in ${c.directory}/ should use ${c.pattern}`,
1707
+ paths: [`${c.directory}/*`],
1708
+ pattern: getNamingPattern(c.pattern),
1709
+ exports: '**',
1710
+ });
1711
+ break;
1712
+ }
1713
+ case 'import-form': {
1714
+ if (!c.antecedent || !c.consequent)
1715
+ continue;
1716
+ proposals.push({
1717
+ kind: 'import-ban',
1718
+ message: `Import '${c.antecedent}' with module form in ${c.directory || '.'}/ (${Math.round(c.confidence * 100)}% convention)`,
1719
+ description: `In ${c.directory || '.'}/, imports of '${c.antecedent}' should use ${c.consequent} form`,
1720
+ module: c.antecedent,
1721
+ except: [{ source: c.directory || '.', form: c.consequent }],
1722
+ });
1723
+ break;
1724
+ }
1725
+ default:
1726
+ // usage-pair, error-handling, export-shape: no existing rule kind
1727
+ break;
1728
+ }
1729
+ }
1730
+ if (options.json) {
1731
+ process.stdout.write(JSON.stringify(proposals, null, 2) + '\n');
1732
+ }
1733
+ else if (proposals.length === 0) {
1734
+ console.log(chalk.gray('No proposals available. Only naming and import-form conventions map to rule kinds.'));
1735
+ console.log(chalk.gray('Usage-pair, error-handling, and export-shape are detector-only.'));
1736
+ }
1737
+ else {
1738
+ console.log(chalk.blue(`Proposed rules (${proposals.length}):\n`));
1739
+ for (const p of proposals) {
1740
+ console.log(chalk.green(` // ${p.message}`));
1741
+ console.log(JSON.stringify(p, null, 4).split('\n').map(l => ` ${l}`).join('\n'));
1742
+ console.log('');
1743
+ }
1744
+ console.log(chalk.gray('# Copy the rule objects above into the "rules" array in .codeauditor.json'));
1745
+ }
1746
+ }
1747
+ catch (error) {
1748
+ console.error(chalk.red('Error:'), error);
1749
+ process.exit(1);
1750
+ }
1751
+ });
1752
+ /**
1753
+ * Map a case name to a regex pattern for the naming rule kind.
1754
+ */
1755
+ function getNamingPattern(caseName) {
1756
+ switch (caseName) {
1757
+ case 'PascalCase': return '^[A-Z][a-zA-Z0-9]*$';
1758
+ case 'camelCase': return '^[a-z][a-zA-Z0-9]*$';
1759
+ case 'UPPER_SNAKE': return '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$';
1760
+ case 'snake_case': return '^[a-z][a-z0-9]*(_[a-z0-9]+)*$';
1761
+ case 'kebab-case': return '^[a-z][a-z0-9]*(-[a-z0-9]+)*$';
1762
+ default: return '^.*$';
1763
+ }
1764
+ }
791
1765
  // Install command — copy skill folder to agent-specific paths
792
1766
  program
793
1767
  .command('install')
@@ -812,6 +1786,155 @@ program
812
1786
  process.exit(1);
813
1787
  }
814
1788
  });
1789
+ // ── Architecture command — Spec 14 R3/R4 ────────────────────────────────
1790
+ program
1791
+ .command('architecture')
1792
+ .description('Analyze import graph: community detection, directory purity, Martin instability metrics')
1793
+ .option('--path <dir>', 'Project path', process.cwd())
1794
+ .option('--json', 'Output as JSON')
1795
+ .option('--format <format>', 'Output format: dot or mermaid (import graph colored by community)')
1796
+ .action(async (options) => {
1797
+ try {
1798
+ const db = CodeIndexDB.getInstance();
1799
+ await db.initialize();
1800
+ const rawDb = db.rawDb;
1801
+ const { buildImportGraphFromCache, detectCommunities, computeDirectoryPurity, computeMartinMetrics, } = await import('./graph/importGraph.js');
1802
+ const importGraph = buildImportGraphFromCache(rawDb);
1803
+ if (importGraph.filePaths.size === 0) {
1804
+ if (options.json) {
1805
+ process.stdout.write(JSON.stringify({ error: 'No import graph data. Run a full sync first.' }) + '\n');
1806
+ }
1807
+ else {
1808
+ console.log(chalk.gray('No import graph data. Run a full sync (code-audit index sync) first.'));
1809
+ }
1810
+ return;
1811
+ }
1812
+ const communities = detectCommunities(importGraph.adjacency, importGraph.filePaths);
1813
+ const purity = computeDirectoryPurity(communities.communities, importGraph.filePaths);
1814
+ const martin = computeMartinMetrics(rawDb, importGraph);
1815
+ if (options.format === 'dot' || options.format === 'mermaid') {
1816
+ // Emit import graph colored by community
1817
+ const { importGraphToDot, importGraphToMermaid } = await import('./graph/outputFormatter.js');
1818
+ if (options.format === 'dot') {
1819
+ const dot = importGraphToDot(importGraph, {
1820
+ label: 'Import Graph — Colored by Community',
1821
+ communities: communities.communities,
1822
+ rankdir: 'LR',
1823
+ showWeights: true,
1824
+ legend: true,
1825
+ });
1826
+ process.stdout.write(dot);
1827
+ }
1828
+ else {
1829
+ const mermaid = importGraphToMermaid(importGraph, {
1830
+ label: 'Import Graph — Colored by Community',
1831
+ communities: communities.communities,
1832
+ showWeights: true,
1833
+ });
1834
+ process.stdout.write(mermaid);
1835
+ }
1836
+ return;
1837
+ }
1838
+ if (options.json) {
1839
+ process.stdout.write(JSON.stringify({
1840
+ communityCount: communities.communityCount,
1841
+ modularity: Math.round(communities.modularity * 10000) / 10000,
1842
+ directoryPurity: purity.directoryPurities.map(d => ({
1843
+ directory: d.directory,
1844
+ totalFiles: d.totalFiles,
1845
+ pluralityCommunity: d.pluralityCommunity,
1846
+ pluralityCount: d.pluralityCount,
1847
+ purity: Math.round(d.purity * 10000) / 10000,
1848
+ })),
1849
+ splitCandidates: purity.splitCandidates.map(s => ({
1850
+ directory: s.directory,
1851
+ communities: s.communities,
1852
+ fileCounts: s.fileCounts,
1853
+ })),
1854
+ mergeCandidates: purity.mergeCandidates.map(m => ({
1855
+ directories: m.directories,
1856
+ community: m.community,
1857
+ fileCount: m.fileCount,
1858
+ })),
1859
+ agreementScore: Math.round(purity.agreementScore * 10000) / 10000,
1860
+ martinMetrics: martin.map(m => ({
1861
+ directory: m.directory,
1862
+ ce: m.ce,
1863
+ ca: m.ca,
1864
+ instability: Math.round(m.instability * 10000) / 10000,
1865
+ abstractness: Math.round(m.abstractness * 10000) / 10000,
1866
+ distanceFromMain: Math.round(m.distanceFromMain * 10000) / 10000,
1867
+ })),
1868
+ }, null, 2) + '\n');
1869
+ }
1870
+ else {
1871
+ // Community summary
1872
+ console.log(chalk.blue('šŸ—ļø Architecture Analysis\n'));
1873
+ console.log(chalk.bold('Community Detection (Louvain):'));
1874
+ console.log(chalk.gray(` ${communities.communityCount} communities detected (modularity: ${communities.modularity.toFixed(4)})`));
1875
+ // Directory Purity
1876
+ if (purity.directoryPurities.length > 0) {
1877
+ console.log(chalk.bold('\nDirectory Purity (agreement score: ') + chalk.yellow(purity.agreementScore.toFixed(2)) + chalk.bold(')\n'));
1878
+ console.log(chalk.gray(' Directory Files Plurality Purity'));
1879
+ console.log(chalk.gray(' ───────── ───── ───────── ──────'));
1880
+ for (const d of purity.directoryPurities) {
1881
+ const dirName = d.directory.length > 40 ? '...' + d.directory.slice(-37) : d.directory.padEnd(40);
1882
+ const files = String(d.totalFiles).padStart(5);
1883
+ const comm = String(d.pluralityCommunity).padStart(9);
1884
+ const pur = (d.purity * 100).toFixed(0).padStart(4) + '%';
1885
+ const purColor = d.purity >= 0.8 ? chalk.green(pur) : d.purity >= 0.5 ? chalk.yellow(pur) : chalk.red(pur);
1886
+ console.log(` ${chalk.dim(dirName)} ${files} ${comm} ${purColor}`);
1887
+ }
1888
+ }
1889
+ // Split Candidates
1890
+ if (purity.splitCandidates.length > 0) {
1891
+ console.log(chalk.bold('\nšŸ”€ Split Candidates (directories spanning ≄2 communities):'));
1892
+ for (const s of purity.splitCandidates) {
1893
+ console.log(chalk.yellow(` ${s.directory}`));
1894
+ for (let i = 0; i < s.communities.length; i++) {
1895
+ console.log(chalk.gray(` Community ${s.communities[i]}: ${s.fileCounts[i]} files`));
1896
+ }
1897
+ }
1898
+ }
1899
+ // Merge Candidates
1900
+ if (purity.mergeCandidates.length > 0) {
1901
+ console.log(chalk.bold('\nšŸ”— Merge Candidates (one community dominating ≄2 directories):'));
1902
+ for (const m of purity.mergeCandidates) {
1903
+ const dirs = m.directories.join(', ');
1904
+ console.log(chalk.green(` Community ${m.community}: ${dirs} (${m.fileCount} files)`));
1905
+ }
1906
+ }
1907
+ // Martin Metrics
1908
+ console.log(chalk.bold('\nMain-Sequence Distance (Martin Metrics):\n'));
1909
+ if (martin.length === 0) {
1910
+ console.log(chalk.gray(' No directory-level metrics available.'));
1911
+ }
1912
+ else {
1913
+ console.log(chalk.gray(' Directory Ce Ca I A |D|'));
1914
+ console.log(chalk.gray(' ───────── ── ── ──── ──── ────'));
1915
+ // Sort by distance from main sequence descending
1916
+ const sorted = [...martin].sort((a, b) => b.distanceFromMain - a.distanceFromMain);
1917
+ for (const m of sorted.slice(0, 30)) {
1918
+ const dirName = m.directory.length > 40 ? '...' + m.directory.slice(-37) : m.directory.padEnd(40);
1919
+ const ce = String(m.ce).padStart(3);
1920
+ const ca = String(m.ca).padStart(3);
1921
+ const inst = m.instability.toFixed(2).padStart(5);
1922
+ const abst = m.abstractness.toFixed(2).padStart(5);
1923
+ const dist = m.distanceFromMain.toFixed(2).padStart(5);
1924
+ const distColor = m.distanceFromMain <= 0.3 ? chalk.green(dist) : m.distanceFromMain <= 0.5 ? chalk.yellow(dist) : chalk.red(dist);
1925
+ console.log(` ${chalk.dim(dirName)} ${ce} ${ca} ${inst} ${abst} ${distColor}`);
1926
+ }
1927
+ console.log(chalk.gray('\n Ce = efferent couplings, Ca = afferent couplings'));
1928
+ console.log(chalk.gray(' I = Ce/(Ca+Ce), A = abstractness, |D| = |A+Iāˆ’1|'));
1929
+ console.log(chalk.gray(' Ideal: |D| → 0 (close to main sequence)'));
1930
+ }
1931
+ }
1932
+ }
1933
+ catch (error) {
1934
+ console.error(chalk.red('Error:'), error);
1935
+ process.exit(1);
1936
+ }
1937
+ });
815
1938
  // Cursor hook adapter — postToolUse (Write matcher) → scoped audit → additional_context + exit 2
816
1939
  // Verified 2026-07-20: cursor.com/docs/hooks + cursor.com/docs/reference/third-party-hooks
817
1940
  program
@@ -839,6 +1962,87 @@ program
839
1962
  process.exit(1);
840
1963
  }
841
1964
  });
1965
+ // ── Coverage command — Spec 15 R4 ─────────────────────────────────────
1966
+ const coverageCmd = program
1967
+ .command('coverage')
1968
+ .description('Import and report test coverage data');
1969
+ coverageCmd
1970
+ .command('import')
1971
+ .description('Import a coverage file (lcov .info or Istanbul JSON)')
1972
+ .argument('<path>', 'Path to the coverage file')
1973
+ .option('--project-root <path>', 'Project root for resolving relative paths', process.cwd())
1974
+ .action(async (filePath, options) => {
1975
+ try {
1976
+ const { importCoverageFile } = await import('./coverage/coverageService.js');
1977
+ const result = await importCoverageFile(filePath, options.projectRoot);
1978
+ console.log(chalk.green(`Imported ${result.format} coverage:`));
1979
+ console.log(` Format: ${result.format}`);
1980
+ console.log(` Files: ${result.filesImported}`);
1981
+ console.log(` Entries: ${result.entriesImported}`);
1982
+ console.log(` Source: ${result.source}`);
1983
+ }
1984
+ catch (error) {
1985
+ console.error(chalk.red('Error:'), error);
1986
+ process.exit(1);
1987
+ }
1988
+ });
1989
+ coverageCmd
1990
+ .command('by-risk', { isDefault: true })
1991
+ .description('Show coverage rate by risk decile')
1992
+ .option('--json', 'Output as JSON')
1993
+ .action(async (options) => {
1994
+ try {
1995
+ const { generateCoverageReport } = await import('./coverage/coverageService.js');
1996
+ const report = await generateCoverageReport();
1997
+ if (options.json) {
1998
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
1999
+ return;
2000
+ }
2001
+ console.log(chalk.blue(`Coverage Report:\n`));
2002
+ console.log(` Total functions: ${report.totalFunctions}`);
2003
+ console.log(` Covered functions: ${report.coveredFunctions}`);
2004
+ console.log(` Coverage rate: ${(report.coverageRate * 100).toFixed(1)}%`);
2005
+ if (report.staleImport) {
2006
+ console.log(chalk.yellow(` ⚠ Coverage data is stale — sync index and re-import`));
2007
+ }
2008
+ console.log(chalk.blue(`\n By Risk Decile:`));
2009
+ if (report.byRiskDecile.length === 0) {
2010
+ console.log(chalk.gray(' No data available. Run a full audit with hotspot scoring first.'));
2011
+ }
2012
+ else {
2013
+ for (const d of report.byRiskDecile) {
2014
+ const bar = renderBar(d.rate, 20);
2015
+ console.log(` Decile ${String(d.decile).padStart(2)}: ${bar} ${(d.rate * 100).toFixed(0)}% ` +
2016
+ `(${d.covered}/${d.total})`);
2017
+ }
2018
+ }
2019
+ console.log(chalk.blue(`\n Untested Top-Risk Functions:`));
2020
+ if (report.untestedTopDecile.length === 0) {
2021
+ console.log(chalk.green(' All top-risk functions have coverage.'));
2022
+ }
2023
+ else {
2024
+ for (const fn of report.untestedTopDecile.slice(0, 10)) {
2025
+ console.log(` ${chalk.yellow('⚠')} ${fn.functionName} (risk: ${fn.riskScore.toFixed(3)}) — ${fn.filePath}`);
2026
+ }
2027
+ if (report.untestedTopDecile.length > 10) {
2028
+ console.log(chalk.gray(` ... and ${report.untestedTopDecile.length - 10} more`));
2029
+ }
2030
+ }
2031
+ }
2032
+ catch (error) {
2033
+ console.error(chalk.red('Error:'), error);
2034
+ process.exit(1);
2035
+ }
2036
+ });
2037
+ /**
2038
+ * Render a simple ASCII bar chart.
2039
+ */
2040
+ function renderBar(value, width) {
2041
+ const filled = Math.round(value * width);
2042
+ const empty = width - filled;
2043
+ const color = value > 0.8 ? chalk.green : value > 0.5 ? chalk.yellow : chalk.red;
2044
+ return color('ā–ˆ'.repeat(filled) + 'ā–‘'.repeat(empty));
2045
+ }
842
2046
  // Parse command line arguments
843
2047
  program.parse(process.argv);
844
2048
  // If no command was provided, show help