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.
- 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 -2
- 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 -3
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.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 -2
- 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 -53
- 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 +61 -4
- 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
|
@@ -0,0 +1,1559 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bench harness — runs analyzers over seeded corpus fixtures and computes
|
|
4
|
+
* precision, recall, and F1 against labeled ground truth.
|
|
5
|
+
*
|
|
6
|
+
* Spec 11 R2
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm bench # Run all corpus analyzers
|
|
10
|
+
* pnpm bench --sweep # Sweep mode: vary thresholds and report curves
|
|
11
|
+
* pnpm bench --json # Output machine-readable JSON only
|
|
12
|
+
*/
|
|
13
|
+
import { initializeLanguages, initParsers } from '../languages/index.js';
|
|
14
|
+
import { UniversalSOLIDAnalyzer } from '../analyzers/universal/UniversalSOLIDAnalyzer.js';
|
|
15
|
+
import { UniversalDRYAnalyzer } from '../analyzers/universal/UniversalDRYAnalyzer.js';
|
|
16
|
+
import { UniversalDataAccessAnalyzer } from '../analyzers/universal/UniversalDataAccessAnalyzer.js';
|
|
17
|
+
import { UniversalDocumentationAnalyzer } from '../analyzers/universal/UniversalDocumentationAnalyzer.js';
|
|
18
|
+
import { UniversalSchemaAnalyzer } from '../analyzers/universal/UniversalSchemaAnalyzer.js';
|
|
19
|
+
import { reactAnalyzer } from '../analyzers/reactAnalyzer.js';
|
|
20
|
+
import { invariantsAnalyzer } from '../analyzers/invariantsAnalyzer.js';
|
|
21
|
+
import { UniversalStylesAnalyzer } from '../analyzers/universal/UniversalStylesAnalyzer.js';
|
|
22
|
+
import { UniversalConventionsAnalyzer } from '../analyzers/universal/UniversalConventionsAnalyzer.js';
|
|
23
|
+
import { CrossDomainAnalyzer } from '../analyzers/crossDomain/CrossDomainAnalyzer.js';
|
|
24
|
+
import { CodeIndexDB } from '../codeIndexDB.js';
|
|
25
|
+
import { extractSymbol } from '../symbols.js';
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
27
|
+
import { readdir } from 'fs/promises';
|
|
28
|
+
import { join, resolve, relative, dirname } from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
// ── Constants ────────────────────────────────────────────────────────────
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
const PROJECT_ROOT = resolve(__dirname, '..', '..');
|
|
35
|
+
const CORPUS_ROOT = join(PROJECT_ROOT, 'bench', 'corpus');
|
|
36
|
+
const RESULTS_DIR = join(PROJECT_ROOT, 'bench', 'results');
|
|
37
|
+
const BASELINE_PATH = join(PROJECT_ROOT, 'bench', 'baselines', 'baseline.json');
|
|
38
|
+
// ── Styles bench seed data ──────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Seed the in-memory SQLite DB with style declarations that trigger all
|
|
41
|
+
* 7 detectors (10 rule IDs) in UniversalStylesAnalyzer.
|
|
42
|
+
*
|
|
43
|
+
* Expected violations:
|
|
44
|
+
* 1. styles/value-drift (color) — 20× #1e2328 tokenised + 1× #273828 straggler
|
|
45
|
+
* 2. styles/value-drift (exact) — 20× 16px + 1× 17px straggler
|
|
46
|
+
* 3. styles/token-bypass — #1e2328 without token_ref matching --color-primary
|
|
47
|
+
* 4. styles/undefined-class — obscure-custom-class-xyz in class usage, no definition
|
|
48
|
+
* 5. styles/mechanism-fragmentation — margin:4px via css+tailwind+inline
|
|
49
|
+
* 6. styles/mechanism-mixing — src/fixture.tsx uses ≥3 mechanisms
|
|
50
|
+
* 7. styles/declaration-set-similarity — two identical 5-decl rule blocks
|
|
51
|
+
* 8. styles/z-index-sprawl — 7 distinct z-index values (> zIndexMaxDistinct=6)
|
|
52
|
+
* 9. styles/z-index-singleton — z-index:70 used only once
|
|
53
|
+
*
|
|
54
|
+
* Known miss: styles/off-scale (algorithmic limitation — see expected.json).
|
|
55
|
+
*/
|
|
56
|
+
function seedStylesData(rawDb, files) {
|
|
57
|
+
const file = files[0] ?? 'src/fixture.tsx';
|
|
58
|
+
let id = 0;
|
|
59
|
+
const nextId = () => ++id;
|
|
60
|
+
const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
61
|
+
const insertDecl = (property, rawValue, mechanism, line, tokenRef, context, normalizedValue) => {
|
|
62
|
+
rawDb.prepare(`INSERT INTO style_declarations (id, property, raw_value, normalized_value, mechanism, file_path, line, context, token_ref, content_hash)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(nextId(), property, rawValue, normalizedValue ?? rawValue, mechanism, file, line, context ?? null, tokenRef ?? null, hash(`${file}:${line}:${property}:${rawValue}`));
|
|
64
|
+
};
|
|
65
|
+
// ── Color drift: 20× #1e2328 tokenised + 1× #273828 straggler ──────
|
|
66
|
+
for (let i = 0; i < 20; i++) {
|
|
67
|
+
insertDecl('background', '#1e2328', 'css', 100 + i, '--color-primary');
|
|
68
|
+
}
|
|
69
|
+
insertDecl('background', '#273828', 'css', 120); // straggler, no token_ref
|
|
70
|
+
// ── Exact-value drift: 20× 16px + 1× 17px straggler ───────────────
|
|
71
|
+
for (let i = 0; i < 20; i++) {
|
|
72
|
+
insertDecl('font-size', '16px', 'css', 200 + i);
|
|
73
|
+
}
|
|
74
|
+
insertDecl('font-size', '17px', 'css', 220); // straggler
|
|
75
|
+
// ── Token bypass: #1e2328 (matches --color-primary) without token_ref
|
|
76
|
+
insertDecl('background-color', '#1e2328', 'css', 300);
|
|
77
|
+
// ── Mechanism fragmentation: margin:4px via 3 mechanisms ───────────
|
|
78
|
+
insertDecl('margin', '4px', 'css', 310);
|
|
79
|
+
insertDecl('margin', '4px', 'tailwind', 311);
|
|
80
|
+
insertDecl('margin', '4px', 'inline', 312);
|
|
81
|
+
// ── Declaration-set similarity: two identical 5-decl rule blocks ───
|
|
82
|
+
const blockA = '.block-a';
|
|
83
|
+
const blockB = '.block-b';
|
|
84
|
+
const sharedPairs = [
|
|
85
|
+
['display', 'flex'],
|
|
86
|
+
['flex-direction', 'column'],
|
|
87
|
+
['gap', '8px'],
|
|
88
|
+
['padding', '16px'],
|
|
89
|
+
['align-items', 'center'],
|
|
90
|
+
];
|
|
91
|
+
for (const [prop, val] of sharedPairs) {
|
|
92
|
+
insertDecl(prop, val, 'css', 400, null, blockA);
|
|
93
|
+
insertDecl(prop, val, 'css', 410, null, blockB);
|
|
94
|
+
}
|
|
95
|
+
// ── Z-index: 7 distinct values (sprawl), one singleton ─────────────
|
|
96
|
+
// Pair each value twice so total corpus > minCorpus
|
|
97
|
+
const zValues = [10, 20, 30, 40, 50, 60, 70];
|
|
98
|
+
for (const z of zValues) {
|
|
99
|
+
const repeat = z === 70 ? 1 : 2; // 70 is singleton
|
|
100
|
+
for (let i = 0; i < repeat; i++) {
|
|
101
|
+
insertDecl('z-index', String(z), 'css', 500 + z + i);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Token: --color-primary = #1e2328 ───────────────────────────────
|
|
105
|
+
rawDb.prepare(`INSERT INTO style_tokens (name, value, file_path, mechanism)
|
|
106
|
+
VALUES (?, ?, ?, ?)`).run('--color-primary', '#1e2328', file, 'css');
|
|
107
|
+
// ── Class usage: obscure-custom-class-xyz (undefined in any sheet) ─
|
|
108
|
+
rawDb.prepare(`INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?)`).run('obscure-custom-class-xyz', file, 1, 'className', 0);
|
|
110
|
+
}
|
|
111
|
+
function seedConventionsData(rawDb, _files) {
|
|
112
|
+
const insertFn = rawDb.prepare(`INSERT INTO functions (id, name, file_path, line_number, is_exported, metadata_json)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
114
|
+
insertFn.run(1, 'errorHandler', 'src/fixture.ts', 5, 0, null);
|
|
115
|
+
insertFn.run(2, 'handlePromiseError', 'src/fixture.ts', 15, 0, JSON.stringify({ body: "fetch('/api/data').then(res => res.json()).catch(err => console.error(err))" }));
|
|
116
|
+
insertFn.run(3, 'DefaultExporter', 'src/fixture.ts', 25, 1, null);
|
|
117
|
+
insertFn.run(4, 'my_snake_function', 'src/fixture.ts', 30, 1, null);
|
|
118
|
+
insertFn.run(5, 'approvedHandler', 'src/approved.ts', 5, 0, null);
|
|
119
|
+
insertFn.run(6, 'approvedWithCatch', 'src/approved.ts', 15, 0, JSON.stringify({ body: 'try { doSomething(); } catch (e) { logError(e); }' }));
|
|
120
|
+
insertFn.run(7, 'ApprovedExport', 'src/approved.ts', 25, 1, null);
|
|
121
|
+
insertFn.run(8, 'NoModeDefault', 'src/no-mode/index.ts', 5, 1, null);
|
|
122
|
+
insertFn.run(9, 'noModeCamelFunction', 'src/no-mode/index.ts', 10, 1, null);
|
|
123
|
+
insertFn.run(10, 'noModeTryCatch', 'src/no-mode/index.ts', 15, 0, JSON.stringify({ body: 'try { x(); } catch(e) {}' }));
|
|
124
|
+
insertFn.run(11, 'noModeCatch', 'src/no-mode/index.ts', 20, 0, JSON.stringify({ body: 'p.then(r => r).catch(err => {})' }));
|
|
125
|
+
insertFn.run(12, 'noModeIfErr', 'src/no-mode/index.ts', 25, 0, JSON.stringify({ body: 'if (err) return;' }));
|
|
126
|
+
const insertCall = rawDb.prepare(`INSERT INTO function_calls (caller_id, callee_name) VALUES (?, ?)`);
|
|
127
|
+
insertCall.run(1, 'handleError');
|
|
128
|
+
insertCall.run(5, 'handleError');
|
|
129
|
+
insertCall.run(5, 'logError');
|
|
130
|
+
const insertConv = rawDb.prepare(`INSERT INTO conventions (id, domain, rule_id, antecedent, consequent, pattern, directory, support, total_cases, confidence, exemplar_file, exemplar_line)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
132
|
+
insertConv.run(1, 'usage-pair', 'conventions/usage-pair', 'handleError', 'logError', null, 'src', 95, 100, 0.95, 'src/approved.ts', 5);
|
|
133
|
+
insertConv.run(2, 'import-form', 'conventions/import-form', 'lodash', 'default', null, 'src', 20, 22, 0.93, 'src/approved.ts', 1);
|
|
134
|
+
insertConv.run(3, 'error-handling', 'conventions/error-handling', null, null, 'try-catch', 'src', 30, 34, 0.88, 'src/approved.ts', 15);
|
|
135
|
+
insertConv.run(4, 'export-shape', 'conventions/export-shape', null, null, 'named', 'src', 18, 20, 0.90, 'src/approved.ts', 25);
|
|
136
|
+
insertConv.run(5, 'naming', 'conventions/naming', null, null, 'PascalCase', 'src', 25, 27, 0.92, 'src/approved.ts', 25);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Seed dry_pair_history with pre-declining similarity rows to simulate
|
|
140
|
+
* a tracked clone pair that has diverged across consecutive audit runs.
|
|
141
|
+
*
|
|
142
|
+
* 3 rows: similarity 0.85 → 0.78 → 0.68
|
|
143
|
+
* With divergenceThreshold=0.05 and divergenceRuns=2:
|
|
144
|
+
* - Row 1→2: 0.78 < 0.85 - 0.05 = 0.80 ✓ (decline)
|
|
145
|
+
* - Row 2→3: 0.68 < 0.78 - 0.05 = 0.73 ✓ (decline)
|
|
146
|
+
* - consecutiveDeclines = 2 ≥ requiredDeclines = 2 → triggers
|
|
147
|
+
*/
|
|
148
|
+
function seedDivergingClonesData(rawDb, files) {
|
|
149
|
+
const file1 = files[0] ?? 'src/clone_a.ts';
|
|
150
|
+
const file2 = files[1] ?? 'src/clone_b.ts';
|
|
151
|
+
const shortHash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
152
|
+
// Stable pair_fingerprint: SHA256 of sorted member identity strings
|
|
153
|
+
const pairFp = createHash('sha256')
|
|
154
|
+
.update([`${file1}|function_declaration|1`, `${file2}|function_declaration|1`].sort().join('||'))
|
|
155
|
+
.digest('hex');
|
|
156
|
+
const insert = rawDb.prepare(`
|
|
157
|
+
INSERT INTO dry_pair_history
|
|
158
|
+
(pair_fingerprint, file1, symbol1, line1, content_hash1,
|
|
159
|
+
file2, symbol2, line2, content_hash2,
|
|
160
|
+
similarity, timestamp, run_id)
|
|
161
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
162
|
+
`);
|
|
163
|
+
// Run 1: initial similarity 0.85
|
|
164
|
+
insert.run(pairFp, file1, 'processOrder', 12, shortHash('order-block-v1'), file2, 'handleInvoice', 12, shortHash('invoice-block-v1'), 0.85, '2026-01-01T00:00:00.000Z', 'run-1');
|
|
165
|
+
// Run 2: slight divergence, similarity 0.78 (decline 0.07 > 0.05)
|
|
166
|
+
insert.run(pairFp, file1, 'processOrder', 12, shortHash('order-block-v2'), file2, 'handleInvoice', 12, shortHash('invoice-block-v2'), 0.78, '2026-01-02T00:00:00.000Z', 'run-2');
|
|
167
|
+
// Run 3: further divergence, similarity 0.68 (decline 0.10 > 0.05)
|
|
168
|
+
insert.run(pairFp, file1, 'processOrder', 12, shortHash('order-block-v3'), file2, 'handleInvoice', 12, shortHash('invoice-block-v3'), 0.68, '2026-01-03T00:00:00.000Z', 'run-3');
|
|
169
|
+
}
|
|
170
|
+
// ── Cross-domain bench seed data ───────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* Seed the in-memory SQLite DB with schema_usage, functions, function_calls,
|
|
173
|
+
* graph_cache, hotspot_scores, and coverage_data to trigger all 5 cross-domain
|
|
174
|
+
* detectors.
|
|
175
|
+
*
|
|
176
|
+
* Tables & expected violations:
|
|
177
|
+
* - written-never-read (5): orders, sessions, inventory, payments, notifications
|
|
178
|
+
* - read-never-written (1): audit_log
|
|
179
|
+
* - transaction-boundary-risk (1): processOrder writes orders + 3 callees (updateInventory→inventory,
|
|
180
|
+
* processPayment→payments, sendNotification→notifications) = 4 ≥ txnTableMax
|
|
181
|
+
* - validation-bypass (4): createUnvalidatedOrder + processPayment + sendNotification + formatOrder (writers reaching no validator; formatOrder is the poison case — reaches formatHelper which cohabits validators.ts but has no zod in its own usedImports)
|
|
182
|
+
* - uncovered-risk (10): high-risk functions without test coverage via static-reach fallback
|
|
183
|
+
*/
|
|
184
|
+
function seedCrossDomainData(rawDb, _files) {
|
|
185
|
+
const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
186
|
+
// ── Functions table ──────────────────────────────────────────────────
|
|
187
|
+
const insertFn = rawDb.prepare(`INSERT INTO functions (id, name, file_path, line_number, complexity, is_exported, used_imports, content_hash)
|
|
188
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
189
|
+
// src/orders.ts
|
|
190
|
+
insertFn.run(1, 'saveOrder', 'src/orders/orders.ts', 5, 2, 1, null, hash('orders:saveOrder'));
|
|
191
|
+
insertFn.run(2, 'processOrder', 'src/orders/orders.ts', 9, 3, 1, null, hash('orders:processOrder'));
|
|
192
|
+
insertFn.run(3, 'updateInventory', 'src/orders/orders.ts', 16, 1, 0, null, hash('orders:updateInventory'));
|
|
193
|
+
insertFn.run(4, 'processPayment', 'src/orders/orders.ts', 20, 1, 0, null, hash('orders:processPayment'));
|
|
194
|
+
insertFn.run(5, 'sendNotification', 'src/orders/orders.ts', 24, 1, 0, null, hash('orders:sendNotification'));
|
|
195
|
+
insertFn.run(6, 'createValidatedOrder', 'src/orders/orders.ts', 28, 2, 1, null, hash('orders:createValidatedOrder'));
|
|
196
|
+
insertFn.run(7, 'createUnvalidatedOrder', 'src/orders/orders.ts', 33, 1, 1, null, hash('orders:createUnvalidatedOrder'));
|
|
197
|
+
insertFn.run(18, 'formatOrder', 'src/orders/orders.ts', 38, 1, 1, JSON.stringify(['../validators/validators']), hash('orders:formatOrder')); // poison: calls formatHelper, not a real validator
|
|
198
|
+
// src/audit.ts
|
|
199
|
+
insertFn.run(8, 'readAuditLog', 'src/audit/audit.ts', 4, 1, 1, null, hash('audit:readAuditLog'));
|
|
200
|
+
// src/users.ts
|
|
201
|
+
insertFn.run(9, 'manageUsers', 'src/users/users.ts', 5, 2, 1, null, hash('users:manageUsers'));
|
|
202
|
+
// src/sessions.ts
|
|
203
|
+
insertFn.run(10, 'createSession', 'src/sessions/sessions.ts', 5, 1, 1, null, hash('sessions:createSession'));
|
|
204
|
+
// src/validators.ts
|
|
205
|
+
insertFn.run(11, 'validateOrder', 'src/validators/validators.ts', 12, 1, 1, JSON.stringify(['zod']), hash('validators:validateOrder'));
|
|
206
|
+
insertFn.run(12, 'validatePayment', 'src/validators/validators.ts', 22, 1, 1, JSON.stringify(['zod']), hash('validators:validatePayment'));
|
|
207
|
+
insertFn.run(17, 'formatHelper', 'src/validators/validators.ts', 35, 1, 1, null, hash('validators:formatHelper')); // poison: exported, no zod in usedImports
|
|
208
|
+
// src/risk.ts
|
|
209
|
+
insertFn.run(13, 'highRiskTested', 'src/risk/risk.ts', 9, 3, 1, null, hash('risk:highRiskTested'));
|
|
210
|
+
insertFn.run(14, 'highRiskUntested', 'src/risk/risk.ts', 19, 3, 1, null, hash('risk:highRiskUntested'));
|
|
211
|
+
// src/orders.test.ts
|
|
212
|
+
insertFn.run(15, 'test saveOrder', 'src/tests/orders.test.ts', 7, 1, 0, null, hash('test:saveOrder'));
|
|
213
|
+
insertFn.run(16, 'test validatedOrder', 'src/tests/orders.test.ts', 11, 1, 0, null, hash('test:validatedOrder'));
|
|
214
|
+
// ── Function calls ───────────────────────────────────────────────────
|
|
215
|
+
const insertCall = rawDb.prepare(`INSERT INTO function_calls (caller_id, callee_name) VALUES (?, ?)`);
|
|
216
|
+
// processOrder calls its 3 internal helpers
|
|
217
|
+
insertCall.run(2, 'updateInventory');
|
|
218
|
+
insertCall.run(2, 'processPayment');
|
|
219
|
+
insertCall.run(2, 'sendNotification');
|
|
220
|
+
// Writers that also call validateOrder (validation-bypass reach)
|
|
221
|
+
insertCall.run(1, 'validateOrder');
|
|
222
|
+
insertCall.run(2, 'validateOrder');
|
|
223
|
+
insertCall.run(3, 'validateOrder');
|
|
224
|
+
// createValidatedOrder calls validateOrder
|
|
225
|
+
insertCall.run(6, 'validateOrder');
|
|
226
|
+
// formatOrder calls formatHelper (poison: non-validator cohabitant)
|
|
227
|
+
insertCall.run(18, 'formatHelper');
|
|
228
|
+
// test functions call production functions
|
|
229
|
+
insertCall.run(15, 'saveOrder');
|
|
230
|
+
insertCall.run(16, 'createValidatedOrder');
|
|
231
|
+
insertCall.run(16, 'processOrder');
|
|
232
|
+
// ── Graph cache (call edges) ─────────────────────────────────────────
|
|
233
|
+
const insertCache = rawDb.prepare(`INSERT OR REPLACE INTO graph_cache (graph_type, node_key, neighbor_key, weight)
|
|
234
|
+
VALUES (?, ?, ?, ?)`);
|
|
235
|
+
// processOrder → callees (for transaction-boundary BFS)
|
|
236
|
+
// Use string values for node_key/neighbor_key — graph_cache columns are TEXT
|
|
237
|
+
insertCache.run('call', '2', '3', 1); // processOrder → updateInventory
|
|
238
|
+
insertCache.run('call', '2', '4', 1); // processOrder → processPayment
|
|
239
|
+
insertCache.run('call', '2', '5', 1); // processOrder → sendNotification
|
|
240
|
+
// Writers that reach validateOrder (for validation-bypass BFS — need ≥ 50% coverage)
|
|
241
|
+
insertCache.run('call', '1', '11', 1); // saveOrder → validateOrder
|
|
242
|
+
insertCache.run('call', '2', '11', 1); // processOrder → validateOrder
|
|
243
|
+
insertCache.run('call', '3', '11', 1); // updateInventory → validateOrder
|
|
244
|
+
insertCache.run('call', '6', '11', 1); // createValidatedOrder → validateOrder
|
|
245
|
+
// formatOrder → formatHelper (poison: non-validator cohabitant in validators.ts)
|
|
246
|
+
insertCache.run('call', '18', '17', 1); // formatOrder → formatHelper (not a real validator)
|
|
247
|
+
// test functions → production functions (for uncovered-risk static-reach fallback)
|
|
248
|
+
insertCache.run('call', '15', '1', 1); // test:saveOrder → saveOrder
|
|
249
|
+
insertCache.run('call', '16', '6', 1); // test:validatedOrder → createValidatedOrder
|
|
250
|
+
insertCache.run('call', '16', '2', 1); // test:validatedOrder → processOrder
|
|
251
|
+
// ── Schema usage ─────────────────────────────────────────────────────
|
|
252
|
+
const insertUsage = rawDb.prepare(`INSERT INTO schema_usage (table_name, file_path, function_name, usage_type, line, column, raw_query, parameters)
|
|
253
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
254
|
+
// Write-only tables (written, never read) → written-never-read (5)
|
|
255
|
+
insertUsage.run('orders', 'src/orders/orders.ts', 'saveOrder', 'insert', 6, 5, null, null);
|
|
256
|
+
insertUsage.run('orders', 'src/orders/orders.ts', 'processOrder', 'update', 12, 5, null, null);
|
|
257
|
+
insertUsage.run('orders', 'src/orders/orders.ts', 'createValidatedOrder', 'insert', 30, 3, null, null);
|
|
258
|
+
insertUsage.run('orders', 'src/orders/orders.ts', 'createUnvalidatedOrder', 'insert', 35, 3, null, null);
|
|
259
|
+
insertUsage.run('orders', 'src/orders/orders.ts', 'formatOrder', 'insert', 40, 3, null, null); // poison: writer reaches only non-validator
|
|
260
|
+
insertUsage.run('inventory', 'src/orders/orders.ts', 'updateInventory', 'update', 17, 5, null, null);
|
|
261
|
+
insertUsage.run('payments', 'src/orders/orders.ts', 'processPayment', 'insert', 21, 5, null, null);
|
|
262
|
+
insertUsage.run('notifications', 'src/orders/orders.ts', 'sendNotification', 'insert', 25, 5, null, null);
|
|
263
|
+
insertUsage.run('sessions', 'src/sessions/sessions.ts', 'createSession', 'insert', 6, 5, null, null);
|
|
264
|
+
// Read-only table (read, never written) → read-never-written (1)
|
|
265
|
+
insertUsage.run('audit_log', 'src/audit/audit.ts', 'readAuditLog', 'select', 5, 5, null, null);
|
|
266
|
+
// Both read+write (no violation)
|
|
267
|
+
insertUsage.run('users', 'src/users/users.ts', 'manageUsers', 'select', 7, 5, null, null);
|
|
268
|
+
insertUsage.run('users', 'src/users/users.ts', 'manageUsers', 'insert', 8, 5, null, null);
|
|
269
|
+
// ── Hotspot scores (for uncovered-risk detection) ─────────────────────
|
|
270
|
+
const insertHotspot = rawDb.prepare(`INSERT OR REPLACE INTO hotspot_scores (target, type, score, churn_pct, complexity_pct, commit_count)
|
|
271
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
272
|
+
// High-risk functions for top decile detection
|
|
273
|
+
insertHotspot.run('highRiskTested', 'function', 0.85, 75.0, 90.0, 15);
|
|
274
|
+
insertHotspot.run('highRiskUntested', 'function', 0.92, 80.0, 95.0, 20);
|
|
275
|
+
// Lower-risk functions to provide contrast
|
|
276
|
+
insertHotspot.run('saveOrder', 'function', 0.30, 20.0, 15.0, 3);
|
|
277
|
+
insertHotspot.run('processOrder', 'function', 0.45, 35.0, 40.0, 8);
|
|
278
|
+
insertHotspot.run('validateOrder', 'function', 0.20, 10.0, 10.0, 2);
|
|
279
|
+
// ── Coverage data ────────────────────────────────────────────────────
|
|
280
|
+
const insertCov = rawDb.prepare(`INSERT OR REPLACE INTO coverage_data (function_name, file_path, line_number, basis, covered, source)
|
|
281
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
282
|
+
// highRiskTested has measured coverage → NOT flagged
|
|
283
|
+
insertCov.run('highRiskTested', 'src/risk/risk.ts', 9, 'measured', 1, 'lcov');
|
|
284
|
+
// highRiskUntested has no coverage_data → flagged as uncovered-risk
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Seed the in-memory SQLite DB with a known call graph and import graph.
|
|
288
|
+
*
|
|
289
|
+
* Graph structure:
|
|
290
|
+
* - core.ts: calculateTotal (hub — high PageRank, called by many)
|
|
291
|
+
* - bridge.ts: validateOrder (bridge — high betweenness, connects module_a ↔ module_b)
|
|
292
|
+
* - untested.ts: processPayment, refundPayment (no test-file callers → untested penalty)
|
|
293
|
+
* - module_a: a1.ts (calculateTax, processModuleA), a2.ts (calculateShipping, estimateTotal)
|
|
294
|
+
* - module_b: b1.ts (processOrder, handleModuleB), b2.ts (generateReport, auditModuleB)
|
|
295
|
+
* - module_c: c_a.ts (crossModuleOperation, moduleCOperation), c_b.ts (processModuleC, handleModuleC)
|
|
296
|
+
* - interfaces/: payments.ts with 2 exported interfaces + 1 exported function (abstractness > 0)
|
|
297
|
+
*/
|
|
298
|
+
function seedGraphData(rawDb, _files) {
|
|
299
|
+
const insertFn = rawDb.prepare(`INSERT INTO functions (id, name, file_path, line_number, complexity, is_exported, content_hash, metadata_json)
|
|
300
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
301
|
+
const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
302
|
+
// ── Functions ───────────────────────────────────────────────────────
|
|
303
|
+
const fns = [
|
|
304
|
+
// core.ts — hub
|
|
305
|
+
[1, 'calculateTotal', 'src/core.ts', 1, 1, 1],
|
|
306
|
+
[2, 'finalizeOrder', 'src/core.ts', 8, 3, 1],
|
|
307
|
+
// bridge.ts — bridge
|
|
308
|
+
[3, 'validateOrder', 'src/bridge.ts', 2, 2, 1],
|
|
309
|
+
[4, 'processAndReport', 'src/bridge.ts', 9, 2, 1],
|
|
310
|
+
// untested.ts
|
|
311
|
+
[5, 'processPayment', 'src/untested.ts', 2, 2, 1],
|
|
312
|
+
[6, 'refundPayment', 'src/untested.ts', 8, 1, 1],
|
|
313
|
+
// module_a/a1.ts — Community A
|
|
314
|
+
[7, 'calculateTax', 'src/module_a/a1.ts', 2, 1, 1],
|
|
315
|
+
[8, 'processModuleA', 'src/module_a/a1.ts', 6, 2, 1],
|
|
316
|
+
// module_a/a2.ts — Community A
|
|
317
|
+
[9, 'calculateShipping', 'src/module_a/a2.ts', 2, 1, 1],
|
|
318
|
+
[10, 'estimateTotal', 'src/module_a/a2.ts', 6, 2, 1],
|
|
319
|
+
// module_b/b1.ts — Community B
|
|
320
|
+
[11, 'processOrder', 'src/module_b/b1.ts', 2, 1, 1],
|
|
321
|
+
[12, 'handleModuleB', 'src/module_b/b1.ts', 6, 2, 1],
|
|
322
|
+
// module_b/b2.ts — Community B
|
|
323
|
+
[13, 'generateReport', 'src/module_b/b2.ts', 2, 1, 1],
|
|
324
|
+
[14, 'auditModuleB', 'src/module_b/b2.ts', 6, 1, 1],
|
|
325
|
+
// module_c/c_a.ts — Community C (split)
|
|
326
|
+
[15, 'crossModuleOperation', 'src/module_c/c_a.ts', 2, 1, 1],
|
|
327
|
+
[16, 'moduleCOperation', 'src/module_c/c_a.ts', 6, 1, 1],
|
|
328
|
+
// module_c/c_b.ts — Community C (split)
|
|
329
|
+
[17, 'processModuleC', 'src/module_c/c_b.ts', 2, 1, 1],
|
|
330
|
+
[18, 'handleModuleC', 'src/module_c/c_b.ts', 6, 1, 1],
|
|
331
|
+
// interfaces/payments.ts
|
|
332
|
+
[19, 'createPaymentProcessor', 'src/interfaces/payments.ts', 11, 1, 1],
|
|
333
|
+
];
|
|
334
|
+
for (const [id, name, file, line, cx, exported] of fns) {
|
|
335
|
+
insertFn.run(id, name, file, line, cx, exported, hash(`${file}:${name}`), null);
|
|
336
|
+
}
|
|
337
|
+
// ── Function calls (caller_id → callee_name) ───────────────────────
|
|
338
|
+
const insertCall = rawDb.prepare(`INSERT INTO function_calls (caller_id, callee_name) VALUES (?, ?)`);
|
|
339
|
+
const calls = [
|
|
340
|
+
// finalizeOrder orchestrates hub + module_a + module_b
|
|
341
|
+
[2, 'calculateTotal'],
|
|
342
|
+
[2, 'calculateTax'],
|
|
343
|
+
[2, 'processOrder'],
|
|
344
|
+
// processAndReport calls bridge + module_b
|
|
345
|
+
[4, 'validateOrder'],
|
|
346
|
+
[4, 'generateReport'],
|
|
347
|
+
// processModuleA calls hub + module_a internal
|
|
348
|
+
[8, 'calculateTotal'],
|
|
349
|
+
[8, 'calculateTax'],
|
|
350
|
+
// estimateTotal calls module_a internal
|
|
351
|
+
[10, 'calculateTax'],
|
|
352
|
+
[10, 'calculateShipping'],
|
|
353
|
+
// handleModuleB calls hub + module_b internal
|
|
354
|
+
[12, 'calculateTotal'],
|
|
355
|
+
[12, 'processOrder'],
|
|
356
|
+
// generateReport calls module_b internal
|
|
357
|
+
[13, 'processOrder'],
|
|
358
|
+
// auditModuleB calls module_b internal
|
|
359
|
+
[14, 'processOrder'],
|
|
360
|
+
// crossModuleOperation calls module_b (cross-community)
|
|
361
|
+
[15, 'generateReport'],
|
|
362
|
+
// processModuleC calls module_c internal
|
|
363
|
+
[17, 'moduleCOperation'],
|
|
364
|
+
// handleModuleC calls module_c internal
|
|
365
|
+
[18, 'processModuleC'],
|
|
366
|
+
// validateOrder bridges module_a (= calls calculateShipping)
|
|
367
|
+
[3, 'calculateShipping'],
|
|
368
|
+
];
|
|
369
|
+
for (const [callerId, calleeName] of calls) {
|
|
370
|
+
insertCall.run(callerId, calleeName);
|
|
371
|
+
}
|
|
372
|
+
// ── Function dependencies (for import graph) ────────────────────────
|
|
373
|
+
// function_dependencies schema: (function_id, dependency)
|
|
374
|
+
// dependency = module specifier string (resolved to files in buildImportGraph)
|
|
375
|
+
const insertDep = rawDb.prepare(`INSERT OR IGNORE INTO function_dependencies (function_id, dependency)
|
|
376
|
+
VALUES (?, ?)`);
|
|
377
|
+
// Map: function_name → function_id (used by both deps and call graph cache)
|
|
378
|
+
const nameToId = new Map();
|
|
379
|
+
for (const [id, name] of fns) {
|
|
380
|
+
nameToId.set(name, id);
|
|
381
|
+
}
|
|
382
|
+
// Simulate import edges between files
|
|
383
|
+
// (source_function_id, dependency_module_specifier)
|
|
384
|
+
const deps = [
|
|
385
|
+
// core.ts functions import from module_a/a1 and module_b/b1
|
|
386
|
+
[nameToId.get('calculateTotal'), './module_a/a1'],
|
|
387
|
+
[nameToId.get('calculateTotal'), './module_b/b1'],
|
|
388
|
+
[nameToId.get('finalizeOrder'), './module_b/b1'],
|
|
389
|
+
// bridge.ts functions import from module_a/a2 and module_b/b2
|
|
390
|
+
[nameToId.get('validateOrder'), './module_a/a2'],
|
|
391
|
+
[nameToId.get('validateOrder'), './module_b/b2'],
|
|
392
|
+
[nameToId.get('customerReport'), './module_b/b2'],
|
|
393
|
+
// module_a/a1.ts imports from core.ts
|
|
394
|
+
[nameToId.get('calculateTax'), './core'],
|
|
395
|
+
[nameToId.get('applyDiscount'), './core'],
|
|
396
|
+
// module_a/a2.ts imports from module_a/a1.ts
|
|
397
|
+
[nameToId.get('calculateShipping'), './module_a/a1'],
|
|
398
|
+
[nameToId.get('updateInventory'), './module_a/a1'],
|
|
399
|
+
// module_b/b1.ts imports from core.ts
|
|
400
|
+
[nameToId.get('processOrder'), './core'],
|
|
401
|
+
[nameToId.get('sendNotification'), './core'],
|
|
402
|
+
// module_b/b2.ts imports from module_b/b1.ts
|
|
403
|
+
[nameToId.get('generateReport'), './module_b/b1'],
|
|
404
|
+
[nameToId.get('archiveData'), './module_b/b1'],
|
|
405
|
+
// module_c/c_a.ts imports from module_b/b2.ts
|
|
406
|
+
[nameToId.get('moduleCOperation'), './module_b/b2'],
|
|
407
|
+
[nameToId.get('crossModuleHelper'), './module_b/b2'],
|
|
408
|
+
// module_c/c_b.ts imports from module_c/c_a.ts
|
|
409
|
+
[nameToId.get('finalModuleStep'), './module_c/c_a'],
|
|
410
|
+
[nameToId.get('aggregateResults'), './module_c/c_a'],
|
|
411
|
+
// untested.ts imports from core.ts
|
|
412
|
+
[nameToId.get('processPayment'), './core'],
|
|
413
|
+
[nameToId.get('refundPayment'), './core'],
|
|
414
|
+
];
|
|
415
|
+
for (const [fnId, dep] of deps) {
|
|
416
|
+
insertDep.run(fnId, dep);
|
|
417
|
+
}
|
|
418
|
+
// ── Populate graph_cache ────────────────────────────────────────────
|
|
419
|
+
// Call graph cache: insert all resolved (caller_id → callee_id) edges
|
|
420
|
+
const insertCache = rawDb.prepare(`INSERT OR REPLACE INTO graph_cache (graph_type, node_key, neighbor_key, weight)
|
|
421
|
+
VALUES (?, ?, ?, ?)`);
|
|
422
|
+
const callWeight = new Map();
|
|
423
|
+
for (const [callerId, calleeName] of calls) {
|
|
424
|
+
const calleeId = nameToId.get(calleeName);
|
|
425
|
+
if (!calleeId)
|
|
426
|
+
continue;
|
|
427
|
+
const key = `call:${callerId}:${calleeId}`;
|
|
428
|
+
callWeight.set(key, (callWeight.get(key) ?? 0) + 1);
|
|
429
|
+
}
|
|
430
|
+
for (const [key, weight] of callWeight) {
|
|
431
|
+
const [, callerStr, calleeStr] = key.split(':');
|
|
432
|
+
insertCache.run('call', Number(callerStr), Number(calleeStr), weight);
|
|
433
|
+
}
|
|
434
|
+
// Import graph cache: insert file-level import edges
|
|
435
|
+
// Derive file paths from the seeded function IDs and dependency specifiers
|
|
436
|
+
const idToFilePath = new Map();
|
|
437
|
+
for (const [id, , filePath] of fns) {
|
|
438
|
+
idToFilePath.set(id, filePath);
|
|
439
|
+
}
|
|
440
|
+
// Resolve dependency specifiers to actual file paths in the fixture
|
|
441
|
+
const resolveDep = (dep) => {
|
|
442
|
+
// './module_a/a1' → 'src/module_a/a1.ts'
|
|
443
|
+
// './module_b/b1' → 'src/module_b/b1.ts'
|
|
444
|
+
// './core' → 'src/core.ts'
|
|
445
|
+
const clean = dep.replace(/^\.\//, '');
|
|
446
|
+
const candidates = [
|
|
447
|
+
`src/${clean}.ts`,
|
|
448
|
+
`src/${clean}/index.ts`,
|
|
449
|
+
];
|
|
450
|
+
for (const c of candidates) {
|
|
451
|
+
if (filePaths.has(c))
|
|
452
|
+
return c;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
};
|
|
456
|
+
const filePaths = new Set();
|
|
457
|
+
for (const [, , fp] of fns) {
|
|
458
|
+
filePaths.add(fp);
|
|
459
|
+
}
|
|
460
|
+
const importWeight = new Map();
|
|
461
|
+
for (const [fnId, dep] of deps) {
|
|
462
|
+
const srcFile = idToFilePath.get(fnId);
|
|
463
|
+
const dstFile = resolveDep(dep);
|
|
464
|
+
if (!srcFile || !dstFile)
|
|
465
|
+
continue;
|
|
466
|
+
const key = `import:${srcFile}:${dstFile}`;
|
|
467
|
+
importWeight.set(key, (importWeight.get(key) ?? 0) + 1);
|
|
468
|
+
}
|
|
469
|
+
for (const [key, weight] of importWeight) {
|
|
470
|
+
const parts = key.split(':');
|
|
471
|
+
// key format: "import:srcFile:dstFile" — srcFile may contain colons on macOS? No.
|
|
472
|
+
const srcFile = parts.slice(1, -1).join(':');
|
|
473
|
+
const dstFile = parts[parts.length - 1];
|
|
474
|
+
insertCache.run('import', srcFile, dstFile, weight);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// ── Analyzer registry (mirrors auditRunner DEFAULT_ANALYZERS) ────────
|
|
478
|
+
function buildAnalyzers() {
|
|
479
|
+
return {
|
|
480
|
+
conventions: {
|
|
481
|
+
name: 'conventions',
|
|
482
|
+
analyze: async (files, config) => {
|
|
483
|
+
CodeIndexDB.resetInstance();
|
|
484
|
+
const db = CodeIndexDB.getInstance(':memory:');
|
|
485
|
+
await db.initialize();
|
|
486
|
+
const rawDb = db.rawDb;
|
|
487
|
+
seedConventionsData(rawDb, files);
|
|
488
|
+
const analyzer = new UniversalConventionsAnalyzer();
|
|
489
|
+
const corpusDir = dirname(dirname(files[0] || '.'));
|
|
490
|
+
return analyzer.analyze(files, { ...config, projectRoot: resolve(corpusDir) });
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
documentation: {
|
|
494
|
+
name: 'documentation',
|
|
495
|
+
analyze: async (files, config) => {
|
|
496
|
+
const analyzer = new UniversalDocumentationAnalyzer();
|
|
497
|
+
return analyzer.analyze(files, config);
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
solid: {
|
|
501
|
+
name: 'solid',
|
|
502
|
+
analyze: async (files, config) => {
|
|
503
|
+
const analyzer = new UniversalSOLIDAnalyzer();
|
|
504
|
+
return analyzer.analyze(files, config);
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
dry: {
|
|
508
|
+
name: 'dry',
|
|
509
|
+
analyze: async (files, config) => {
|
|
510
|
+
const analyzer = new UniversalDRYAnalyzer();
|
|
511
|
+
return analyzer.analyze(files, config);
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
'diverging-clones': {
|
|
515
|
+
name: 'diverging-clones',
|
|
516
|
+
analyze: async (files, _config) => {
|
|
517
|
+
CodeIndexDB.resetInstance();
|
|
518
|
+
const db = CodeIndexDB.getInstance(':memory:');
|
|
519
|
+
await db.initialize();
|
|
520
|
+
const rawDb = db.rawDb;
|
|
521
|
+
seedDivergingClonesData(rawDb, files);
|
|
522
|
+
const violations = [];
|
|
523
|
+
const threshold = 0.05;
|
|
524
|
+
const requiredDeclines = 2;
|
|
525
|
+
// Run the divergence tracking pass (mirrors auditRunner Phase 2)
|
|
526
|
+
const fingerprints = rawDb
|
|
527
|
+
.prepare('SELECT DISTINCT pair_fingerprint FROM dry_pair_history')
|
|
528
|
+
.all();
|
|
529
|
+
if (fingerprints.length > 0) {
|
|
530
|
+
const getRows = rawDb.prepare(`
|
|
531
|
+
SELECT similarity, timestamp
|
|
532
|
+
FROM dry_pair_history
|
|
533
|
+
WHERE pair_fingerprint = ?
|
|
534
|
+
ORDER BY timestamp ASC
|
|
535
|
+
`);
|
|
536
|
+
for (const { pair_fingerprint: fp } of fingerprints) {
|
|
537
|
+
const rows = getRows.all(fp);
|
|
538
|
+
if (rows.length < requiredDeclines + 1)
|
|
539
|
+
continue;
|
|
540
|
+
let consecutiveDeclines = 0;
|
|
541
|
+
for (let i = rows.length - requiredDeclines; i < rows.length; i++) {
|
|
542
|
+
if (rows[i].similarity < rows[i - 1].similarity - threshold) {
|
|
543
|
+
consecutiveDeclines++;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (consecutiveDeclines >= requiredDeclines) {
|
|
547
|
+
const lastRow = rawDb.prepare(`
|
|
548
|
+
SELECT file1, file2, line1, line2 FROM dry_pair_history
|
|
549
|
+
WHERE pair_fingerprint = ?
|
|
550
|
+
ORDER BY timestamp DESC LIMIT 1
|
|
551
|
+
`).get(fp);
|
|
552
|
+
if (lastRow) {
|
|
553
|
+
const currentSim = rows[rows.length - 1].similarity;
|
|
554
|
+
const prevSim = rows[rows.length - 2].similarity;
|
|
555
|
+
const drop = Math.round((prevSim - currentSim) * 1000) / 1000;
|
|
556
|
+
violations.push({
|
|
557
|
+
file: lastRow.file1,
|
|
558
|
+
line: lastRow.line1,
|
|
559
|
+
severity: 'suggestion',
|
|
560
|
+
message: `Clone pair has diverged: similarity dropped ${drop} (from ${prevSim.toFixed(3)} to ${currentSim.toFixed(3)}) across ${requiredDeclines} consecutive runs (pair: ${fp.slice(0, 12)}…). Review ${lastRow.file1}:${lastRow.line1} and ${lastRow.file2}:${lastRow.line2} for diverged logic.`,
|
|
561
|
+
analyzer: 'dry',
|
|
562
|
+
rule: 'dry/diverging-clone',
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { violations, filesProcessed: files.length, executionTime: 0 };
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
'data-access': {
|
|
572
|
+
name: 'data-access',
|
|
573
|
+
analyze: async (files, config) => {
|
|
574
|
+
const analyzer = new UniversalDataAccessAnalyzer();
|
|
575
|
+
return analyzer.analyze(files, config);
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
schema: {
|
|
579
|
+
name: 'schema',
|
|
580
|
+
analyze: async (files, config) => {
|
|
581
|
+
const analyzer = new UniversalSchemaAnalyzer();
|
|
582
|
+
return analyzer.analyze(files, config);
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
styles: {
|
|
586
|
+
name: 'styles',
|
|
587
|
+
analyze: async (files, config) => {
|
|
588
|
+
CodeIndexDB.resetInstance();
|
|
589
|
+
const db = CodeIndexDB.getInstance(':memory:');
|
|
590
|
+
await db.initialize();
|
|
591
|
+
const rawDb = db.rawDb;
|
|
592
|
+
seedStylesData(rawDb, files);
|
|
593
|
+
const analyzer = new UniversalStylesAnalyzer();
|
|
594
|
+
return analyzer.analyze(files, config);
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
react: {
|
|
598
|
+
name: 'react',
|
|
599
|
+
analyze: async (files, config) => {
|
|
600
|
+
return reactAnalyzer.analyze(files, config, {});
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
invariants: {
|
|
604
|
+
name: 'invariants',
|
|
605
|
+
analyze: async (files, _config) => {
|
|
606
|
+
// invariants reads config from .codeauditor.json on disk,
|
|
607
|
+
// projectRoot is derived from the corpus directory
|
|
608
|
+
const corpusDir = dirname(files[0] || '.');
|
|
609
|
+
const projectDir = resolve(corpusDir, '..');
|
|
610
|
+
return invariantsAnalyzer.analyze(files, {}, {
|
|
611
|
+
projectRoot: projectDir,
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
'non-english': {
|
|
616
|
+
name: 'non-english',
|
|
617
|
+
analyze: async (files, config) => {
|
|
618
|
+
const analyzer = new UniversalDataAccessAnalyzer();
|
|
619
|
+
return analyzer.analyze(files, config);
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
// ── Graph — metrics-only (advisory, zero violations) ────────────────
|
|
623
|
+
graph: {
|
|
624
|
+
name: 'graph',
|
|
625
|
+
async analyze(_files, _config) {
|
|
626
|
+
CodeIndexDB.resetInstance();
|
|
627
|
+
const db = CodeIndexDB.getInstance(':memory:');
|
|
628
|
+
await db.initialize();
|
|
629
|
+
const rawDb = db.rawDb;
|
|
630
|
+
// Seed graph data into tables
|
|
631
|
+
seedGraphData(rawDb, _files);
|
|
632
|
+
// Dynamic import from built graph modules (bench runs against built dist)
|
|
633
|
+
// Use computed import path so tsc doesn't statically resolve to dist files (avoids TS5055)
|
|
634
|
+
const _importDist = (p) => import(p);
|
|
635
|
+
const callGraphMod = await _importDist('../../dist/graph/callGraph.js');
|
|
636
|
+
const { buildCallGraphFromCache, computeRisk } = callGraphMod;
|
|
637
|
+
const importGraphMod = await _importDist('../../dist/graph/importGraph.js');
|
|
638
|
+
const { buildImportGraphFromCache, detectCommunities, computeDirectoryPurity, computeMartinMetrics, } = importGraphMod;
|
|
639
|
+
// Build call graph from cache and compute risk rankings
|
|
640
|
+
const callGraphResult = buildCallGraphFromCache(rawDb);
|
|
641
|
+
const callGraph = callGraphResult.graph;
|
|
642
|
+
const riskEntries = computeRisk(rawDb, callGraph.adjacency, callGraph.nodeIds, callGraph.nodeNames, callGraph.nodePaths);
|
|
643
|
+
// Build import graph from cache and compute community detection + Martin metrics
|
|
644
|
+
const importGraph = buildImportGraphFromCache(rawDb);
|
|
645
|
+
const communityResult = detectCommunities(importGraph.adjacency, importGraph.filePaths);
|
|
646
|
+
const purityResult = computeDirectoryPurity(communityResult.communities, importGraph.filePaths);
|
|
647
|
+
const martinEntries = computeMartinMetrics(rawDb, importGraph);
|
|
648
|
+
// Collect metrics for expectedMetrics range assertions
|
|
649
|
+
const coreEntry = riskEntries.find((e) => e.functionName === 'calculateTotal');
|
|
650
|
+
const bridgeEntry = riskEntries.find((e) => e.functionName === 'validateOrder');
|
|
651
|
+
const untestedEntry = riskEntries.find((e) => e.functionName === 'processPayment');
|
|
652
|
+
// Abstractness: extract from Martin entries for interfaces directory
|
|
653
|
+
const interfacesMartin = martinEntries.find((e) => e.directory === 'src/interfaces');
|
|
654
|
+
const metrics = {
|
|
655
|
+
riskEntryCount: riskEntries.length,
|
|
656
|
+
corePageRank: coreEntry?.pageRankPercentile ?? 0,
|
|
657
|
+
bridgeBetweenness: bridgeEntry?.betweennessPercentile ?? 0,
|
|
658
|
+
untestedRisk: untestedEntry?.riskScore ?? 0,
|
|
659
|
+
communityCount: communityResult.communityCount,
|
|
660
|
+
agreementScore: purityResult.agreementScore,
|
|
661
|
+
abstractness: interfacesMartin?.abstractness ?? 0,
|
|
662
|
+
martinEntryCount: martinEntries.length,
|
|
663
|
+
// Extra diagnostics
|
|
664
|
+
callNodes: callGraph.nodeIds.size,
|
|
665
|
+
importNodes: importGraph.filePaths.size,
|
|
666
|
+
};
|
|
667
|
+
return {
|
|
668
|
+
violations: [],
|
|
669
|
+
filesProcessed: _files.length,
|
|
670
|
+
executionTime: 0,
|
|
671
|
+
extras: { metrics },
|
|
672
|
+
};
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
// ── Cross-Domain — metrics-only (DB-driven, advisory) ─────────────
|
|
676
|
+
'cross-domain': {
|
|
677
|
+
name: 'cross-domain',
|
|
678
|
+
async analyze(_files, config) {
|
|
679
|
+
CodeIndexDB.resetInstance();
|
|
680
|
+
const db = CodeIndexDB.getInstance(':memory:');
|
|
681
|
+
await db.initialize();
|
|
682
|
+
const rawDb = db.rawDb;
|
|
683
|
+
seedCrossDomainData(rawDb, _files);
|
|
684
|
+
const analyzer = new CrossDomainAnalyzer();
|
|
685
|
+
const result = await analyzer.analyze(_files, config);
|
|
686
|
+
// Collect metrics for expectedMetrics range assertions
|
|
687
|
+
const violations = result.violations;
|
|
688
|
+
const metrics = {
|
|
689
|
+
writtenNeverReadCount: violations.filter(v => v.rule === 'cross-domain/written-never-read').length,
|
|
690
|
+
readNeverWrittenCount: violations.filter(v => v.rule === 'cross-domain/read-never-written').length,
|
|
691
|
+
transactionBoundaryRiskCount: violations.filter(v => v.rule === 'cross-domain/transaction-boundary-risk').length,
|
|
692
|
+
validationBypassCount: violations.filter(v => v.rule === 'cross-domain/validation-bypass').length,
|
|
693
|
+
uncoveredRiskCount: violations.filter(v => v.rule === 'cross-domain/uncovered-risk').length,
|
|
694
|
+
totalViolations: violations.length,
|
|
695
|
+
};
|
|
696
|
+
return {
|
|
697
|
+
violations: [],
|
|
698
|
+
filesProcessed: _files.length,
|
|
699
|
+
executionTime: 0,
|
|
700
|
+
extras: { metrics },
|
|
701
|
+
};
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
707
|
+
/**
|
|
708
|
+
* Hook-contract validation: count violations with empty file paths or line 0.
|
|
709
|
+
* These always indicate a bug — every violation must anchor to a real file.
|
|
710
|
+
* Used in the bench runner (production guard is validateHookContract in auditRunner.ts).
|
|
711
|
+
*/
|
|
712
|
+
function countHookContractViolations(violations) {
|
|
713
|
+
return violations.filter(v => !v.file || v.file.trim() === '' || v.line === 0).length;
|
|
714
|
+
}
|
|
715
|
+
async function collectFiles(dir) {
|
|
716
|
+
const files = [];
|
|
717
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|scss)$/.test(entry.name) && !entry.name.includes('.d.')) {
|
|
720
|
+
files.push(join(entry.parentPath, entry.name));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return files;
|
|
724
|
+
}
|
|
725
|
+
function loadManifest(corpusDir) {
|
|
726
|
+
const path = join(corpusDir, 'expected.json');
|
|
727
|
+
if (!existsSync(path)) {
|
|
728
|
+
throw new Error(`Missing expected.json in ${corpusDir}`);
|
|
729
|
+
}
|
|
730
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
731
|
+
return raw;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Get the rule identifier for a violation.
|
|
735
|
+
* The React analyzer uses `violationType` instead of `rule`,
|
|
736
|
+
* and for hooks-naming it's in `details.rule`.
|
|
737
|
+
*/
|
|
738
|
+
function getViolationRule(v, analyzerName) {
|
|
739
|
+
// Direct rule field
|
|
740
|
+
if (v.rule)
|
|
741
|
+
return v.rule;
|
|
742
|
+
// React analyzer puts it in details.rule
|
|
743
|
+
if (analyzerName === 'react' && v.details?.rule) {
|
|
744
|
+
return v.details.rule;
|
|
745
|
+
}
|
|
746
|
+
// React analyzer uses violationType as the rule identifier
|
|
747
|
+
if (analyzerName === 'react' && v.violationType) {
|
|
748
|
+
return v.violationType;
|
|
749
|
+
}
|
|
750
|
+
return '';
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Get the symbol for a violation using the same priority chain as extractSymbol.
|
|
754
|
+
*/
|
|
755
|
+
function getViolationSymbol(v) {
|
|
756
|
+
return extractSymbol(v);
|
|
757
|
+
}
|
|
758
|
+
function loadBaseline() {
|
|
759
|
+
if (!existsSync(BASELINE_PATH))
|
|
760
|
+
return null;
|
|
761
|
+
return JSON.parse(readFileSync(BASELINE_PATH, 'utf-8'));
|
|
762
|
+
}
|
|
763
|
+
function saveBaseline(baseline) {
|
|
764
|
+
const dir = dirname(BASELINE_PATH);
|
|
765
|
+
if (!existsSync(dir))
|
|
766
|
+
mkdirSync(dir, { recursive: true });
|
|
767
|
+
writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2) + '\n');
|
|
768
|
+
}
|
|
769
|
+
// ── Core matching logic ──────────────────────────────────────────────────
|
|
770
|
+
function matchViolations(violations, expected, nearMissFiles, analyzerName, knownMisses = []) {
|
|
771
|
+
// Hook-contract regression guard: no violation may carry an empty file path or line 0.
|
|
772
|
+
// This is a permanent integration test — every analyzer must produce properly anchored violations.
|
|
773
|
+
const hookContractViolations = violations.filter(v => !v.file || v.file.trim() === '' || v.line === 0).length;
|
|
774
|
+
const nearMissSet = new Set(nearMissFiles);
|
|
775
|
+
// Partition violations by whether they're from near-miss files
|
|
776
|
+
const nearMissViolations = [];
|
|
777
|
+
const candidateViolations = [];
|
|
778
|
+
for (const v of violations) {
|
|
779
|
+
const vFile = relative('.', v.file);
|
|
780
|
+
const isNearMiss = nearMissFiles.some(nm => vFile.endsWith(nm));
|
|
781
|
+
if (isNearMiss) {
|
|
782
|
+
nearMissViolations.push(v);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
candidateViolations.push(v);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// Match expected violations to actual
|
|
789
|
+
const details = [];
|
|
790
|
+
const matchedActual = new Set(); // indices into candidateViolations
|
|
791
|
+
let falseNegatives = 0;
|
|
792
|
+
for (const exp of expected) {
|
|
793
|
+
let found = false;
|
|
794
|
+
for (let i = 0; i < candidateViolations.length; i++) {
|
|
795
|
+
if (matchedActual.has(i))
|
|
796
|
+
continue;
|
|
797
|
+
const v = candidateViolations[i];
|
|
798
|
+
const vFile = relative('.', v.file);
|
|
799
|
+
const vRule = getViolationRule(v, analyzerName);
|
|
800
|
+
const vSymbol = getViolationSymbol(v);
|
|
801
|
+
// Match by file (suffix), rule, and optionally symbol
|
|
802
|
+
const fileMatch = vFile.endsWith(exp.file);
|
|
803
|
+
const ruleMatch = vRule === exp.rule;
|
|
804
|
+
// Symbol match: only when expected.symbol is explicitly set
|
|
805
|
+
const symbolMatch = !exp.symbol || exp.symbol === '' || vSymbol === exp.symbol;
|
|
806
|
+
if (fileMatch && ruleMatch && symbolMatch) {
|
|
807
|
+
matchedActual.add(i);
|
|
808
|
+
found = true;
|
|
809
|
+
details.push({ expected: exp, matched: true, actualViolation: v });
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (!found) {
|
|
814
|
+
falseNegatives++;
|
|
815
|
+
details.push({ expected: exp, matched: false });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const truePositives = matchedActual.size;
|
|
819
|
+
// Collect unmatched expected entries for known-miss reconciliation
|
|
820
|
+
const unmatchedExpected = details
|
|
821
|
+
.filter(d => !d.matched)
|
|
822
|
+
.map(d => d.expected);
|
|
823
|
+
// Match known-miss entries against remaining actual violations.
|
|
824
|
+
// Known misses are ground-truth violations the analyzer cannot currently
|
|
825
|
+
// detect. They are NOT false negatives — they're acknowledged gaps.
|
|
826
|
+
// If a known-miss entry DOES match (the limitation was fixed), it becomes
|
|
827
|
+
// a "recovered miss" — informational, not a scoring penalty.
|
|
828
|
+
const knownMissDetails = [];
|
|
829
|
+
const matchedActualByKnown = new Set();
|
|
830
|
+
let reconciledFalseNegatives = 0;
|
|
831
|
+
for (const km of knownMisses) {
|
|
832
|
+
let found = false;
|
|
833
|
+
// First: check if this known-miss matches a remaining actual violation
|
|
834
|
+
// (recovered miss — the limitation was fixed)
|
|
835
|
+
for (let i = 0; i < candidateViolations.length; i++) {
|
|
836
|
+
if (matchedActual.has(i) || matchedActualByKnown.has(i))
|
|
837
|
+
continue;
|
|
838
|
+
const v = candidateViolations[i];
|
|
839
|
+
const vFile = relative('.', v.file);
|
|
840
|
+
const vRule = getViolationRule(v, analyzerName);
|
|
841
|
+
const vSymbol = getViolationSymbol(v);
|
|
842
|
+
const fileMatch = vFile.endsWith(km.file);
|
|
843
|
+
const ruleMatch = vRule === km.rule;
|
|
844
|
+
const symbolMatch = !km.symbol || km.symbol === '' || vSymbol === km.symbol;
|
|
845
|
+
if (fileMatch && ruleMatch && symbolMatch) {
|
|
846
|
+
matchedActualByKnown.add(i);
|
|
847
|
+
found = true;
|
|
848
|
+
knownMissDetails.push({ expected: km, matched: true, actualViolation: v });
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (!found) {
|
|
853
|
+
// Second: check if a matched expected entry now covers this known-miss's
|
|
854
|
+
// file+rule — the limitation was fixed, but the expected entry consumed
|
|
855
|
+
// the actual violation before the known-miss could claim it directly.
|
|
856
|
+
const recovered = details.some(d => d.matched &&
|
|
857
|
+
d.expected.file === km.file &&
|
|
858
|
+
d.expected.rule === km.rule &&
|
|
859
|
+
(!km.symbol || km.symbol === '' || d.expected.symbol === km.symbol));
|
|
860
|
+
if (recovered) {
|
|
861
|
+
knownMissDetails.push({ expected: km, matched: true });
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
knownMissDetails.push({ expected: km, matched: false });
|
|
865
|
+
// Check if this known-miss corresponds to an unmatched expected entry —
|
|
866
|
+
// if so, it explains a false negative and should be reconciled.
|
|
867
|
+
const reconciles = unmatchedExpected.some(ue => ue.file === km.file && ue.rule === km.rule &&
|
|
868
|
+
(!km.symbol || km.symbol === '' || ue.symbol === km.symbol));
|
|
869
|
+
if (reconciles) {
|
|
870
|
+
reconciledFalseNegatives++;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const recoveredMisses = knownMissDetails.filter(d => d.matched).length;
|
|
876
|
+
const stillKnownMisses = knownMisses.length - recoveredMisses;
|
|
877
|
+
// Reduce falseNegatives by the number reconciled via known-miss annotations
|
|
878
|
+
falseNegatives = Math.max(0, falseNegatives - reconciledFalseNegatives);
|
|
879
|
+
// Unmatched actual violations — exclude those matched by known-miss entries
|
|
880
|
+
// so fixing a known limitation doesn't create false positives.
|
|
881
|
+
const unmatchedActual = candidateViolations.length - truePositives - matchedActualByKnown.size;
|
|
882
|
+
const falsePositives = Math.max(0, unmatchedActual);
|
|
883
|
+
const nearMissFailures = nearMissViolations.filter(v => {
|
|
884
|
+
const vRule = getViolationRule(v, analyzerName);
|
|
885
|
+
return vRule !== ''; // ignore violations without identifiable rules
|
|
886
|
+
});
|
|
887
|
+
// Near-miss files that fired
|
|
888
|
+
const nearMissResults = nearMissFiles.map(nm => {
|
|
889
|
+
const viols = nearMissViolations.filter(v => relative('.', v.file).endsWith(nm));
|
|
890
|
+
return { file: nm, violations: viols.length, passed: viols.length === 0 };
|
|
891
|
+
});
|
|
892
|
+
// Add near-miss false positives
|
|
893
|
+
const totalFalsePositives = falsePositives + nearMissFailures.length;
|
|
894
|
+
const precision = truePositives + totalFalsePositives > 0
|
|
895
|
+
? truePositives / (truePositives + totalFalsePositives)
|
|
896
|
+
: 1.0;
|
|
897
|
+
const recall = truePositives + falseNegatives > 0
|
|
898
|
+
? truePositives / (truePositives + falseNegatives)
|
|
899
|
+
: 1.0;
|
|
900
|
+
const f1 = precision + recall > 0
|
|
901
|
+
? (2 * precision * recall) / (precision + recall)
|
|
902
|
+
: 0;
|
|
903
|
+
// True recall: against full ground truth (expected + non-overlapping known misses).
|
|
904
|
+
// Known-miss entries that reconcile with an expected violation are annotations on that
|
|
905
|
+
// same ground-truth entry — don't double-count. Non-overlapping known-misses (if any)
|
|
906
|
+
// represent additional real violations not yet listed in expectedViolations.
|
|
907
|
+
const totalGroundTruth = expected.length + (stillKnownMisses - reconciledFalseNegatives);
|
|
908
|
+
const trueRecall = totalGroundTruth > 0
|
|
909
|
+
? truePositives / totalGroundTruth
|
|
910
|
+
: 1.0;
|
|
911
|
+
const trueF1 = precision + trueRecall > 0
|
|
912
|
+
? (2 * precision * trueRecall) / (precision + trueRecall)
|
|
913
|
+
: 0;
|
|
914
|
+
const warnings = [];
|
|
915
|
+
if (nearMissFailures.length > 0) {
|
|
916
|
+
warnings.push(`${nearMissFailures.length} unexpected violation(s) in near-miss files`);
|
|
917
|
+
}
|
|
918
|
+
if (recoveredMisses > 0) {
|
|
919
|
+
warnings.push(`${recoveredMisses} known-miss(es) flipped to recovered — delete the stale knownMisses annotation(s)`);
|
|
920
|
+
}
|
|
921
|
+
// Per-rule metrics — group matched/unmatched by rule
|
|
922
|
+
const ruleCounts = new Map();
|
|
923
|
+
const ensureRule = (rule) => {
|
|
924
|
+
if (!ruleCounts.has(rule))
|
|
925
|
+
ruleCounts.set(rule, { tp: 0, fp: 0, fn: 0, km: 0, rm: 0 });
|
|
926
|
+
return ruleCounts.get(rule);
|
|
927
|
+
};
|
|
928
|
+
for (const d of details) {
|
|
929
|
+
const rule = d.expected.rule || 'unknown';
|
|
930
|
+
const rc = ensureRule(rule);
|
|
931
|
+
if (d.matched)
|
|
932
|
+
rc.tp++;
|
|
933
|
+
else
|
|
934
|
+
rc.fn++;
|
|
935
|
+
}
|
|
936
|
+
for (let i = 0; i < candidateViolations.length; i++) {
|
|
937
|
+
if (matchedActual.has(i) || matchedActualByKnown.has(i))
|
|
938
|
+
continue;
|
|
939
|
+
const rule = getViolationRule(candidateViolations[i], analyzerName) || 'unknown';
|
|
940
|
+
ensureRule(rule).fp++;
|
|
941
|
+
}
|
|
942
|
+
for (const v of nearMissViolations) {
|
|
943
|
+
const rule = getViolationRule(v, analyzerName) || 'unknown';
|
|
944
|
+
ensureRule(rule).fp++;
|
|
945
|
+
}
|
|
946
|
+
for (const km of knownMissDetails) {
|
|
947
|
+
const rule = km.expected.rule || 'unknown';
|
|
948
|
+
const rc = ensureRule(rule);
|
|
949
|
+
if (km.matched)
|
|
950
|
+
rc.rm++;
|
|
951
|
+
else
|
|
952
|
+
rc.km++;
|
|
953
|
+
}
|
|
954
|
+
const ruleMetrics = Array.from(ruleCounts.entries())
|
|
955
|
+
.map(([rule, rc]) => {
|
|
956
|
+
const rPrecision = rc.tp + rc.fp > 0 ? rc.tp / (rc.tp + rc.fp) : 1.0;
|
|
957
|
+
const rRecall = rc.tp + rc.fn > 0 ? rc.tp / (rc.tp + rc.fn) : 1.0;
|
|
958
|
+
const rF1 = rPrecision + rRecall > 0 ? (2 * rPrecision * rRecall) / (rPrecision + rRecall) : 0;
|
|
959
|
+
const rTotalGT = rc.tp + rc.fn + rc.km;
|
|
960
|
+
const rTrueRecall = rTotalGT > 0 ? rc.tp / rTotalGT : 1.0;
|
|
961
|
+
const rTrueF1 = rPrecision + rTrueRecall > 0 ? (2 * rPrecision * rTrueRecall) / (rPrecision + rTrueRecall) : 0;
|
|
962
|
+
return {
|
|
963
|
+
rule,
|
|
964
|
+
truePositives: rc.tp,
|
|
965
|
+
falsePositives: rc.fp,
|
|
966
|
+
falseNegatives: rc.fn,
|
|
967
|
+
precision: Math.round(rPrecision * 10000) / 10000,
|
|
968
|
+
recall: Math.round(rRecall * 10000) / 10000,
|
|
969
|
+
f1: Math.round(rF1 * 10000) / 10000,
|
|
970
|
+
knownMisses: rc.km,
|
|
971
|
+
recoveredMisses: rc.rm,
|
|
972
|
+
trueRecall: Math.round(rTrueRecall * 10000) / 10000,
|
|
973
|
+
trueF1: Math.round(rTrueF1 * 10000) / 10000,
|
|
974
|
+
};
|
|
975
|
+
})
|
|
976
|
+
.sort((a, b) => a.rule.localeCompare(b.rule));
|
|
977
|
+
return {
|
|
978
|
+
truePositives,
|
|
979
|
+
falsePositives: totalFalsePositives,
|
|
980
|
+
falseNegatives,
|
|
981
|
+
precision: Math.round(precision * 10000) / 10000,
|
|
982
|
+
recall: Math.round(recall * 10000) / 10000,
|
|
983
|
+
f1: Math.round(f1 * 10000) / 10000,
|
|
984
|
+
trueRecall: Math.round(trueRecall * 10000) / 10000,
|
|
985
|
+
trueF1: Math.round(trueF1 * 10000) / 10000,
|
|
986
|
+
knownMisses: stillKnownMisses,
|
|
987
|
+
recoveredMisses,
|
|
988
|
+
ruleMetrics,
|
|
989
|
+
nearMissResults,
|
|
990
|
+
details,
|
|
991
|
+
knownMissDetails,
|
|
992
|
+
warnings,
|
|
993
|
+
hookContractViolations,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
// ── Main ─────────────────────────────────────────────────────────────────
|
|
997
|
+
async function main() {
|
|
998
|
+
const args = process.argv.slice(2);
|
|
999
|
+
const jsonOnly = args.includes('--json');
|
|
1000
|
+
const sweepMode = args.includes('--sweep');
|
|
1001
|
+
const sweepParam = args.includes('--sweep')
|
|
1002
|
+
? (args[args.indexOf('--sweep') + 1] ?? null)
|
|
1003
|
+
: null;
|
|
1004
|
+
if (!jsonOnly) {
|
|
1005
|
+
console.log('\n🔬 Code Auditor — Benchmark Harness\n');
|
|
1006
|
+
}
|
|
1007
|
+
if (sweepMode) {
|
|
1008
|
+
initializeLanguages();
|
|
1009
|
+
await initParsers();
|
|
1010
|
+
const corpusDirs = [];
|
|
1011
|
+
const entries = await readdir(CORPUS_ROOT, { withFileTypes: true });
|
|
1012
|
+
for (const entry of entries) {
|
|
1013
|
+
if (entry.isDirectory())
|
|
1014
|
+
corpusDirs.push(join(CORPUS_ROOT, entry.name));
|
|
1015
|
+
}
|
|
1016
|
+
const analyzersForSweep = buildAnalyzers();
|
|
1017
|
+
const { curves } = await runSweep(corpusDirs, analyzersForSweep, jsonOnly, sweepParam);
|
|
1018
|
+
if (!jsonOnly) {
|
|
1019
|
+
console.log('══════════════════════════════════════════════');
|
|
1020
|
+
console.log(' Sweep Summary');
|
|
1021
|
+
console.log('══════════════════════════════════════════════');
|
|
1022
|
+
const changed = curves.filter(c => !c.confirmed);
|
|
1023
|
+
const confirmed = curves.filter(c => c.confirmed);
|
|
1024
|
+
console.log(` Total: ${curves.length} Confirmed: ${confirmed.length} Changed: ${changed.length}`);
|
|
1025
|
+
if (changed.length > 0) {
|
|
1026
|
+
console.log('\n Changed defaults (precision-first):');
|
|
1027
|
+
for (const c of changed) {
|
|
1028
|
+
console.log(` ${c.parameter.label}: ${c.parameter.configKey} = ${c.currentDefault} → ${c.recommended.value}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
console.log();
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
console.log(JSON.stringify({ sweeps: curves.map(c => ({
|
|
1035
|
+
parameter: c.parameter,
|
|
1036
|
+
recommended: c.recommended.value,
|
|
1037
|
+
currentDefault: c.currentDefault,
|
|
1038
|
+
confirmed: c.confirmed,
|
|
1039
|
+
points: c.points,
|
|
1040
|
+
})) }, null, 2));
|
|
1041
|
+
}
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const report = await runBench();
|
|
1045
|
+
if (!jsonOnly) {
|
|
1046
|
+
// Print per-analyzer results
|
|
1047
|
+
for (const [name, metrics] of Object.entries(report.analyzers).sort()) {
|
|
1048
|
+
printAnalyzerResults(name, metrics);
|
|
1049
|
+
}
|
|
1050
|
+
printReport(report);
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1054
|
+
}
|
|
1055
|
+
// Regression gate — fail if any analyzer dropped below baseline
|
|
1056
|
+
const regressions = report.baselineComparison?.regressions ?? [];
|
|
1057
|
+
if (regressions.length > 0) {
|
|
1058
|
+
console.error(`\n❌ F1 regression detected: ${regressions.length} analyzer(s) below baseline`);
|
|
1059
|
+
for (const r of regressions) {
|
|
1060
|
+
console.error(` ${r}`);
|
|
1061
|
+
}
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
// Also fail if any basic fixture didn't get 100% recall (unexpected gap)
|
|
1065
|
+
const totalFailures = report.summary.failed;
|
|
1066
|
+
if (totalFailures > 0) {
|
|
1067
|
+
console.error(`\n❌ ${totalFailures} analyzer(s) have unmatched expected violations`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// ── Single corpus run ────────────────────────────────────────────────────
|
|
1072
|
+
async function runSingleCorpus(corpusDir, analyzers, report, print) {
|
|
1073
|
+
const name = corpusDir.split('/').pop();
|
|
1074
|
+
const manifest = loadManifest(corpusDir);
|
|
1075
|
+
if (print) {
|
|
1076
|
+
console.log(`── ${manifest.analyzer} (${manifest.description}) ──`);
|
|
1077
|
+
}
|
|
1078
|
+
const runner = analyzers[manifest.analyzer];
|
|
1079
|
+
if (!runner) {
|
|
1080
|
+
console.log(` ⚠️ No runner for analyzer "${manifest.analyzer}" — skipping`);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const srcDir = join(corpusDir, 'src');
|
|
1084
|
+
const files = existsSync(srcDir) ? await collectFiles(srcDir) : [];
|
|
1085
|
+
if (files.length === 0) {
|
|
1086
|
+
console.log(` ⚠️ No source files found in ${corpusDir}/src — skipping`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const config = manifest.config ?? {};
|
|
1090
|
+
const result = await runner.analyze(files, config);
|
|
1091
|
+
// Hook-contract regression guard: surface contract-violating violations
|
|
1092
|
+
// discovered during bench runs. Production guard is validateHookContract()
|
|
1093
|
+
// in auditRunner.ts — this is the permanent test gate.
|
|
1094
|
+
const contractViolations = countHookContractViolations(result.violations);
|
|
1095
|
+
// Metrics-only analyzers (e.g. graph): zero violations, real gate is expectedMetrics ranges
|
|
1096
|
+
const isMetricsOnly = manifest.kind === 'metrics';
|
|
1097
|
+
if (isMetricsOnly) {
|
|
1098
|
+
const actualMetrics = result.extras?.metrics ?? {};
|
|
1099
|
+
const expectedMetrics = manifest.expectedMetrics ?? {};
|
|
1100
|
+
const warnings = [];
|
|
1101
|
+
let metricsPassed = true;
|
|
1102
|
+
for (const [key, range] of Object.entries(expectedMetrics)) {
|
|
1103
|
+
const actual = actualMetrics[key] ?? null;
|
|
1104
|
+
if (actual === null) {
|
|
1105
|
+
warnings.push(`Metric "${key}" not found in analyzer output`);
|
|
1106
|
+
metricsPassed = false;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (range.min !== undefined && actual < range.min) {
|
|
1110
|
+
warnings.push(`Metric "${key}" = ${actual} (< min ${range.min})`);
|
|
1111
|
+
metricsPassed = false;
|
|
1112
|
+
}
|
|
1113
|
+
if (range.max !== undefined && actual > range.max) {
|
|
1114
|
+
warnings.push(`Metric "${key}" = ${actual} (> max ${range.max})`);
|
|
1115
|
+
metricsPassed = false;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Vacuous violation metrics — empty sets, precision/recall/F1 = 1.0
|
|
1119
|
+
const metrics = {
|
|
1120
|
+
truePositives: 0,
|
|
1121
|
+
falsePositives: 0,
|
|
1122
|
+
falseNegatives: 0,
|
|
1123
|
+
precision: 1.0,
|
|
1124
|
+
recall: 1.0,
|
|
1125
|
+
f1: 1.0,
|
|
1126
|
+
trueRecall: 1.0,
|
|
1127
|
+
trueF1: 1.0,
|
|
1128
|
+
knownMisses: 0,
|
|
1129
|
+
recoveredMisses: 0,
|
|
1130
|
+
ruleMetrics: [],
|
|
1131
|
+
nearMissResults: [],
|
|
1132
|
+
details: [],
|
|
1133
|
+
knownMissDetails: [],
|
|
1134
|
+
warnings,
|
|
1135
|
+
hookContractViolations: contractViolations,
|
|
1136
|
+
};
|
|
1137
|
+
report.analyzers[manifest.analyzer] = metrics;
|
|
1138
|
+
if (print) {
|
|
1139
|
+
if (metricsPassed) {
|
|
1140
|
+
console.log(` ✅ ${name}: ${Object.keys(expectedMetrics).length} metric ranges passed`);
|
|
1141
|
+
for (const [key, val] of Object.entries(actualMetrics)) {
|
|
1142
|
+
if (expectedMetrics[key] !== undefined) {
|
|
1143
|
+
console.log(` ${key}: ${typeof val === 'number' ? val.toFixed(4) : val}`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
console.log(` ❌ ${name}: ${warnings.length} metric range failures`);
|
|
1149
|
+
for (const w of warnings) {
|
|
1150
|
+
console.log(` ${w}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const metrics = matchViolations(result.violations, manifest.expectedViolations, manifest.nearMissFiles, manifest.analyzer, manifest.knownMisses ?? []);
|
|
1157
|
+
report.analyzers[manifest.analyzer] = metrics;
|
|
1158
|
+
if (print) {
|
|
1159
|
+
printAnalyzerResults(manifest.analyzer, metrics);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Define sweepable parameters per analyzer.
|
|
1164
|
+
* Each parameter has a config key, label, and range of values to sweep.
|
|
1165
|
+
*/
|
|
1166
|
+
function buildSweepParameters() {
|
|
1167
|
+
return [
|
|
1168
|
+
// ── DRY analyzer ──────────────────────────────────────────────────────
|
|
1169
|
+
{ configKey: 'minLineThreshold', label: 'DRY minLineThreshold', analyzer: 'dry',
|
|
1170
|
+
shippedDefault: 15, values: [3, 5, 8, 10, 12, 15, 20, 30] },
|
|
1171
|
+
{ configKey: 'similarityThreshold', label: 'DRY similarityThreshold', analyzer: 'dry',
|
|
1172
|
+
shippedDefault: 0.85, values: [0.50, 0.60, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95] },
|
|
1173
|
+
// ── React analyzer ────────────────────────────────────────────────────
|
|
1174
|
+
{ configKey: 'maxComponentComplexity', label: 'React maxComponentComplexity', analyzer: 'react',
|
|
1175
|
+
shippedDefault: 15, values: [5, 8, 10, 12, 15, 20, 25, 30] },
|
|
1176
|
+
{ configKey: 'wrapperMinUsages', label: 'React wrapperMinUsages', analyzer: 'react',
|
|
1177
|
+
shippedDefault: 4, values: [2, 3, 4, 5, 6, 8, 10, 15] },
|
|
1178
|
+
// ── SOLID analyzer — method-level complexity ─────────────────────────
|
|
1179
|
+
{ configKey: 'maxMethodComplexity', label: 'SOLID maxMethodComplexity', analyzer: 'solid',
|
|
1180
|
+
shippedDefault: 50, values: [5, 10, 15, 20, 30, 50, 75, 100] },
|
|
1181
|
+
{ configKey: 'maxLinesPerMethod', label: 'SOLID maxLinesPerMethod', analyzer: 'solid',
|
|
1182
|
+
shippedDefault: 50, values: [10, 20, 30, 50, 75, 100, 150] },
|
|
1183
|
+
{ configKey: 'maxParametersPerMethod', label: 'SOLID maxParametersPerMethod', analyzer: 'solid',
|
|
1184
|
+
shippedDefault: 4, values: [2, 3, 4, 5, 6, 8, 10] },
|
|
1185
|
+
{ configKey: 'maxImportsPerFile', label: 'SOLID maxImportsPerFile', analyzer: 'solid',
|
|
1186
|
+
shippedDefault: 20, values: [5, 10, 15, 20, 25, 30, 40, 50] },
|
|
1187
|
+
{ configKey: 'classMethodsThreshold', label: 'SOLID classMethodsThreshold', analyzer: 'solid',
|
|
1188
|
+
shippedDefault: 15, values: [5, 8, 10, 12, 15, 20, 30] },
|
|
1189
|
+
{ configKey: 'classAggregateComplexity', label: 'SOLID classAggregateComplexity', analyzer: 'solid',
|
|
1190
|
+
shippedDefault: 100, values: [20, 40, 60, 80, 100, 150, 200, 300] },
|
|
1191
|
+
// ── Schema analyzer ───────────────────────────────────────────────────
|
|
1192
|
+
{ configKey: 'maxQueriesPerFunction', label: 'Schema maxQueriesPerFunction', analyzer: 'schema',
|
|
1193
|
+
shippedDefault: 5, values: [1, 2, 3, 4, 5, 7, 10, 15] },
|
|
1194
|
+
// ── Data-access analyzer ──────────────────────────────────────────────
|
|
1195
|
+
{ configKey: 'joinedTableCount', label: 'Data-access joinedTableCount', analyzer: 'data-access',
|
|
1196
|
+
shippedDefault: 2, values: [2, 3, 4, 5, 6, 8, 10] },
|
|
1197
|
+
// ── Documentation analyzer ────────────────────────────────────────────
|
|
1198
|
+
{ configKey: 'minDescriptionLength', label: 'Documentation minDescriptionLength', analyzer: 'documentation',
|
|
1199
|
+
shippedDefault: 10, values: [2, 5, 10, 15, 20, 30, 50] },
|
|
1200
|
+
// ── Style analyzer (Spec 10 — registered for future use) ──────────────
|
|
1201
|
+
{ configKey: 'colorDeltaE', label: 'Style colorDeltaE', analyzer: 'styles',
|
|
1202
|
+
shippedDefault: 3.0, values: [1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 15.0] },
|
|
1203
|
+
{ configKey: 'outlierMaxShare', label: 'Style outlierMaxShare', analyzer: 'styles',
|
|
1204
|
+
shippedDefault: 0.05, values: [0.01, 0.03, 0.05, 0.07, 0.10, 0.15, 0.20] },
|
|
1205
|
+
{ configKey: 'modeMinCount', label: 'Style modeMinCount', analyzer: 'styles',
|
|
1206
|
+
shippedDefault: 3, values: [1, 2, 3, 5, 7, 10, 15] },
|
|
1207
|
+
{ configKey: 'styleSimilarityThreshold', label: 'Style similarityThreshold', analyzer: 'styles',
|
|
1208
|
+
shippedDefault: 0.75, values: [0.50, 0.60, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95] },
|
|
1209
|
+
{ configKey: 'zIndexMaxDistinct', label: 'Style zIndexMaxDistinct', analyzer: 'styles',
|
|
1210
|
+
shippedDefault: 5, values: [3, 5, 7, 10, 15, 20, 30] },
|
|
1211
|
+
{ configKey: 'minCorpus', label: 'Style minCorpus', analyzer: 'styles',
|
|
1212
|
+
shippedDefault: 3, values: [1, 2, 3, 5, 8, 10, 15] },
|
|
1213
|
+
// ── Cross-Domain analyzer ──────────────────────────────────────────
|
|
1214
|
+
{ configKey: 'schemaLifecycle.txnTableMax', label: 'Cross-Domain txnTableMax', analyzer: 'cross-domain',
|
|
1215
|
+
shippedDefault: 4, values: [2, 3, 4, 5, 6, 8, 10] },
|
|
1216
|
+
{ configKey: 'validatorBypass.modeShare', label: 'Cross-Domain modeShare', analyzer: 'cross-domain',
|
|
1217
|
+
shippedDefault: 0.8, values: [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95] },
|
|
1218
|
+
{ configKey: 'validatorBypass.depth', label: 'Cross-Domain depth', analyzer: 'cross-domain',
|
|
1219
|
+
shippedDefault: 3, values: [1, 2, 3, 4, 5] },
|
|
1220
|
+
];
|
|
1221
|
+
}
|
|
1222
|
+
async function runSweep(corpusDirs, analyzers, jsonOnly, sweepParam) {
|
|
1223
|
+
const parameters = buildSweepParameters().filter(p => {
|
|
1224
|
+
if (!sweepParam)
|
|
1225
|
+
return true;
|
|
1226
|
+
// Filter by config key or analyzer match
|
|
1227
|
+
return p.configKey === sweepParam || p.analyzer === sweepParam || p.label.includes(sweepParam);
|
|
1228
|
+
});
|
|
1229
|
+
const allCurves = [];
|
|
1230
|
+
if (!jsonOnly) {
|
|
1231
|
+
console.log(`Sweeping ${parameters.length} parameters across ${corpusDirs.length} fixtures...\n`);
|
|
1232
|
+
}
|
|
1233
|
+
for (const param of parameters) {
|
|
1234
|
+
// Find the corpus dir for this analyzer
|
|
1235
|
+
const corpusDir = corpusDirs.find(d => d.endsWith(param.analyzer));
|
|
1236
|
+
if (!corpusDir) {
|
|
1237
|
+
if (!jsonOnly)
|
|
1238
|
+
console.log(` ⚠️ No corpus for ${param.analyzer} — skipping ${param.label}`);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const manifest = loadManifest(corpusDir);
|
|
1242
|
+
const runner = analyzers[manifest.analyzer];
|
|
1243
|
+
if (!runner)
|
|
1244
|
+
continue;
|
|
1245
|
+
const srcDir = join(corpusDir, 'src');
|
|
1246
|
+
if (!existsSync(srcDir))
|
|
1247
|
+
continue;
|
|
1248
|
+
const files = await collectFiles(srcDir);
|
|
1249
|
+
// Use the shipped default — what users actually get without custom config
|
|
1250
|
+
const currentDefault = param.shippedDefault;
|
|
1251
|
+
const points = [];
|
|
1252
|
+
for (const value of param.values) {
|
|
1253
|
+
const config = { ...(manifest.config ?? {}) };
|
|
1254
|
+
// Handle nested keys like 'performanceThresholds.joinedTableCount'
|
|
1255
|
+
if (param.configKey.includes('.')) {
|
|
1256
|
+
const [parent, child] = param.configKey.split('.');
|
|
1257
|
+
config[parent] = { ...(config[parent] ?? {}), [child]: value };
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
config[param.configKey] = value;
|
|
1261
|
+
}
|
|
1262
|
+
const result = await runner.analyze(files, config);
|
|
1263
|
+
const metrics = matchViolations(result.violations, manifest.expectedViolations, manifest.nearMissFiles, manifest.analyzer, manifest.knownMisses ?? []);
|
|
1264
|
+
points.push({
|
|
1265
|
+
value,
|
|
1266
|
+
precision: metrics.precision,
|
|
1267
|
+
recall: metrics.recall,
|
|
1268
|
+
f1: metrics.f1,
|
|
1269
|
+
truePositives: metrics.truePositives,
|
|
1270
|
+
falsePositives: metrics.falsePositives,
|
|
1271
|
+
falseNegatives: metrics.falseNegatives,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
// Pick precision-first operating point: highest precision, break ties with highest recall
|
|
1275
|
+
const recommended = points.reduce((best, p) => {
|
|
1276
|
+
if (p.precision > best.precision)
|
|
1277
|
+
return p;
|
|
1278
|
+
if (p.precision === best.precision && p.recall > best.recall)
|
|
1279
|
+
return p;
|
|
1280
|
+
return best;
|
|
1281
|
+
});
|
|
1282
|
+
const curve = {
|
|
1283
|
+
parameter: param,
|
|
1284
|
+
points,
|
|
1285
|
+
recommended,
|
|
1286
|
+
currentDefault,
|
|
1287
|
+
confirmed: Math.abs(recommended.value - currentDefault) < 0.01,
|
|
1288
|
+
};
|
|
1289
|
+
allCurves.push(curve);
|
|
1290
|
+
if (!jsonOnly) {
|
|
1291
|
+
printSweepCurve(curve);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// Generate sweep report
|
|
1295
|
+
if (!jsonOnly && allCurves.length > 0) {
|
|
1296
|
+
generateSweepReport(allCurves);
|
|
1297
|
+
}
|
|
1298
|
+
return { curves: allCurves };
|
|
1299
|
+
}
|
|
1300
|
+
function printSweepCurve(curve) {
|
|
1301
|
+
const { parameter, points, recommended, currentDefault } = curve;
|
|
1302
|
+
console.log(`── ${parameter.label} ──`);
|
|
1303
|
+
console.log(` Value │ Precision │ Recall │ F1 │ TP │ FP │ FN`);
|
|
1304
|
+
console.log(` ${'─'.repeat(6)}┼${'─'.repeat(11)}┼${'─'.repeat(11)}┼${'─'.repeat(11)}┼${'─'.repeat(4)}┼${'─'.repeat(4)}┼${'─'.repeat(4)}`);
|
|
1305
|
+
for (const p of points) {
|
|
1306
|
+
const marker = p.value === recommended.value ? '◀' : ' ';
|
|
1307
|
+
console.log(` ${String(p.value).padEnd(6)}│ ${p.precision.toFixed(4).padEnd(9)}│ ${p.recall.toFixed(4).padEnd(9)}│ ${p.f1.toFixed(4).padEnd(9)}│ ${String(p.truePositives).padEnd(2)}│ ${String(p.falsePositives).padEnd(2)}│ ${String(p.falseNegatives).padEnd(2)} ${marker}`);
|
|
1308
|
+
}
|
|
1309
|
+
const status = curve.confirmed ? '✅ CONFIRMED' : `⚠️ CHANGE: ${currentDefault} → ${recommended.value}`;
|
|
1310
|
+
console.log(` Default: ${currentDefault} → Recommended: ${recommended.value} (precision-first) ${status}\n`);
|
|
1311
|
+
}
|
|
1312
|
+
// ── Sweep report generation ──────────────────────────────────────────────
|
|
1313
|
+
/**
|
|
1314
|
+
* Generate bench/results/sweep-report.md from sweep curves.
|
|
1315
|
+
* Spec 11 R3.4: "A sweep-report.md is generated into bench/results/ with
|
|
1316
|
+
* per-parameter curves, chosen operating points, and the delta between
|
|
1317
|
+
* the previous shipped default and the new one (if any)."
|
|
1318
|
+
*/
|
|
1319
|
+
function generateSweepReport(curves) {
|
|
1320
|
+
const now = new Date().toISOString();
|
|
1321
|
+
const confirmed = curves.filter(c => c.confirmed);
|
|
1322
|
+
const changed = curves.filter(c => !c.confirmed);
|
|
1323
|
+
let md = `# Sweep Report
|
|
1324
|
+
|
|
1325
|
+
**Generated:** ${now}
|
|
1326
|
+
**Parameters swept:** ${curves.length}
|
|
1327
|
+
**Confirmed (no change):** ${confirmed.length}
|
|
1328
|
+
**Changed:** ${changed.length}
|
|
1329
|
+
|
|
1330
|
+
## Summary
|
|
1331
|
+
|
|
1332
|
+
| Parameter | Current | Recommended | Status |
|
|
1333
|
+
|-----------|---------|-------------|--------|
|
|
1334
|
+
`;
|
|
1335
|
+
for (const curve of curves) {
|
|
1336
|
+
const status = curve.confirmed ? '✅ CONFIRMED' : '⚠️ CHANGED';
|
|
1337
|
+
md += `| ${curve.parameter.label} | ${curve.currentDefault} | ${curve.recommended.value} | ${status} |\n`;
|
|
1338
|
+
}
|
|
1339
|
+
if (changed.length > 0) {
|
|
1340
|
+
md += `\n### Changes Required\n\n`;
|
|
1341
|
+
md += `These defaults should be updated in \`src/config/defaults.ts\`:\n\n`;
|
|
1342
|
+
for (const c of changed) {
|
|
1343
|
+
md += `- **${c.parameter.label}**: \`${c.parameter.configKey}: ${c.currentDefault}\` → \`${c.recommended.value}\`\n`;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// Per-parameter curves
|
|
1347
|
+
md += `\n## Per-Parameter Curves\n\n`;
|
|
1348
|
+
for (const curve of curves) {
|
|
1349
|
+
const { parameter, points, recommended, currentDefault } = curve;
|
|
1350
|
+
const status = curve.confirmed ? '✅ CONFIRMED' : '⚠️ CHANGED';
|
|
1351
|
+
md += `### ${parameter.label}\n\n`;
|
|
1352
|
+
md += `- **Analyzer:** \`${parameter.analyzer}\`\n`;
|
|
1353
|
+
md += `- **Config key:** \`${parameter.configKey}\`\n`;
|
|
1354
|
+
md += `- **Selection:** precision-first (highest precision; ties broken by highest recall)\n`;
|
|
1355
|
+
md += `- **Current default:** ${currentDefault}\n`;
|
|
1356
|
+
md += `- **Recommended:** ${recommended.value} ${status}\n\n`;
|
|
1357
|
+
md += `| Value | Precision | Recall | F1 | TP | FP | FN |\n`;
|
|
1358
|
+
md += `|-------|-----------|--------|----|----|----|----|\n`;
|
|
1359
|
+
for (const p of points) {
|
|
1360
|
+
const marker = p.value === recommended.value ? ' **← chosen**' : '';
|
|
1361
|
+
md += `| ${p.value} | ${p.precision.toFixed(4)} | ${p.recall.toFixed(4)} | ${p.f1.toFixed(4)} | ${p.truePositives} | ${p.falsePositives} | ${p.falseNegatives}${marker} |\n`;
|
|
1362
|
+
}
|
|
1363
|
+
md += '\n';
|
|
1364
|
+
}
|
|
1365
|
+
// Caveat about minimal corpus
|
|
1366
|
+
md += `## Notes\n\n`;
|
|
1367
|
+
md += `- The bench corpus is intentionally minimal (1-9 fixtures per analyzer). `;
|
|
1368
|
+
md += `For parameters where the minimal corpus cannot discriminate between threshold values `;
|
|
1369
|
+
md += `(e.g., all values produce identical metrics on a single fixture), the precision-first `;
|
|
1370
|
+
md += `selection picks the most permissive value. Real-corpus triage (Spec 11 R4) validates `;
|
|
1371
|
+
md += `these recommendations against external repositories.\n`;
|
|
1372
|
+
md += `- Style analyzer parameters (Spec 10) have no corpus and were skipped. They will be `;
|
|
1373
|
+
md += `calibrated when the analyzer is implemented.\n`;
|
|
1374
|
+
// Save
|
|
1375
|
+
const reportPath = join(RESULTS_DIR, 'sweep-report.md');
|
|
1376
|
+
writeFileSync(reportPath, md);
|
|
1377
|
+
if (process.env['NODE_ENV'] !== 'test') {
|
|
1378
|
+
console.log(`\n Report saved: bench/results/sweep-report.md`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
// ── Output ───────────────────────────────────────────────────────────────
|
|
1382
|
+
function printAnalyzerResults(name, metrics) {
|
|
1383
|
+
console.log(` TP: ${metrics.truePositives} FP: ${metrics.falsePositives} FN: ${metrics.falseNegatives}`);
|
|
1384
|
+
const recallLine = metrics.knownMisses > 0
|
|
1385
|
+
? ` Precision: ${metrics.precision.toFixed(4)} Recall: ${metrics.recall.toFixed(4)} effective / ${metrics.trueRecall.toFixed(4)} vs ground truth F1: ${metrics.f1.toFixed(4)} effective / ${metrics.trueF1.toFixed(4)} vs ground truth`
|
|
1386
|
+
: ` Precision: ${metrics.precision.toFixed(4)} Recall: ${metrics.recall.toFixed(4)} F1: ${metrics.f1.toFixed(4)}`;
|
|
1387
|
+
console.log(recallLine);
|
|
1388
|
+
if (metrics.knownMisses > 0 || metrics.recoveredMisses > 0) {
|
|
1389
|
+
const parts = [];
|
|
1390
|
+
if (metrics.knownMisses > 0)
|
|
1391
|
+
parts.push(`${metrics.knownMisses} known miss(es)`);
|
|
1392
|
+
if (metrics.recoveredMisses > 0)
|
|
1393
|
+
parts.push(`${metrics.recoveredMisses} recovered`);
|
|
1394
|
+
console.log(` ⚡ ${parts.join(', ')}`);
|
|
1395
|
+
}
|
|
1396
|
+
for (const detail of metrics.knownMissDetails) {
|
|
1397
|
+
const prefix = detail.matched ? ' ✅ Recovered:' : ' 💤 Known miss:';
|
|
1398
|
+
const reason = detail.expected.reason ? ` (${detail.expected.reason})` : '';
|
|
1399
|
+
console.log(`${prefix} ${detail.expected.file} [${detail.expected.rule}]${reason}`);
|
|
1400
|
+
}
|
|
1401
|
+
for (const detail of metrics.details) {
|
|
1402
|
+
if (!detail.matched) {
|
|
1403
|
+
console.log(` ❌ Missed: ${detail.expected.file} [${detail.expected.rule}] ${detail.expected.symbol || '(any)'}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
for (const nm of metrics.nearMissResults) {
|
|
1407
|
+
if (!nm.passed) {
|
|
1408
|
+
console.log(` ⚠️ Near-miss fired: ${nm.file} (${nm.violations} violation(s))`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
for (const w of metrics.warnings) {
|
|
1412
|
+
console.log(` ⚠️ ${w}`);
|
|
1413
|
+
}
|
|
1414
|
+
// Per-rule breakdown
|
|
1415
|
+
if (metrics.ruleMetrics.length > 0) {
|
|
1416
|
+
console.log(` ── Per-Rule ──`);
|
|
1417
|
+
console.log(` ${'Rule'.padEnd(30)} │ Prec │ Recall │ F1 │ TP │ FP │ FN ${metrics.knownMisses > 0 ? '│ KM' : ''} ${metrics.recoveredMisses > 0 ? '│ Rec' : ''}`);
|
|
1418
|
+
for (const rm of metrics.ruleMetrics) {
|
|
1419
|
+
const km = metrics.knownMisses > 0 ? `│ ${String(rm.knownMisses).padEnd(2)}` : '';
|
|
1420
|
+
const rec = metrics.recoveredMisses > 0 ? `│ ${String(rm.recoveredMisses).padEnd(3)}` : '';
|
|
1421
|
+
const flag = rm.f1 < 1.0 ? ' ⚠️' : '';
|
|
1422
|
+
console.log(` ${rm.rule.padEnd(30)} │ ${rm.precision.toFixed(4)} │ ${rm.recall.toFixed(4)} │ ${rm.f1.toFixed(4)} │ ${String(rm.truePositives).padStart(2)} │ ${String(rm.falsePositives).padStart(2)} │ ${String(rm.falseNegatives).padStart(2)} ${km} ${rec}${flag}`);
|
|
1423
|
+
}
|
|
1424
|
+
console.log();
|
|
1425
|
+
}
|
|
1426
|
+
const passed = metrics.falseNegatives === 0 && metrics.falsePositives === 0;
|
|
1427
|
+
const statusIcon = passed ? '✅' : '❌';
|
|
1428
|
+
const statusLabel = passed
|
|
1429
|
+
? (metrics.knownMisses > 0 ? `PASS (${metrics.knownMisses} known miss(es))` : 'PASS')
|
|
1430
|
+
: 'FAIL';
|
|
1431
|
+
console.log(` ${statusIcon} ${statusLabel}\n`);
|
|
1432
|
+
}
|
|
1433
|
+
function printReport(report) {
|
|
1434
|
+
console.log('══════════════════════════════════════════════');
|
|
1435
|
+
console.log(' Summary');
|
|
1436
|
+
console.log('══════════════════════════════════════════════');
|
|
1437
|
+
console.log(` Analyzers: ${report.summary.totalAnalyzers}`);
|
|
1438
|
+
console.log(` Passed: ${report.summary.passed}`);
|
|
1439
|
+
console.log(` Failed: ${report.summary.failed}`);
|
|
1440
|
+
if (report.summary.totalKnownMisses > 0) {
|
|
1441
|
+
console.log(` Known misses: ${report.summary.totalKnownMisses}`);
|
|
1442
|
+
}
|
|
1443
|
+
console.log(` μPrecision: ${report.summary.microAvgPrecision.toFixed(4)}`);
|
|
1444
|
+
console.log(` μRecall: ${report.summary.microAvgRecall.toFixed(4)}`);
|
|
1445
|
+
console.log(` μF1: ${report.summary.microAvgF1.toFixed(4)}`);
|
|
1446
|
+
if (report.summary.totalKnownMisses > 0) {
|
|
1447
|
+
console.log(` μTrueRecall: ${report.summary.microAvgTrueRecall.toFixed(4)}`);
|
|
1448
|
+
console.log(` μTrueF1: ${report.summary.microAvgTrueF1.toFixed(4)}`);
|
|
1449
|
+
}
|
|
1450
|
+
if (report.baselineComparison) {
|
|
1451
|
+
if (report.baselineComparison.regressions.length > 0) {
|
|
1452
|
+
console.log(`\n ⚠️ Regressions:`);
|
|
1453
|
+
for (const r of report.baselineComparison.regressions) {
|
|
1454
|
+
console.log(` ${r}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (report.baselineComparison.improvements.length > 0) {
|
|
1458
|
+
console.log(`\n 📈 Improvements:`);
|
|
1459
|
+
for (const imp of report.baselineComparison.improvements) {
|
|
1460
|
+
console.log(` ${imp}`);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
console.log(`\n Report: bench/results/latest.json\n`);
|
|
1465
|
+
}
|
|
1466
|
+
// ── Public API for programmatic use ──────────────────────────────────────
|
|
1467
|
+
/**
|
|
1468
|
+
* Run the full bench harness and return the report.
|
|
1469
|
+
* Exported so tests can assert bench results programmatically.
|
|
1470
|
+
*/
|
|
1471
|
+
export async function runBench() {
|
|
1472
|
+
initializeLanguages();
|
|
1473
|
+
await initParsers();
|
|
1474
|
+
const analyzers = buildAnalyzers();
|
|
1475
|
+
// Discover corpus directories
|
|
1476
|
+
const corpusDirs = [];
|
|
1477
|
+
const entries = await readdir(CORPUS_ROOT, { withFileTypes: true });
|
|
1478
|
+
for (const entry of entries) {
|
|
1479
|
+
if (entry.isDirectory()) {
|
|
1480
|
+
corpusDirs.push(join(CORPUS_ROOT, entry.name));
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (corpusDirs.length === 0) {
|
|
1484
|
+
throw new Error(`No corpus directories found in ${CORPUS_ROOT}`);
|
|
1485
|
+
}
|
|
1486
|
+
// Ensure results directory
|
|
1487
|
+
if (!existsSync(RESULTS_DIR)) {
|
|
1488
|
+
mkdirSync(RESULTS_DIR, { recursive: true });
|
|
1489
|
+
}
|
|
1490
|
+
const report = {
|
|
1491
|
+
timestamp: new Date().toISOString(),
|
|
1492
|
+
summary: { totalAnalyzers: 0, passed: 0, failed: 0, totalKnownMisses: 0, microAvgPrecision: 0, microAvgRecall: 0, microAvgF1: 0, microAvgTrueRecall: 0, microAvgTrueF1: 0 },
|
|
1493
|
+
analyzers: {},
|
|
1494
|
+
};
|
|
1495
|
+
for (const corpusDir of corpusDirs.sort()) {
|
|
1496
|
+
await runSingleCorpus(corpusDir, analyzers, report, false);
|
|
1497
|
+
}
|
|
1498
|
+
// Compute micro-averages
|
|
1499
|
+
let totalTP = 0, totalFP = 0, totalFN = 0, totalTrueFN = 0;
|
|
1500
|
+
for (const metrics of Object.values(report.analyzers)) {
|
|
1501
|
+
totalTP += metrics.truePositives;
|
|
1502
|
+
totalFP += metrics.falsePositives;
|
|
1503
|
+
totalFN += metrics.falseNegatives;
|
|
1504
|
+
totalTrueFN += metrics.falseNegatives + metrics.knownMisses;
|
|
1505
|
+
}
|
|
1506
|
+
report.summary.totalAnalyzers = Object.keys(report.analyzers).length;
|
|
1507
|
+
report.summary.microAvgPrecision = Math.round((totalTP / (totalTP + totalFP || 1)) * 10000) / 10000;
|
|
1508
|
+
report.summary.microAvgRecall = Math.round((totalTP / (totalTP + totalFN || 1)) * 10000) / 10000;
|
|
1509
|
+
report.summary.microAvgF1 = Math.round((2 * report.summary.microAvgPrecision * report.summary.microAvgRecall /
|
|
1510
|
+
(report.summary.microAvgPrecision + report.summary.microAvgRecall || 1)) * 10000) / 10000;
|
|
1511
|
+
report.summary.microAvgTrueRecall = Math.round((totalTP / (totalTP + totalTrueFN || 1)) * 10000) / 10000;
|
|
1512
|
+
report.summary.microAvgTrueF1 = Math.round((2 * report.summary.microAvgPrecision * report.summary.microAvgTrueRecall /
|
|
1513
|
+
(report.summary.microAvgPrecision + report.summary.microAvgTrueRecall || 1)) * 10000) / 10000;
|
|
1514
|
+
// Compare against baseline
|
|
1515
|
+
const baseline = loadBaseline();
|
|
1516
|
+
if (baseline) {
|
|
1517
|
+
report.baselineComparison = { regressions: [], improvements: [] };
|
|
1518
|
+
for (const [name, metrics] of Object.entries(report.analyzers)) {
|
|
1519
|
+
const bl = baseline.analyzers[name];
|
|
1520
|
+
if (bl && metrics.f1 < bl.f1) {
|
|
1521
|
+
report.baselineComparison.regressions.push(`${name}: F1 ${bl.f1} → ${metrics.f1} (Δ${Math.round((metrics.f1 - bl.f1) * 10000) / 100})`);
|
|
1522
|
+
}
|
|
1523
|
+
else if (bl && metrics.f1 > bl.f1) {
|
|
1524
|
+
report.baselineComparison.improvements.push(`${name}: F1 ${bl.f1} → ${metrics.f1} (+${Math.round((metrics.f1 - bl.f1) * 10000) / 100})`);
|
|
1525
|
+
}
|
|
1526
|
+
// Per-rule baseline comparison (schemaVersion >= 2)
|
|
1527
|
+
if (bl?.rules && metrics.ruleMetrics.length > 0) {
|
|
1528
|
+
for (const rm of metrics.ruleMetrics) {
|
|
1529
|
+
const ruleBl = bl.rules[rm.rule];
|
|
1530
|
+
if (!ruleBl)
|
|
1531
|
+
continue; // new rule — no baseline
|
|
1532
|
+
if (rm.f1 < ruleBl.f1) {
|
|
1533
|
+
report.baselineComparison.regressions.push(` ${name}/${rm.rule}: F1 ${ruleBl.f1} → ${rm.f1} (Δ${Math.round((rm.f1 - ruleBl.f1) * 10000) / 100})`);
|
|
1534
|
+
}
|
|
1535
|
+
else if (rm.f1 > ruleBl.f1) {
|
|
1536
|
+
report.baselineComparison.improvements.push(` ${name}/${rm.rule}: F1 ${ruleBl.f1} → ${rm.f1} (+${Math.round((rm.f1 - ruleBl.f1) * 10000) / 100})`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
for (const [name, metrics] of Object.entries(report.analyzers)) {
|
|
1543
|
+
const allMatched = metrics.falseNegatives === 0 && metrics.falsePositives === 0;
|
|
1544
|
+
report.summary.passed += allMatched ? 1 : 0;
|
|
1545
|
+
report.summary.failed += allMatched ? 0 : 1;
|
|
1546
|
+
report.summary.totalKnownMisses += metrics.knownMisses;
|
|
1547
|
+
}
|
|
1548
|
+
// Save machine-readable report
|
|
1549
|
+
writeFileSync(join(RESULTS_DIR, 'latest.json'), JSON.stringify(report, null, 2) + '\n');
|
|
1550
|
+
return report;
|
|
1551
|
+
}
|
|
1552
|
+
// ── CLI entry point ─────────────────────────────────────────────────────
|
|
1553
|
+
if (process.argv[1] && (process.argv[1].endsWith('runBench.ts') || process.argv[1].endsWith('runBench.js'))) {
|
|
1554
|
+
main().catch(err => {
|
|
1555
|
+
console.error('Bench harness error:', err);
|
|
1556
|
+
process.exit(2);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
//# sourceMappingURL=runBench.js.map
|