code-auditor-mcp 3.1.0 ā 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +665 -1
- package/README.md +85 -11
- package/dist/__test_tmp__/spec18_test.d.ts +2 -0
- package/dist/__test_tmp__/spec18_test.d.ts.map +1 -0
- package/dist/__test_tmp__/spec18_test.js +5 -0
- package/dist/__test_tmp__/spec18_test.js.map +1 -0
- package/dist/__tests__/fixtures/spec-20/scripts/tool.d.ts +6 -0
- package/dist/__tests__/fixtures/spec-20/scripts/tool.d.ts.map +1 -0
- package/dist/__tests__/fixtures/spec-20/scripts/tool.js +12 -0
- package/dist/__tests__/fixtures/spec-20/scripts/tool.js.map +1 -0
- package/dist/__tests__/fixtures/spec-20/src/demo-module.d.ts +7 -0
- package/dist/__tests__/fixtures/spec-20/src/demo-module.d.ts.map +1 -0
- package/dist/__tests__/fixtures/spec-20/src/demo-module.js +13 -0
- package/dist/__tests__/fixtures/spec-20/src/demo-module.js.map +1 -0
- package/dist/analyzers/analyzerUtils.js +2 -2
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +2 -4
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts +3 -0
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js +3 -5
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +0 -3
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +2 -5
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +83 -0
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +611 -0
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -0
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +8 -4
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.d.ts +41 -0
- package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -0
- package/dist/analyzers/orm/adapterRegistry.js +73 -0
- package/dist/analyzers/orm/adapterRegistry.js.map +1 -0
- package/dist/analyzers/orm/drizzleAdapter.d.ts +42 -0
- package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -0
- package/dist/analyzers/orm/drizzleAdapter.js +166 -0
- package/dist/analyzers/orm/drizzleAdapter.js.map +1 -0
- package/dist/analyzers/orm/index.d.ts +14 -0
- package/dist/analyzers/orm/index.d.ts.map +1 -0
- package/dist/analyzers/orm/index.js +24 -0
- package/dist/analyzers/orm/index.js.map +1 -0
- package/dist/analyzers/orm/prismaAdapter.d.ts +45 -0
- package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -0
- package/dist/analyzers/orm/prismaAdapter.js +170 -0
- package/dist/analyzers/orm/prismaAdapter.js.map +1 -0
- package/dist/analyzers/orm/types.d.ts +63 -0
- package/dist/analyzers/orm/types.d.ts.map +1 -0
- package/dist/analyzers/orm/types.js +10 -0
- package/dist/analyzers/orm/types.js.map +1 -0
- package/dist/analyzers/provenance.d.ts +133 -0
- package/dist/analyzers/provenance.d.ts.map +1 -0
- package/dist/analyzers/provenance.js +1041 -0
- package/dist/analyzers/provenance.js.map +1 -0
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +117 -1
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +38 -0
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -0
- package/dist/analyzers/ruleRegistry.js +123 -0
- package/dist/analyzers/ruleRegistry.js.map +1 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +58 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +445 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -0
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +52 -0
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +108 -26
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +64 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +259 -103
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +5 -15
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +10 -10
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +25 -0
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +250 -59
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +103 -0
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +750 -0
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +483 -3
- package/dist/auditRunner.js.map +1 -1
- package/dist/baseline.d.ts +98 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +184 -0
- package/dist/baseline.js.map +1 -0
- package/dist/churn/churnExtractor.d.ts +32 -0
- package/dist/churn/churnExtractor.d.ts.map +1 -0
- package/dist/churn/churnExtractor.js +261 -0
- package/dist/churn/churnExtractor.js.map +1 -0
- package/dist/cli.js +1215 -11
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB-enhanced.js +1 -1
- package/dist/codeIndexDB-enhanced.js.map +1 -1
- package/dist/codeIndexDB.d.ts +101 -0
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +661 -5
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +95 -1
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +72 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +146 -15
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/pathProfiles.d.ts +38 -0
- package/dist/config/pathProfiles.d.ts.map +1 -0
- package/dist/config/pathProfiles.js +47 -0
- package/dist/config/pathProfiles.js.map +1 -0
- package/dist/conventions/conventionMiner.d.ts +63 -0
- package/dist/conventions/conventionMiner.d.ts.map +1 -0
- package/dist/conventions/conventionMiner.js +696 -0
- package/dist/conventions/conventionMiner.js.map +1 -0
- package/dist/coverage/coverageService.d.ts +49 -0
- package/dist/coverage/coverageService.d.ts.map +1 -0
- package/dist/coverage/coverageService.js +117 -0
- package/dist/coverage/coverageService.js.map +1 -0
- package/dist/coverage/istanbulParser.d.ts +84 -0
- package/dist/coverage/istanbulParser.d.ts.map +1 -0
- package/dist/coverage/istanbulParser.js +120 -0
- package/dist/coverage/istanbulParser.js.map +1 -0
- package/dist/coverage/lcovParser.d.ts +40 -0
- package/dist/coverage/lcovParser.d.ts.map +1 -0
- package/dist/coverage/lcovParser.js +168 -0
- package/dist/coverage/lcovParser.js.map +1 -0
- package/dist/fingerprint.d.ts +17 -0
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +42 -0
- package/dist/fingerprint.js.map +1 -1
- package/dist/grammars/tree-sitter-css.wasm +0 -0
- package/dist/graph/blastRadius.d.ts +27 -0
- package/dist/graph/blastRadius.d.ts.map +1 -0
- package/dist/graph/blastRadius.js +150 -0
- package/dist/graph/blastRadius.js.map +1 -0
- package/dist/graph/callGraph.d.ts +78 -0
- package/dist/graph/callGraph.d.ts.map +1 -0
- package/dist/graph/callGraph.js +484 -0
- package/dist/graph/callGraph.js.map +1 -0
- package/dist/graph/importGraph.d.ts +91 -0
- package/dist/graph/importGraph.d.ts.map +1 -0
- package/dist/graph/importGraph.js +602 -0
- package/dist/graph/importGraph.js.map +1 -0
- package/dist/graph/outputFormatter.d.ts +55 -0
- package/dist/graph/outputFormatter.d.ts.map +1 -0
- package/dist/graph/outputFormatter.js +283 -0
- package/dist/graph/outputFormatter.js.map +1 -0
- package/dist/hooks/codex.d.ts +36 -1
- package/dist/hooks/codex.d.ts.map +1 -1
- package/dist/hooks/codex.js +88 -54
- package/dist/hooks/codex.js.map +1 -1
- package/dist/hotspots/hotspotScorer.d.ts +20 -0
- package/dist/hotspots/hotspotScorer.d.ts.map +1 -0
- package/dist/hotspots/hotspotScorer.js +202 -0
- package/dist/hotspots/hotspotScorer.js.map +1 -0
- package/dist/invariants/invariant-rules.schema.json +42 -1
- package/dist/invariants/ruleEngine.d.ts +4 -1
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +141 -5
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/invariants/ruleValidator.d.ts.map +1 -1
- package/dist/invariants/ruleValidator.js +62 -1
- package/dist/invariants/ruleValidator.js.map +1 -1
- package/dist/invariants/types.d.ts +23 -2
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +59 -24
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/index.d.ts +1 -0
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +3 -0
- package/dist/languages/index.js.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +59 -0
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -0
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +236 -0
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -0
- package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
- package/dist/languages/tree-sitter/parser.js +2 -0
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +61 -10
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/ledger.d.ts +128 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +345 -0
- package/dist/ledger.js.map +1 -0
- package/dist/mcp-tools/projectTasks.d.ts.map +1 -1
- package/dist/mcp-tools/projectTasks.js +9 -14
- package/dist/mcp-tools/projectTasks.js.map +1 -1
- package/dist/mcp.js +0 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +3 -0
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +7 -1
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
- package/dist/reporting/htmlReportGenerator.js +5 -1
- package/dist/reporting/htmlReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +10 -2
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +17 -17
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/scripts/runBench.d.ts +94 -0
- package/dist/scripts/runBench.d.ts.map +1 -0
- package/dist/scripts/runBench.js +1559 -0
- package/dist/scripts/runBench.js.map +1 -0
- package/dist/search/QueryParser.d.ts.map +1 -1
- package/dist/search/QueryParser.js +51 -4
- package/dist/search/QueryParser.js.map +1 -1
- package/dist/services/CodeMapGenerator.d.ts +33 -0
- package/dist/services/CodeMapGenerator.d.ts.map +1 -1
- package/dist/services/CodeMapGenerator.js +404 -0
- package/dist/services/CodeMapGenerator.js.map +1 -1
- package/dist/styles/normalizer.d.ts +37 -0
- package/dist/styles/normalizer.d.ts.map +1 -0
- package/dist/styles/normalizer.js +301 -0
- package/dist/styles/normalizer.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +38 -0
- package/dist/styles/styleExtractor.d.ts.map +1 -0
- package/dist/styles/styleExtractor.js +630 -0
- package/dist/styles/styleExtractor.js.map +1 -0
- package/dist/styles/styleIndexer.d.ts +35 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -0
- package/dist/styles/styleIndexer.js +266 -0
- package/dist/styles/styleIndexer.js.map +1 -0
- package/dist/styles/tailwindConfigLoader.d.ts +38 -0
- package/dist/styles/tailwindConfigLoader.d.ts.map +1 -0
- package/dist/styles/tailwindConfigLoader.js +374 -0
- package/dist/styles/tailwindConfigLoader.js.map +1 -0
- package/dist/styles/tailwindExpander.d.ts +38 -0
- package/dist/styles/tailwindExpander.d.ts.map +1 -0
- package/dist/styles/tailwindExpander.js +637 -0
- package/dist/styles/tailwindExpander.js.map +1 -0
- package/dist/styles/types.d.ts +92 -0
- package/dist/styles/types.d.ts.map +1 -0
- package/dist/styles/types.js +10 -0
- package/dist/styles/types.js.map +1 -0
- package/dist/symbols.d.ts +21 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +31 -0
- package/dist/symbols.js.map +1 -0
- package/dist/types.d.ts +326 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -5
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/scripts/hook-audit.sh +5 -1
- package/plugin/skills/code-auditor/SKILL.md +51 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts +0 -21
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js +0 -21
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts +0 -13
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js +0 -12
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts +0 -11
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js +0 -22
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts +0 -9
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js +0 -13
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts +0 -13
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js +0 -18
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts +0 -10
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js +0 -16
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts +0 -14
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js +0 -18
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts +0 -13
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js +0 -22
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts +0 -10
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js +0 -20
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts +0 -18
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js +0 -28
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts +0 -11
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js +0 -31
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts +0 -11
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js +0 -18
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts +0 -13
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js +0 -61
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts +0 -10
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js +0 -15
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts +0 -11
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js +0 -49
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts +0 -10
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js +0 -19
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts +0 -37
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js +0 -73
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts +0 -12
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js +0 -82
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts +0 -10
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts.map +0 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js +0 -15
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js.map +0 -1
- package/dist/analyzers/dataAccessAnalyzer.d.ts +0 -46
- package/dist/analyzers/dataAccessAnalyzer.d.ts.map +0 -1
- package/dist/analyzers/dataAccessAnalyzer.js +0 -474
- package/dist/analyzers/dataAccessAnalyzer.js.map +0 -1
- package/dist/analyzers/dryAnalyzer.d.ts +0 -31
- package/dist/analyzers/dryAnalyzer.d.ts.map +0 -1
- package/dist/analyzers/dryAnalyzer.js +0 -734
- package/dist/analyzers/dryAnalyzer.js.map +0 -1
- package/dist/analyzers/schemaAnalyzer.d.ts +0 -23
- package/dist/analyzers/schemaAnalyzer.d.ts.map +0 -1
- package/dist/analyzers/schemaAnalyzer.js +0 -564
- package/dist/analyzers/schemaAnalyzer.js.map +0 -1
- package/dist/analyzers/solidAnalyzer.d.ts +0 -33
- package/dist/analyzers/solidAnalyzer.d.ts.map +0 -1
- package/dist/analyzers/solidAnalyzer.js +0 -847
- 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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|