code-auditor-mcp 5.0.1 → 5.2.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 +227 -3
- package/README.md +24 -2
- package/configs/hhra-compat.json +12 -118
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +11 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +57 -52
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/handleIdentification.d.ts.map +1 -1
- package/dist/analyzers/handleIdentification.js +4 -1
- package/dist/analyzers/handleIdentification.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +9 -4
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +8 -25
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +0 -5
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/receiverRoot.d.ts +20 -0
- package/dist/analyzers/receiverRoot.d.ts.map +1 -1
- package/dist/analyzers/receiverRoot.js +42 -0
- package/dist/analyzers/receiverRoot.js.map +1 -1
- package/dist/analyzers/ruleRegistry.js +4 -4
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/ruleTiming.d.ts +1 -1
- package/dist/analyzers/ruleTiming.js +2 -2
- package/dist/analyzers/ruleTiming.js.map +1 -1
- package/dist/analyzers/tsEcosystem.d.ts.map +1 -1
- package/dist/analyzers/tsEcosystem.js +10 -0
- package/dist/analyzers/tsEcosystem.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +22 -26
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +32 -15
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +189 -115
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +0 -2
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +0 -5
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -10
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +7 -49
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/config.js +0 -2
- package/dist/analyzers/universal/schema/config.js.map +1 -1
- package/dist/analyzers/universal/schema/types.d.ts +0 -2
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -1
- package/dist/analyzers/vendorSurfaces.d.ts +126 -0
- package/dist/analyzers/vendorSurfaces.d.ts.map +1 -0
- package/dist/analyzers/vendorSurfaces.js +580 -0
- package/dist/analyzers/vendorSurfaces.js.map +1 -0
- package/dist/auditRunner.d.ts +2 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +159 -160
- package/dist/auditRunner.js.map +1 -1
- package/dist/churn/churnExtractor.d.ts +0 -1
- package/dist/churn/churnExtractor.d.ts.map +1 -1
- package/dist/churn/churnExtractor.js +2 -4
- package/dist/churn/churnExtractor.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +212 -29
- package/dist/cli.js.map +1 -1
- package/dist/codeIndex/migrations.d.ts.map +1 -1
- package/dist/codeIndex/migrations.js +4 -3
- package/dist/codeIndex/migrations.js.map +1 -1
- package/dist/codeIndexDB.d.ts +11 -2
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +27 -2
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/configLoader.d.ts +60 -7
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +252 -60
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +77 -18
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +105 -43
- package/dist/config/defaults.js.map +1 -1
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/daemon/main.d.ts +0 -1
- package/dist/daemon/main.d.ts.map +1 -1
- package/dist/daemon/main.js +31 -8
- package/dist/daemon/main.js.map +1 -1
- package/dist/dataPaths.d.ts +17 -11
- package/dist/dataPaths.d.ts.map +1 -1
- package/dist/dataPaths.js +51 -16
- package/dist/dataPaths.js.map +1 -1
- package/dist/graph/resolutionEnvironment.d.ts +61 -0
- package/dist/graph/resolutionEnvironment.d.ts.map +1 -0
- package/dist/graph/resolutionEnvironment.js +57 -0
- package/dist/graph/resolutionEnvironment.js.map +1 -0
- package/dist/hookLog.d.ts +54 -0
- package/dist/hookLog.d.ts.map +1 -0
- package/dist/hookLog.js +105 -0
- package/dist/hookLog.js.map +1 -0
- package/dist/hookLogPath.generated.d.ts +3 -0
- package/dist/hookLogPath.generated.d.ts.map +1 -0
- package/dist/hookLogPath.generated.js +8 -0
- package/dist/hookLogPath.generated.js.map +1 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -7
- package/dist/index.js.map +1 -1
- package/dist/installConfig.d.ts +15 -2
- package/dist/installConfig.d.ts.map +1 -1
- package/dist/installConfig.js +17 -2
- package/dist/installConfig.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +18 -3
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +8 -8
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +5 -2
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp-ui-simple.d.ts +5 -0
- package/dist/mcp-ui-simple.d.ts.map +1 -1
- package/dist/mcp-ui-simple.js +9 -2
- package/dist/mcp-ui-simple.js.map +1 -1
- package/dist/mcp.d.ts +0 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +36 -33
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +10 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +10 -1
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/mcpAutoIndex.d.ts.map +1 -1
- package/dist/mcpAutoIndex.js +2 -1
- package/dist/mcpAutoIndex.js.map +1 -1
- package/dist/phase/colorMath.d.ts +18 -5
- package/dist/phase/colorMath.d.ts.map +1 -1
- package/dist/phase/colorMath.js +88 -5
- package/dist/phase/colorMath.js.map +1 -1
- package/dist/phase/dataAccessCallsCandidates.js +1 -1
- package/dist/phase/dataAccessCallsCandidates.js.map +1 -1
- package/dist/phase/goFunctionAnalysis.d.ts +2 -2
- package/dist/phase/goFunctionAnalysis.js +2 -2
- package/dist/phase/goFunctions.d.ts +2 -2
- package/dist/phase/goFunctions.js +2 -2
- package/dist/phase/goImports.d.ts +2 -2
- package/dist/phase/goImports.js +2 -2
- package/dist/phase/goSwitches.d.ts +2 -2
- package/dist/phase/goSwitches.js +2 -2
- package/dist/phase/loopQueryCandidates.js +1 -1
- package/dist/phase/loopQueryCandidates.js.map +1 -1
- package/dist/phase/phaseModel.d.ts +37 -8
- package/dist/phase/phaseModel.d.ts.map +1 -1
- package/dist/phase/phaseModel.js +94 -46
- package/dist/phase/phaseModel.js.map +1 -1
- package/dist/phase/producers.d.ts.map +1 -1
- package/dist/phase/producers.js +27 -17
- package/dist/phase/producers.js.map +1 -1
- package/dist/phase/reachability.d.ts +44 -1
- package/dist/phase/reachability.d.ts.map +1 -1
- package/dist/phase/reachability.js +79 -0
- package/dist/phase/reachability.js.map +1 -1
- package/dist/phase/receiverConsumers.d.ts +8 -1
- package/dist/phase/receiverConsumers.d.ts.map +1 -1
- package/dist/phase/receiverConsumers.js +59 -21
- package/dist/phase/receiverConsumers.js.map +1 -1
- package/dist/phase/receiverProvenance.d.ts +23 -0
- package/dist/phase/receiverProvenance.d.ts.map +1 -1
- package/dist/phase/receiverProvenance.js +40 -0
- package/dist/phase/receiverProvenance.js.map +1 -1
- package/dist/phase/rules/crossDomain.d.ts.map +1 -1
- package/dist/phase/rules/crossDomain.js +16 -6
- package/dist/phase/rules/crossDomain.js.map +1 -1
- package/dist/phase/rules/dry.d.ts.map +1 -1
- package/dist/phase/rules/dry.js +0 -1
- package/dist/phase/rules/dry.js.map +1 -1
- package/dist/phase/rules/goRules.d.ts +2 -2
- package/dist/phase/rules/goRules.js +2 -2
- package/dist/phase/rules/secrets.d.ts +22 -8
- package/dist/phase/rules/secrets.d.ts.map +1 -1
- package/dist/phase/rules/secrets.js +48 -67
- package/dist/phase/rules/secrets.js.map +1 -1
- package/dist/phase/rules/styles.d.ts +23 -9
- package/dist/phase/rules/styles.d.ts.map +1 -1
- package/dist/phase/rules/styles.js +47 -15
- package/dist/phase/rules/styles.js.map +1 -1
- package/dist/phase/rules/unreferencedModule.d.ts.map +1 -1
- package/dist/phase/rules/unreferencedModule.js +16 -5
- package/dist/phase/rules/unreferencedModule.js.map +1 -1
- package/dist/phase/runner.d.ts +16 -7
- package/dist/phase/runner.d.ts.map +1 -1
- package/dist/phase/runner.js +32 -20
- package/dist/phase/runner.js.map +1 -1
- package/dist/phase/stylesCss.d.ts.map +1 -1
- package/dist/phase/stylesCss.js +4 -0
- package/dist/phase/stylesCss.js.map +1 -1
- package/dist/phase/stylesMarkup.d.ts.map +1 -1
- package/dist/phase/stylesMarkup.js +4 -0
- package/dist/phase/stylesMarkup.js.map +1 -1
- package/dist/phase/stylesSource.d.ts.map +1 -1
- package/dist/phase/stylesSource.js +6 -0
- package/dist/phase/stylesSource.js.map +1 -1
- package/dist/phase/testProgram.d.ts +57 -0
- package/dist/phase/testProgram.d.ts.map +1 -0
- package/dist/phase/testProgram.js +244 -0
- package/dist/phase/testProgram.js.map +1 -0
- package/dist/phase/typeDeclarations.d.ts +2 -2
- package/dist/phase/typeDeclarations.js +2 -2
- package/dist/phase/types.d.ts +54 -0
- package/dist/phase/types.d.ts.map +1 -1
- package/dist/pipelineAdapters.d.ts +1 -171
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +47 -1281
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +0 -6
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
- package/dist/reporting/htmlReportGenerator.js +0 -4
- package/dist/reporting/htmlReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +0 -1
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +0 -7
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/sqlite/driver.d.ts +17 -1
- package/dist/sqlite/driver.d.ts.map +1 -1
- package/dist/sqlite/driver.js +28 -5
- package/dist/sqlite/driver.js.map +1 -1
- package/dist/styles/cssInJsHarvest.d.ts +36 -0
- package/dist/styles/cssInJsHarvest.d.ts.map +1 -0
- package/dist/styles/cssInJsHarvest.js +78 -0
- package/dist/styles/cssInJsHarvest.js.map +1 -0
- package/dist/styles/definedClasses.d.ts +95 -0
- package/dist/styles/definedClasses.d.ts.map +1 -0
- package/dist/styles/definedClasses.js +129 -0
- package/dist/styles/definedClasses.js.map +1 -0
- package/dist/styles/markupStyleHarvest.d.ts +33 -0
- package/dist/styles/markupStyleHarvest.d.ts.map +1 -0
- package/dist/styles/markupStyleHarvest.js +73 -0
- package/dist/styles/markupStyleHarvest.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +37 -0
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +89 -33
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +11 -10
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindExpander.d.ts.map +1 -1
- package/dist/styles/tailwindExpander.js +31 -10
- package/dist/styles/tailwindExpander.js.map +1 -1
- package/dist/telemetry.d.ts +67 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +100 -20
- package/dist/telemetry.js.map +1 -1
- package/dist/types/crossLanguage.d.ts +1 -0
- package/dist/types/crossLanguage.d.ts.map +1 -1
- package/dist/types.d.ts +36 -56
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/examples/.auditrc.example.json +7 -22
- package/examples/.auditrc.json +12 -155
- package/examples/.code-auditor.json +8 -51
- package/examples/nextjs.auditrc.json +7 -34
- package/examples/node-api.auditrc.json +6 -46
- package/examples/react.auditrc.json +8 -25
- package/examples/tolerant-solid.auditrc.json +12 -44
- package/package.json +13 -9
- package/plugin/.claude-plugin/manifest.json +10 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +4 -4
- package/plugin/scripts/hook-audit.sh +14 -2
- package/plugin/scripts/hook-common.sh +41 -11
- package/plugin/scripts/hook-drift-check.sh +19 -6
- package/plugin/scripts/hook-log-path.sh +7 -0
- package/plugin/scripts/hook-self-audit.sh +14 -2
- package/plugin/scripts/hook-warm.sh +22 -17
- package/plugin/skills/code-auditor/SKILL.md +26 -3
- package/server.json +10 -4
- package/dist/__tests__/fixtures/spec-20/scripts/tool.d.ts +0 -6
- package/dist/__tests__/fixtures/spec-20/scripts/tool.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-20/scripts/tool.js +0 -12
- package/dist/__tests__/fixtures/spec-20/scripts/tool.js.map +0 -1
- package/dist/__tests__/fixtures/spec-20/src/demo-module.d.ts +0 -7
- package/dist/__tests__/fixtures/spec-20/src/demo-module.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-20/src/demo-module.js +0 -13
- package/dist/__tests__/fixtures/spec-20/src/demo-module.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-01-prepare-in-loop.d.ts +0 -14
- package/dist/__tests__/fixtures/spec-52/item-01-prepare-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-01-prepare-in-loop.js +0 -16
- package/dist/__tests__/fixtures/spec-52/item-01-prepare-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-02-prepare-bind-accumulate-batch.d.ts +0 -15
- package/dist/__tests__/fixtures/spec-52/item-02-prepare-bind-accumulate-batch.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-02-prepare-bind-accumulate-batch.js +0 -17
- package/dist/__tests__/fixtures/spec-52/item-02-prepare-bind-accumulate-batch.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.d.ts +0 -14
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.js +0 -19
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts +0 -18
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.js +0 -19
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts +0 -19
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.js +0 -19
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.d.ts +0 -17
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.js +0 -19
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.d.ts +0 -25
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.js +0 -21
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-08-promise-all-of-statements-in-loop.d.ts +0 -15
- package/dist/__tests__/fixtures/spec-52/item-08-promise-all-of-statements-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-08-promise-all-of-statements-in-loop.js +0 -19
- package/dist/__tests__/fixtures/spec-52/item-08-promise-all-of-statements-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-09-promise-all-map-of-statements-in-loop.d.ts +0 -14
- package/dist/__tests__/fixtures/spec-52/item-09-promise-all-map-of-statements-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-09-promise-all-map-of-statements-in-loop.js +0 -14
- package/dist/__tests__/fixtures/spec-52/item-09-promise-all-map-of-statements-in-loop.js.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.d.ts +0 -15
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.d.ts.map +0 -1
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.js +0 -16
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.js.map +0 -1
- package/dist/languages/go/analyzer-darwin-amd64 +0 -0
- package/dist/languages/go/analyzer-darwin-arm64 +0 -0
- package/dist/languages/go/analyzer-linux-amd64 +0 -0
- package/dist/languages/go/analyzer-linux-arm64 +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +0 -643
- package/dist/languages/go/analyzer-src/dataaccess.go +0 -388
- package/dist/languages/go/analyzer-src/go.mod +0 -3
- package/dist/languages/go/analyzer-src/indexer.go +0 -268
- package/dist/languages/go/analyzer-src/parser.go +0 -461
- package/dist/languages/go/analyzer-src/solid.go +0 -311
- package/dist/languages/go/analyzer-src/testconventions.go +0 -25
- package/dist/languages/go/analyzer-src/types.go +0 -156
- package/dist/languages/go/analyzer-windows-amd64.exe +0 -0
- package/dist/languages/go/go.mod +0 -7
- package/dist/languages/go/main.go +0 -203
- package/dist/native/ast-grep-napi.darwin-arm64.node +0 -0
- package/dist/native/ast-grep-napi.darwin-x64.node +0 -0
- package/dist/native/ast-grep-napi.linux-arm64-gnu.node +0 -0
- package/dist/native/ast-grep-napi.linux-arm64-musl.node +0 -0
- package/dist/native/ast-grep-napi.linux-x64-gnu.node +0 -0
- package/dist/native/ast-grep-napi.linux-x64-musl.node +0 -0
- package/dist/native/ast-grep-napi.win32-x64-msvc.node +0 -0
- package/dist/native-bootstrap.d.ts +0 -15
- package/dist/native-bootstrap.d.ts.map +0 -1
- package/dist/native-bootstrap.js +0 -75
- package/dist/native-bootstrap.js.map +0 -1
package/dist/pipelineAdapters.js
CHANGED
|
@@ -26,7 +26,7 @@ import { findTableReferences, checkNamingConventions, checkQueryPatterns, checkS
|
|
|
26
26
|
import { passesFileGate, extractTablesFromRegistry, } from './analyzers/universal/schema/discovery.js';
|
|
27
27
|
import { buildBindingEnv } from './analyzers/receiverRoot.js';
|
|
28
28
|
import { applyMigrationOps, stripIdentifier } from './analyzers/universal/schema/migrations.js';
|
|
29
|
-
import {
|
|
29
|
+
import { harvestDefinedClasses } from './styles/definedClasses.js';
|
|
30
30
|
// ── Rule ID helpers ──────────────────────────────────────────────────────────
|
|
31
31
|
function getRuleIdsFor(name) {
|
|
32
32
|
return MIGRATED_RULES.filter((r) => r.analyzer === name).map((r) => r.id);
|
|
@@ -48,181 +48,6 @@ function lazySingleton(loader) {
|
|
|
48
48
|
return promise;
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
function createAnalyzerAstVisitor(spec) {
|
|
52
|
-
return {
|
|
53
|
-
name: spec.name,
|
|
54
|
-
stage: 'visitor',
|
|
55
|
-
getRuleIds: () => getRuleIdsFor(spec.name),
|
|
56
|
-
async visit(ast, adapter, context, sourceCode) {
|
|
57
|
-
const a = await spec.getAnalyzer();
|
|
58
|
-
const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
|
|
59
|
-
return { violations, facts: {} };
|
|
60
|
-
},
|
|
61
|
-
defaultConfig: {},
|
|
62
|
-
description: spec.description,
|
|
63
|
-
category: spec.category,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/** Read the analyzer's accumulated samples/pairs, swallowing a load failure. */
|
|
67
|
-
async function readAccumulated(getAnalyzer, pick) {
|
|
68
|
-
try {
|
|
69
|
-
const a = await getAnalyzer();
|
|
70
|
-
return pick(a) ?? [];
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/** A bundle visitor: an analyzeAST visitor plus a getter for the analyzer's
|
|
77
|
-
* accumulated samples/pairs. The bundle creators (SOLID, DRY) share one lazy
|
|
78
|
-
* analyzer instance between the visitor and the getter. */
|
|
79
|
-
function createAnalyzerBundle(spec) {
|
|
80
|
-
const getAnalyzer = lazySingleton(spec.loader);
|
|
81
|
-
return {
|
|
82
|
-
visitor: createAnalyzerAstVisitor({
|
|
83
|
-
name: spec.name,
|
|
84
|
-
getAnalyzer,
|
|
85
|
-
description: spec.description,
|
|
86
|
-
category: spec.category,
|
|
87
|
-
}),
|
|
88
|
-
getAccumulated: () => readAccumulated(getAnalyzer, spec.pick),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Create the SOLID stage-2 visitor, which lazily loads the SOLID analyzer and
|
|
93
|
-
* also exposes its accumulated size samples.
|
|
94
|
-
*
|
|
95
|
-
* @returns The SOLID visitor bundle.
|
|
96
|
-
*/
|
|
97
|
-
export function createSolidVisitor() {
|
|
98
|
-
const { visitor, getAccumulated } = createAnalyzerBundle({
|
|
99
|
-
name: 'solid',
|
|
100
|
-
loader: () => import('./analyzers/universal/UniversalSOLIDAnalyzer.js').then((m) => new m.UniversalSOLIDAnalyzer()),
|
|
101
|
-
description: 'Detects violations of SOLID principles',
|
|
102
|
-
category: 'architecture',
|
|
103
|
-
pick: (a) => a.sizeSamples,
|
|
104
|
-
});
|
|
105
|
-
return { visitor, getSizeSamples: getAccumulated };
|
|
106
|
-
}
|
|
107
|
-
// ── Data-Access org-filter reducer (Spec 62 Amendment B) ─────────────────────
|
|
108
|
-
/**
|
|
109
|
-
* Stage-4 derived reducer for `missing-org-filter`. It joins the data-access
|
|
110
|
-
* visitor's per-query facts (resolved tables + org-filter presence + query-site
|
|
111
|
-
* location) against the tenant tiers resolved from config + the schema
|
|
112
|
-
* reducer's corpus-wide DDL column catalog. Moving the rule here (out of the
|
|
113
|
-
* Stage-2 analyzer) is the fix for the Amendment B defect: the firing predicate
|
|
114
|
-
* needed the DDL tier only Stage 3 produces, but the Stage-2 analyzer could only
|
|
115
|
-
* read config tiers — so a DDL-declared multi-tenant codebase was un-suppressed
|
|
116
|
-
* without ever firing, and reported `clean` on a real leak.
|
|
117
|
-
*
|
|
118
|
-
* Firing and applicability both derive from {@link buildOrgFilterTierSet}, so
|
|
119
|
-
* they read the identical tier set (Tier 1 config / Tier 2 schema / Tier 3 DDL).
|
|
120
|
-
*
|
|
121
|
-
* @returns The data-access org-filter stage-4 reducer.
|
|
122
|
-
*/
|
|
123
|
-
export function createOrgFilterReducer() {
|
|
124
|
-
return {
|
|
125
|
-
name: 'data-access-org-filter',
|
|
126
|
-
stage: 'derivedReducer',
|
|
127
|
-
// Hard dependency on the data-access visitor's per-query facts. The schema
|
|
128
|
-
// table catalog (`allFacts['schema'].tableColumns`) is read opportunistically
|
|
129
|
-
// so a run without `schema` facts degrades to Tier 1–2 (config-only) instead
|
|
130
|
-
// of throwing — see buildOrgFilterTierSet.
|
|
131
|
-
getRuleIds: () => getRuleIdsFor('data-access-org-filter'),
|
|
132
|
-
async reduce(allFacts, context) {
|
|
133
|
-
const violations = [];
|
|
134
|
-
const schemaFacts = allFacts['schema'];
|
|
135
|
-
const ddlTableColumns = schemaFacts?.tableColumns;
|
|
136
|
-
// One tier source of truth — the same function the applicability predicate
|
|
137
|
-
// reads, so firing and applicability cannot drift to different tier sets.
|
|
138
|
-
const tierSet = buildOrgFilterTierSet(context.config, ddlTableColumns);
|
|
139
|
-
const daFacts = (allFacts['data-access'] ?? {});
|
|
140
|
-
for (const fileFacts of Object.values(daFacts)) {
|
|
141
|
-
for (const call of fileFacts.calls ?? []) {
|
|
142
|
-
if (call.hasOrganizationFilter)
|
|
143
|
-
continue;
|
|
144
|
-
if (!call.tables || call.tables.length === 0)
|
|
145
|
-
continue;
|
|
146
|
-
if (!tableRequiresOrgFilter(call.tables, tierSet))
|
|
147
|
-
continue;
|
|
148
|
-
const symbol = `${call.enclosingFunction ?? 'top-level'}:${call.method}`;
|
|
149
|
-
violations.push({
|
|
150
|
-
file: call.file,
|
|
151
|
-
line: call.line,
|
|
152
|
-
column: call.column,
|
|
153
|
-
severity: 'critical',
|
|
154
|
-
message: `Query on ${call.tables.join(', ')} has no organization/tenant predicate`,
|
|
155
|
-
suggestion: 'Add the tenant column (organization_id / org_id) to the WHERE predicate so this query is scoped to the current organization, not just by primary key.',
|
|
156
|
-
// Structured next action (Spec 37 R1) — the rule is `resolvable`, so
|
|
157
|
-
// the gate requires a `resolution`, not just the `suggestion` string.
|
|
158
|
-
resolution: {
|
|
159
|
-
action: 'add-tenant-predicate',
|
|
160
|
-
summary: `Add the tenant column (organization_id / org_id) to the WHERE predicate on ${call.tables.join(', ')} so this query is scoped to the current organization, not just by primary key.`,
|
|
161
|
-
symbols: call.tables,
|
|
162
|
-
},
|
|
163
|
-
rule: 'missing-org-filter',
|
|
164
|
-
analyzer: 'data-access',
|
|
165
|
-
symbol,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return { violations, facts: {} };
|
|
170
|
-
},
|
|
171
|
-
defaultConfig: {},
|
|
172
|
-
description: 'Detects queries on tenant-scoped tables that omit an organization/tenant predicate',
|
|
173
|
-
category: 'security',
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
// ── Secrets visitor ──────────────────────────────────────────────────────────
|
|
177
|
-
/**
|
|
178
|
-
* Create the secrets stage-2 visitor, which detects hardcoded credentials,
|
|
179
|
-
* API keys, and tokens.
|
|
180
|
-
*
|
|
181
|
-
* @returns The secrets stage-2 visitor.
|
|
182
|
-
*/
|
|
183
|
-
export function createSecretsVisitor() {
|
|
184
|
-
return createAnalyzerAstVisitor({
|
|
185
|
-
name: 'secrets',
|
|
186
|
-
getAnalyzer: lazySingleton(() => import('./analyzers/universal/UniversalSecretsAnalyzer.js').then((m) => new m.UniversalSecretsAnalyzer())),
|
|
187
|
-
description: 'Detects hardcoded credentials, API keys, and tokens',
|
|
188
|
-
category: 'security',
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
// ── Security visitor (Spec 61 R6) ────────────────────────────────────────────
|
|
192
|
-
/**
|
|
193
|
-
* Create the security stage-2 visitor, which detects command injection,
|
|
194
|
-
* dynamic require of project paths, and unescaped HTML interpolation.
|
|
195
|
-
*
|
|
196
|
-
* @returns The security stage-2 visitor.
|
|
197
|
-
*/
|
|
198
|
-
export function createSecurityVisitor() {
|
|
199
|
-
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSecurityAnalyzer.js').then((m) => new m.UniversalSecurityAnalyzer()));
|
|
200
|
-
return {
|
|
201
|
-
name: 'security',
|
|
202
|
-
stage: 'visitor',
|
|
203
|
-
getRuleIds: () => getRuleIdsFor('security'),
|
|
204
|
-
async visit(ast, adapter, context, sourceCode) {
|
|
205
|
-
const a = await getAnalyzer();
|
|
206
|
-
const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
|
|
207
|
-
// Emit a fact per trigger construct the file contained. These fold
|
|
208
|
-
// corpus-wide (only keys seen in ≥1 file survive the spread-merge) and
|
|
209
|
-
// feed securityInputApplicability, so a rule whose construct is absent
|
|
210
|
-
// reads `notApplicable` instead of `clean`.
|
|
211
|
-
const saw = a.inputPresence;
|
|
212
|
-
const facts = {};
|
|
213
|
-
if (saw.shellProcess)
|
|
214
|
-
facts.shellProcessSeen = true;
|
|
215
|
-
if (saw.dynamicRequire)
|
|
216
|
-
facts.dynamicRequireSeen = true;
|
|
217
|
-
if (saw.htmlSink)
|
|
218
|
-
facts.htmlSinkSeen = true;
|
|
219
|
-
return { violations, facts };
|
|
220
|
-
},
|
|
221
|
-
defaultConfig: {},
|
|
222
|
-
description: 'Detects command injection, dynamic require of project paths, and unescaped HTML interpolation',
|
|
223
|
-
category: 'security',
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
51
|
/**
|
|
227
52
|
* Collect function/method/component entries from an AST in three passes:
|
|
228
53
|
* (1) named function declarations and methods, (2) arrow functions assigned
|
|
@@ -576,87 +401,6 @@ export function createFunctionIndexVisitor() {
|
|
|
576
401
|
category: 'infrastructure',
|
|
577
402
|
};
|
|
578
403
|
}
|
|
579
|
-
/**
|
|
580
|
-
* Create the React stage-2 visitor, which scans React components and exposes
|
|
581
|
-
* cross-component checks to run after stage 2 completes.
|
|
582
|
-
*
|
|
583
|
-
* @returns The React visitor bundle.
|
|
584
|
-
*/
|
|
585
|
-
export function createReactVisitor() {
|
|
586
|
-
const scanResults = [];
|
|
587
|
-
const visitor = {
|
|
588
|
-
name: 'react',
|
|
589
|
-
stage: 'visitor',
|
|
590
|
-
getRuleIds: () => getRuleIdsFor('react'),
|
|
591
|
-
async visit(_ast, _adapter, context, _sourceCode) {
|
|
592
|
-
const filePath = context.filePath;
|
|
593
|
-
if (!filePath)
|
|
594
|
-
return { violations: [], facts: {} };
|
|
595
|
-
try {
|
|
596
|
-
const { scanFile } = await import('./componentScanner.js');
|
|
597
|
-
const { analyzeComponent: analyzeComp, DEFAULT_REACT_CONFIG } = await import('./analyzers/reactAnalyzer.js');
|
|
598
|
-
// Merge with defaults so partial configs get all required fields
|
|
599
|
-
const cfg = { ...DEFAULT_REACT_CONFIG, ...context.config };
|
|
600
|
-
const sr = await scanFile(filePath, {
|
|
601
|
-
includeTests: false,
|
|
602
|
-
includeStories: false,
|
|
603
|
-
extractProps: true,
|
|
604
|
-
extractHooks: cfg.checkHooksRules,
|
|
605
|
-
extractImports: true,
|
|
606
|
-
detectComplexity: true,
|
|
607
|
-
});
|
|
608
|
-
scanResults.push(sr);
|
|
609
|
-
if (sr.parseErrors && sr.parseErrors.length > 0) {
|
|
610
|
-
return { violations: [], facts: {} };
|
|
611
|
-
}
|
|
612
|
-
// Per-component checks — same logic as reactAnalyzer.analyze()
|
|
613
|
-
const violations = [];
|
|
614
|
-
for (const comp of sr.components ?? []) {
|
|
615
|
-
violations.push(...analyzeComp(comp, cfg, sr));
|
|
616
|
-
}
|
|
617
|
-
return { violations, facts: {} };
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
return { violations: [], facts: {} };
|
|
621
|
-
}
|
|
622
|
-
},
|
|
623
|
-
defaultConfig: {},
|
|
624
|
-
description: 'Analyzes React components for best practices and performance',
|
|
625
|
-
category: 'frontend',
|
|
626
|
-
};
|
|
627
|
-
return {
|
|
628
|
-
visitor,
|
|
629
|
-
finalizeCrossComponent: async (config) => {
|
|
630
|
-
if (scanResults.length === 0)
|
|
631
|
-
return [];
|
|
632
|
-
const violations = [];
|
|
633
|
-
try {
|
|
634
|
-
const { checkCircularDependencies, checkErrorBoundaryUsage, checkRawElements, DEFAULT_REACT_CONFIG, } = await import('./analyzers/reactAnalyzer.js');
|
|
635
|
-
// Merge with defaults — mirrors the visitor's
|
|
636
|
-
// `{ ...DEFAULT_REACT_CONFIG, ...context.config }` so a partial config
|
|
637
|
-
// still yields every required field, and so the `react.*` flags set in
|
|
638
|
-
// .codeauditor.json reach the cross-component checks.
|
|
639
|
-
const cfg = { ...DEFAULT_REACT_CONFIG, ...(config ?? {}) };
|
|
640
|
-
// Build component tree and check for circular dependencies
|
|
641
|
-
const { buildComponentTree } = await import('./componentScanner.js');
|
|
642
|
-
const tree = buildComponentTree(scanResults);
|
|
643
|
-
violations.push(...checkCircularDependencies(tree));
|
|
644
|
-
// Check for missing error boundaries
|
|
645
|
-
if (cfg.requireErrorBoundaries !== false) {
|
|
646
|
-
violations.push(...checkErrorBoundaryUsage(scanResults));
|
|
647
|
-
}
|
|
648
|
-
// Check for raw element usage (Spec 10 R4)
|
|
649
|
-
if (cfg.rawElementCheck !== false) {
|
|
650
|
-
violations.push(...checkRawElements(scanResults, cfg));
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
catch {
|
|
654
|
-
// Cross-component checks are high
|
|
655
|
-
}
|
|
656
|
-
return violations;
|
|
657
|
-
},
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
404
|
// ── Styles reducer (stage 3) ─────────────────────────────────────────────────
|
|
661
405
|
// ── Styles CSS visitor (Spec 26 Phase 2) ───────────────────────────────────────
|
|
662
406
|
/**
|
|
@@ -723,6 +467,7 @@ export function createStylesSourceVisitor() {
|
|
|
723
467
|
// Lazy-load to avoid circular dependency issues at module load time.
|
|
724
468
|
const { extractDeclarations } = await import('./styles/styleExtractor.js');
|
|
725
469
|
const { extractClassUsage } = await import('./styles/styleIndexer.js');
|
|
470
|
+
const { harvestCssInJsStyleClasses } = await import('./styles/cssInJsHarvest.js');
|
|
726
471
|
const { loadTailwindConfig } = await import('./styles/tailwindConfigLoader.js');
|
|
727
472
|
const tsAst = ast;
|
|
728
473
|
const tsAdapter = adapter;
|
|
@@ -733,6 +478,10 @@ export function createStylesSourceVisitor() {
|
|
|
733
478
|
const tailwindTokens = tailwindConfig.tokens;
|
|
734
479
|
const declarations = extractDeclarations(filePath, tsAdapter, sourceCode, tsAst, tailwindTokens);
|
|
735
480
|
const classUsage = extractClassUsage(filePath, sourceCode);
|
|
481
|
+
// Defined classes come from the CSS parser over each CSS-in-JS template
|
|
482
|
+
// body — the same parser path markup `<style>` blocks use, so keyframes are
|
|
483
|
+
// excluded structurally rather than scanned out of the selector `context`.
|
|
484
|
+
const definedClasses = harvestCssInJsStyleClasses(filePath, sourceCode, tsAst);
|
|
736
485
|
const contentHash = createHash('sha256').update(sourceCode).digest('hex');
|
|
737
486
|
const diagnostic = tailwindConfig.diagnostic && !emittedTailwindDiagnostic
|
|
738
487
|
? tailwindConfig.diagnostic
|
|
@@ -742,7 +491,7 @@ export function createStylesSourceVisitor() {
|
|
|
742
491
|
return {
|
|
743
492
|
violations: [],
|
|
744
493
|
facts: {
|
|
745
|
-
[filePath]: { declarations, classUsage, contentHash },
|
|
494
|
+
[filePath]: { declarations, classUsage, definedClasses, contentHash },
|
|
746
495
|
},
|
|
747
496
|
...(diagnostic ? { diagnostics: [diagnostic] } : {}),
|
|
748
497
|
};
|
|
@@ -834,11 +583,12 @@ function insertSourceStyleFacts(indexHandle, sourceFacts, isScoped) {
|
|
|
834
583
|
// Insert declarations (and their defined-class catalog entries).
|
|
835
584
|
for (const decl of facts.declarations) {
|
|
836
585
|
runStyleWrite(stmts, 'insDecl', [decl.property, decl.rawValue, decl.normalizedValue ? JSON.stringify(decl.normalizedValue) : null, decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, facts.contentHash]);
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
586
|
+
}
|
|
587
|
+
// Defined classes come from the CSS parser over each CSS-in-JS template body
|
|
588
|
+
// (harvested by the visitor and carried on the fact) — not a selector scan
|
|
589
|
+
// over `declarations[].context`, so keyframes are excluded structurally.
|
|
590
|
+
for (const ref of facts.definedClasses) {
|
|
591
|
+
runStyleWrite(stmts, 'insClass', [ref.className, ref.filePath]);
|
|
842
592
|
}
|
|
843
593
|
// Insert class usage.
|
|
844
594
|
for (const cu of facts.classUsage) {
|
|
@@ -917,15 +667,12 @@ export function createStylesReducer() {
|
|
|
917
667
|
// Insert declarations
|
|
918
668
|
for (const decl of facts.declarations) {
|
|
919
669
|
runStyleWrite(stmts, 'insDecl', [decl.property, decl.rawValue, JSON.stringify(decl.normalizedValue), decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, contentHash]);
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
runStyleWrite(stmts, 'insClass', [m[1], decl.filePath]);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
670
|
+
}
|
|
671
|
+
// Populate the defined-class catalog (Spec 45) from the CSS parser's
|
|
672
|
+
// class-selector harvest (`classUsage`) — keyframes and pseudo-classes
|
|
673
|
+
// are excluded structurally, so no selector scrape over `context`.
|
|
674
|
+
for (const ref of harvestDefinedClasses(facts.declarations, facts.classUsage, true)) {
|
|
675
|
+
runStyleWrite(stmts, 'insClass', [ref.className, ref.filePath]);
|
|
929
676
|
}
|
|
930
677
|
// Insert tokens
|
|
931
678
|
for (const tok of facts.tokens) {
|
|
@@ -968,125 +715,6 @@ export function createStylesReducer() {
|
|
|
968
715
|
category: 'style',
|
|
969
716
|
};
|
|
970
717
|
}
|
|
971
|
-
// ── Conventions reducer (stage 3) ────────────────────────────────────────────
|
|
972
|
-
/**
|
|
973
|
-
* Builds the shared analyzer-config bag for reducers that query the code
|
|
974
|
-
* index: the reducer's own namespaced config spread over the index handle,
|
|
975
|
-
* project root, in-scope file set, and scope flag. Reducers that need extra
|
|
976
|
-
* inputs (`readSource`, `exportsMap`) spread this and layer them on top.
|
|
977
|
-
*
|
|
978
|
-
* Scoped/diff audits thread `_infra.files` (absolute) as `scopedFiles` so the
|
|
979
|
-
* analyzer scopes its function/file queries to only the changed files;
|
|
980
|
-
* `functions.file_path` is also absolute, so the IN clause matches directly.
|
|
981
|
-
*/
|
|
982
|
-
function makeIndexAnalyzerConfig(context) {
|
|
983
|
-
const scopedFiles = context.isScoped ? (context.config.files ?? []) : undefined;
|
|
984
|
-
return {
|
|
985
|
-
...context.config,
|
|
986
|
-
indexHandle: context.indexHandle,
|
|
987
|
-
projectRoot: context.projectRoot,
|
|
988
|
-
scopedFiles,
|
|
989
|
-
isScoped: context.isScoped,
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Conventions reducer — queries conventions table and flags deviations
|
|
994
|
-
* (usage-pair, import-form, error-handling, export-shape, naming).
|
|
995
|
-
* Uses analyzer.analyze([]) with rawDb injected.
|
|
996
|
-
*
|
|
997
|
-
* @returns The conventions stage-3 reducer.
|
|
998
|
-
*/
|
|
999
|
-
export function createConventionsReducer() {
|
|
1000
|
-
return {
|
|
1001
|
-
name: 'conventions',
|
|
1002
|
-
stage: 'reducer',
|
|
1003
|
-
consumes: [],
|
|
1004
|
-
getRuleIds: () => getRuleIdsFor('conventions'),
|
|
1005
|
-
async reduce(_allFacts, context) {
|
|
1006
|
-
if (!context.indexHandle)
|
|
1007
|
-
return { violations: [], facts: {} };
|
|
1008
|
-
try {
|
|
1009
|
-
const { UniversalConventionsAnalyzer } = await import('./analyzers/universal/UniversalConventionsAnalyzer.js');
|
|
1010
|
-
const analyzer = new UniversalConventionsAnalyzer();
|
|
1011
|
-
// B2: Build exports map from function-index visitor facts (AST-extracted)
|
|
1012
|
-
const functionIndexFacts = _allFacts['function-index'];
|
|
1013
|
-
const exportsMap = functionIndexFacts
|
|
1014
|
-
? new Map(Object.entries(functionIndexFacts)
|
|
1015
|
-
.filter(([, data]) => data?.exports?.length)
|
|
1016
|
-
.map(([filePath, data]) => [filePath, data.exports]))
|
|
1017
|
-
: undefined;
|
|
1018
|
-
const config = { ...makeIndexAnalyzerConfig(context), readSource: context.readSource, exportsMap };
|
|
1019
|
-
const result = await analyzer.analyze([], config);
|
|
1020
|
-
// Count the INPUT the conventions analyzer reads (the function index the
|
|
1021
|
-
// function-index visitor populated), not the derived `conventions` table it
|
|
1022
|
-
// writes. `conventions` is an output cache (mineAllConventions) that can be
|
|
1023
|
-
// empty when `minCorpus` is unmet even though thousands of functions fed the
|
|
1024
|
-
// analyzer — counting it as "facts consumed" misreports a present input as
|
|
1025
|
-
// notApplicable. (Spec 39 R2/R3 bug B)
|
|
1026
|
-
const factsConsumed = context.indexHandle.count('functions');
|
|
1027
|
-
return {
|
|
1028
|
-
violations: result.violations ?? [],
|
|
1029
|
-
facts: {},
|
|
1030
|
-
factsConsumed,
|
|
1031
|
-
...(result.diagnostics && result.diagnostics.length > 0 && { diagnostics: result.diagnostics }),
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
catch {
|
|
1035
|
-
return { violations: [], facts: {} };
|
|
1036
|
-
}
|
|
1037
|
-
},
|
|
1038
|
-
defaultConfig: {},
|
|
1039
|
-
description: 'Mines codebase conventions and flags deviations',
|
|
1040
|
-
category: 'style',
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
// ── Cross-Domain reducer (stage 4) ───────────────────────────────────────────
|
|
1044
|
-
/**
|
|
1045
|
-
* Cross-domain reducer — runs written-never-read, read-never-written,
|
|
1046
|
-
* transaction-boundary risk, no-validator-reachable, and uncovered-risk detectors.
|
|
1047
|
-
* Uses analyzer.analyze([]) with rawDb injected.
|
|
1048
|
-
*
|
|
1049
|
-
* @returns The cross-domain stage-4 reducer.
|
|
1050
|
-
*/
|
|
1051
|
-
export function createCrossDomainReducer() {
|
|
1052
|
-
return {
|
|
1053
|
-
name: 'cross-domain',
|
|
1054
|
-
stage: 'derivedReducer',
|
|
1055
|
-
getRuleIds: () => getRuleIdsFor('cross-domain'),
|
|
1056
|
-
async reduce(_allFacts, context) {
|
|
1057
|
-
if (!context.indexHandle)
|
|
1058
|
-
return { violations: [], facts: {} };
|
|
1059
|
-
try {
|
|
1060
|
-
const { CrossDomainAnalyzer } = await import('./analyzers/crossDomain/CrossDomainAnalyzer.js');
|
|
1061
|
-
const analyzer = new CrossDomainAnalyzer();
|
|
1062
|
-
const config = makeIndexAnalyzerConfig(context);
|
|
1063
|
-
const result = await analyzer.analyze([], config);
|
|
1064
|
-
// Count DB rows consumed across primary cross-domain tables
|
|
1065
|
-
let factsConsumed = 0;
|
|
1066
|
-
try {
|
|
1067
|
-
factsConsumed += context.indexHandle.count('schema_usage');
|
|
1068
|
-
}
|
|
1069
|
-
catch { /* table may not exist */ }
|
|
1070
|
-
// `indexed_functions` is not a real table — the function index lives in
|
|
1071
|
-
// `functions`. The old name made this term silently no-op (swallowed by the
|
|
1072
|
-
// catch), undercounting the input for read/write coverage detectors.
|
|
1073
|
-
// (Spec 39 R2/R3 bug C)
|
|
1074
|
-
try {
|
|
1075
|
-
factsConsumed += context.indexHandle.count('functions');
|
|
1076
|
-
}
|
|
1077
|
-
catch { /* table may not exist */ }
|
|
1078
|
-
return { violations: result.violations ?? [], facts: {}, factsConsumed };
|
|
1079
|
-
}
|
|
1080
|
-
catch (e) {
|
|
1081
|
-
console.error('[cross-domain reducer] error:', e.message);
|
|
1082
|
-
return { violations: [], facts: {} };
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
defaultConfig: {},
|
|
1086
|
-
description: 'Detects cross-domain issues (schema lifecycle, validation bypass, coverage gaps)',
|
|
1087
|
-
category: 'architecture',
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
718
|
// ── Cross-language entity extraction + Stage-4 reducers ──────────────────────
|
|
1091
719
|
//
|
|
1092
720
|
// The three cross-language analyzers (SchemaValidator, APIContractAnalyzer,
|
|
@@ -1150,6 +778,32 @@ function clCollectCallees(node, sourceCode) {
|
|
|
1150
778
|
walk(node);
|
|
1151
779
|
return [...names];
|
|
1152
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Whether a function node is lexically nested inside another function body.
|
|
783
|
+
*
|
|
784
|
+
* A nested function (a closure) is only reachable from its enclosing scope, not
|
|
785
|
+
* from module top-level, so a flat name-based call graph cannot observe its use
|
|
786
|
+
* the way it observes a top-level symbol's callers. The cross-language entity
|
|
787
|
+
* extractor records this flag so orphan detection can treat a closure's "no
|
|
788
|
+
* connections" as unproven rather than as evidence of dead code. Go named
|
|
789
|
+
* functions are never nested (Go only admits anonymous function *literals*
|
|
790
|
+
* inside a body, and those are not `function_declaration` nodes), so this is a
|
|
791
|
+
* TS/JS-family signal.
|
|
792
|
+
*/
|
|
793
|
+
function clIsNestedFunction(node) {
|
|
794
|
+
let p = node.parent ?? null;
|
|
795
|
+
while (p) {
|
|
796
|
+
const t = p.type;
|
|
797
|
+
if (t === 'function_declaration' ||
|
|
798
|
+
t === 'method_definition' ||
|
|
799
|
+
t === 'arrow_function' ||
|
|
800
|
+
t === 'function_expression') {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
p = p.parent ?? null;
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
1153
807
|
/**
|
|
1154
808
|
* Collect every reference name in a file's AST, independent of entity
|
|
1155
809
|
* boundaries. Orphan detection needs the complete same-file reference set —
|
|
@@ -1237,107 +891,6 @@ function clMakeFunction(opts) {
|
|
|
1237
891
|
metadata: { callees, isMethod },
|
|
1238
892
|
};
|
|
1239
893
|
}
|
|
1240
|
-
/**
|
|
1241
|
-
* Detect a dynamic `import('…')`/`require('…')` call expression.
|
|
1242
|
-
*
|
|
1243
|
-
* Returns `{ specifier }` for a static-string argument (resolvable into an
|
|
1244
|
-
* import edge), `{ computed, expression }` for a computed specifier
|
|
1245
|
-
* (`import(someVariable)` — unresolvable, reported rather than silenced), or
|
|
1246
|
-
* `null` when the call is not a dynamic import/require.
|
|
1247
|
-
*/
|
|
1248
|
-
function clDynamicImport(node, sourceCode) {
|
|
1249
|
-
if (node.type !== 'call_expression')
|
|
1250
|
-
return null;
|
|
1251
|
-
const fn = node.children?.[0];
|
|
1252
|
-
const isImport = fn?.type === 'import';
|
|
1253
|
-
const isRequire = fn?.type === 'identifier' && getNodeText(fn, sourceCode) === 'require';
|
|
1254
|
-
if (!isImport && !isRequire)
|
|
1255
|
-
return null;
|
|
1256
|
-
const args = node.children?.find((c) => c.type === 'arguments');
|
|
1257
|
-
const stringNode = args?.children?.find((c) => c.type === 'string');
|
|
1258
|
-
if (stringNode) {
|
|
1259
|
-
const text = getNodeText(stringNode, sourceCode);
|
|
1260
|
-
if (text.length >= 2) {
|
|
1261
|
-
return { specifier: text.slice(1, -1) };
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
// A template literal with no `${…}` substitution is a compile-time constant,
|
|
1265
|
-
// not a computed specifier — `import(\`@babel/plugin-syntax-jsx\`)` resolves
|
|
1266
|
-
// to an edge just like `import('@babel/plugin-syntax-jsx')`. Only an
|
|
1267
|
-
// interpolated template (`` import(`./${name}.js`) ``) is genuinely computed.
|
|
1268
|
-
const templateNode = args?.children?.find((c) => c.type === 'template_string');
|
|
1269
|
-
if (templateNode) {
|
|
1270
|
-
const hasSubstitution = (templateNode.children ?? []).some((c) => c.type === 'template_substitution');
|
|
1271
|
-
if (!hasSubstitution) {
|
|
1272
|
-
const t = getNodeText(templateNode, sourceCode);
|
|
1273
|
-
if (t.length >= 2 && t.startsWith('`') && t.endsWith('`')) {
|
|
1274
|
-
return { specifier: t.slice(1, -1) };
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
// Computed specifier — the target module cannot be resolved statically.
|
|
1279
|
-
const named = args?.children?.[0];
|
|
1280
|
-
const expression = named
|
|
1281
|
-
? getNodeText(named, sourceCode)
|
|
1282
|
-
: getNodeText(args, sourceCode).replace(/^\(|\)$/g, '').trim();
|
|
1283
|
-
return { computed: true, expression };
|
|
1284
|
-
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Collect a file's import specifiers and whether it declares any exports.
|
|
1287
|
-
*
|
|
1288
|
-
* Feeds file-level reachability: an `import_statement` (TS/JS) or
|
|
1289
|
-
* `import_declaration` (Go) records a dependency; an `export_statement`
|
|
1290
|
-
* (TS/JS) records that the file exposes symbols to importers. Go exports are
|
|
1291
|
-
* computed separately from the extracted entities (capitalized top-level
|
|
1292
|
-
* names), since Go has no `export` keyword. Dynamic `import()`/`require()`
|
|
1293
|
-
* with a static string argument also record an import edge (Spec 58 R2).
|
|
1294
|
-
*/
|
|
1295
|
-
function clCollectFileInfo(root, lang, sourceCode) {
|
|
1296
|
-
const imports = new Set();
|
|
1297
|
-
const unresolvedDynamicImports = [];
|
|
1298
|
-
let hasExports = false;
|
|
1299
|
-
walkAST(root, (node) => {
|
|
1300
|
-
if (node.type === 'import_statement') {
|
|
1301
|
-
const source = getFieldNode(node, 'source');
|
|
1302
|
-
if (source) {
|
|
1303
|
-
const text = getNodeText(source, sourceCode);
|
|
1304
|
-
imports.add(text.replace(/^['"]|['"]$/g, ''));
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
else if (node.type === 'import_declaration') {
|
|
1308
|
-
for (const spec of node.children ?? []) {
|
|
1309
|
-
if (spec.type === 'import_spec') {
|
|
1310
|
-
const p = getFieldNode(spec, 'path');
|
|
1311
|
-
if (p) {
|
|
1312
|
-
const text = getNodeText(p, sourceCode);
|
|
1313
|
-
imports.add(text.replace(/^['"]|['"]$/g, ''));
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
else if (node.type === 'export_statement') {
|
|
1319
|
-
hasExports = true;
|
|
1320
|
-
// Re-exports (`export { x } from './y'`, `export * from './y'`) are import
|
|
1321
|
-
// edges for reachability: a barrel re-exporting a module marks it live,
|
|
1322
|
-
// not dead. The `source` field is present only on the `from` form.
|
|
1323
|
-
const source = getFieldNode(node, 'source');
|
|
1324
|
-
if (source) {
|
|
1325
|
-
const text = getNodeText(source, sourceCode);
|
|
1326
|
-
imports.add(text.replace(/^['"]|['"]$/g, ''));
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
else if (node.type === 'call_expression') {
|
|
1330
|
-
const dyn = clDynamicImport(node, sourceCode);
|
|
1331
|
-
if (dyn && 'specifier' in dyn) {
|
|
1332
|
-
imports.add(dyn.specifier);
|
|
1333
|
-
}
|
|
1334
|
-
else if (dyn) {
|
|
1335
|
-
unresolvedDynamicImports.push({ line: node.location.start.line, expression: dyn.expression });
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
});
|
|
1339
|
-
return { imports: [...imports], hasExports, unresolvedDynamicImports };
|
|
1340
|
-
}
|
|
1341
894
|
// ── TS/JS extraction ─────────────────────────────────────────────────────────
|
|
1342
895
|
function clExtractTSParams(node, sourceCode) {
|
|
1343
896
|
const params = [];
|
|
@@ -1406,6 +959,7 @@ function clExtractTSEntities(root, filePath, sourceCode, lang, out) {
|
|
|
1406
959
|
isMethod: node.type === 'method_definition',
|
|
1407
960
|
});
|
|
1408
961
|
entity.metadata.fileReferences = fileReferences;
|
|
962
|
+
entity.metadata.isNested = clIsNestedFunction(node);
|
|
1409
963
|
out.push(entity);
|
|
1410
964
|
return;
|
|
1411
965
|
}
|
|
@@ -1428,6 +982,7 @@ function clExtractTSEntities(root, filePath, sourceCode, lang, out) {
|
|
|
1428
982
|
complexity: calculateComplexity(node),
|
|
1429
983
|
});
|
|
1430
984
|
entity.metadata.fileReferences = fileReferences;
|
|
985
|
+
entity.metadata.isNested = clIsNestedFunction(node);
|
|
1431
986
|
out.push(entity);
|
|
1432
987
|
return;
|
|
1433
988
|
}
|
|
@@ -1611,638 +1166,6 @@ export function extractCrossLanguageEntities(ast, filePath, sourceCode, lang) {
|
|
|
1611
1166
|
}
|
|
1612
1167
|
return entities;
|
|
1613
1168
|
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Create the cross-language-entities stage-2 visitor, which extracts entities
|
|
1616
|
-
* and import/export info for the cross-language analyzers.
|
|
1617
|
-
*
|
|
1618
|
-
* @returns The cross-language-entities stage-2 visitor.
|
|
1619
|
-
*/
|
|
1620
|
-
export function createCrossLanguageEntityVisitor() {
|
|
1621
|
-
return {
|
|
1622
|
-
name: 'cross-language-entities',
|
|
1623
|
-
stage: 'visitor',
|
|
1624
|
-
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs', '.go'],
|
|
1625
|
-
getRuleIds: () => [],
|
|
1626
|
-
async visit(ast, _adapter, context, sourceCode) {
|
|
1627
|
-
const filePath = context.filePath;
|
|
1628
|
-
if (!filePath)
|
|
1629
|
-
return { violations: [], facts: {} };
|
|
1630
|
-
const lang = getLanguageFromPath(filePath);
|
|
1631
|
-
if (lang !== 'typescript' && lang !== 'javascript' && lang !== 'go')
|
|
1632
|
-
return { violations: [], facts: {} };
|
|
1633
|
-
const root = ast.root;
|
|
1634
|
-
const entities = [];
|
|
1635
|
-
if (lang === 'go') {
|
|
1636
|
-
clExtractGoEntities(root, filePath, sourceCode, entities);
|
|
1637
|
-
}
|
|
1638
|
-
else {
|
|
1639
|
-
clExtractTSEntities(root, filePath, sourceCode, lang, entities);
|
|
1640
|
-
}
|
|
1641
|
-
const fileInfo = clCollectFileInfo(root, lang, sourceCode);
|
|
1642
|
-
// Go has no `export` keyword — an exported symbol is a capitalized
|
|
1643
|
-
// top-level name, which the entity extractor already records as public.
|
|
1644
|
-
const hasExports = lang === 'go'
|
|
1645
|
-
? entities.some((e) => e.visibility === 'public')
|
|
1646
|
-
: fileInfo.hasExports;
|
|
1647
|
-
return {
|
|
1648
|
-
violations: [],
|
|
1649
|
-
facts: { [filePath]: { entities, imports: fileInfo.imports, hasExports, unresolvedDynamicImports: fileInfo.unresolvedDynamicImports } },
|
|
1650
|
-
};
|
|
1651
|
-
},
|
|
1652
|
-
defaultConfig: {},
|
|
1653
|
-
description: 'Extracts cross-language entities (functions, structs, interfaces) for the cross-language analyzers',
|
|
1654
|
-
category: 'infrastructure',
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
// ── Shared flatten + reference building ──────────────────────────────────────
|
|
1658
|
-
function clFlattenEntities(allFacts) {
|
|
1659
|
-
const facts = allFacts['cross-language-entities'];
|
|
1660
|
-
if (!facts)
|
|
1661
|
-
return [];
|
|
1662
|
-
const entities = [];
|
|
1663
|
-
for (const data of Object.values(facts)) {
|
|
1664
|
-
if (data?.entities)
|
|
1665
|
-
entities.push(...data.entities);
|
|
1666
|
-
}
|
|
1667
|
-
return entities;
|
|
1668
|
-
}
|
|
1669
|
-
/** Extract per-file `{ imports, hasExports }` from the cross-language facts. */
|
|
1670
|
-
function clFileFacts(allFacts) {
|
|
1671
|
-
const facts = allFacts['cross-language-entities'];
|
|
1672
|
-
const map = new Map();
|
|
1673
|
-
if (!facts)
|
|
1674
|
-
return map;
|
|
1675
|
-
for (const [filePath, data] of Object.entries(facts)) {
|
|
1676
|
-
if (!data)
|
|
1677
|
-
continue;
|
|
1678
|
-
map.set(filePath, {
|
|
1679
|
-
imports: data.imports ?? [],
|
|
1680
|
-
hasExports: data.hasExports ?? false,
|
|
1681
|
-
unresolvedDynamicImports: data.unresolvedDynamicImports ?? [],
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
return map;
|
|
1685
|
-
}
|
|
1686
|
-
function clIsTestFile(fp) {
|
|
1687
|
-
const lower = fp.toLowerCase();
|
|
1688
|
-
return lower.includes('.test.') || lower.includes('.spec.') ||
|
|
1689
|
-
lower.includes('__tests__') || lower.includes('/test/') || lower.includes('/tests/') ||
|
|
1690
|
-
lower.endsWith('_test.go');
|
|
1691
|
-
}
|
|
1692
|
-
// Files the framework loads directly rather than via an import from sibling
|
|
1693
|
-
// code. Skipped when flagging unreferenced modules — a route/page/entry file
|
|
1694
|
-
// that nothing imports is an entry point, not dead code.
|
|
1695
|
-
const CL_ENTRY_BASENAMES = new Set([
|
|
1696
|
-
'route', 'page', 'layout', 'loading', 'error', 'not-found', 'template', 'default',
|
|
1697
|
-
'middleware', 'instrumentation', 'server', 'client', 'cli', 'main', 'app', 'index', 'worker',
|
|
1698
|
-
'setup', 'seed',
|
|
1699
|
-
]);
|
|
1700
|
-
function clIsEntryPointFile(fp) {
|
|
1701
|
-
const segments = fp.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
1702
|
-
const base = segments[segments.length - 1] ?? '';
|
|
1703
|
-
const stem = base.replace(/\.[^.]+$/, '');
|
|
1704
|
-
if (CL_ENTRY_BASENAMES.has(stem))
|
|
1705
|
-
return true;
|
|
1706
|
-
if (stem.endsWith('.config'))
|
|
1707
|
-
return true; // next.config, vite.config, tailwind.config, …
|
|
1708
|
-
const joined = '/' + segments.join('/') + '/';
|
|
1709
|
-
if (joined.includes('/app/api/') || joined.includes('/pages/api/'))
|
|
1710
|
-
return true;
|
|
1711
|
-
if (joined.includes('/scripts/') || joined.includes('/bin/') || joined.includes('/cmd/'))
|
|
1712
|
-
return true;
|
|
1713
|
-
return false;
|
|
1714
|
-
}
|
|
1715
|
-
/** Resolve one import specifier to zero or one internal file via the classifier. */
|
|
1716
|
-
function clResolveImport(dep, sourceFile, options) {
|
|
1717
|
-
const { classification, resolvedPath } = classifyImportSpecifier(dep, sourceFile, options.corpusFiles, {
|
|
1718
|
-
virtualModules: options.virtualModules,
|
|
1719
|
-
aliasPatterns: options.tsconfigAliases?.pathPatterns ?? [],
|
|
1720
|
-
pathMappings: options.tsconfigAliases?.paths ?? {},
|
|
1721
|
-
baseUrl: options.tsconfigAliases?.baseUrl,
|
|
1722
|
-
projectRoot: options.projectRoot,
|
|
1723
|
-
});
|
|
1724
|
-
if (classification === 'internal-resolved' && resolvedPath)
|
|
1725
|
-
return [resolvedPath];
|
|
1726
|
-
return [];
|
|
1727
|
-
}
|
|
1728
|
-
/**
|
|
1729
|
-
* Compute file-level reachability from per-file imports.
|
|
1730
|
-
*
|
|
1731
|
-
* A file is a framework entry point (live → 1.0), imported by another file
|
|
1732
|
-
* (live → 0.5), or referenced by nothing (dead → 0.0). Resolution goes through
|
|
1733
|
-
* `classifyImportSpecifier` (relative paths and tsconfig `paths` aliases like
|
|
1734
|
-
* `@/`) instead of the legacy basename/fuzzy matcher, so an alias import creates
|
|
1735
|
-
* a real edge and a bare package import never fabricates a local one.
|
|
1736
|
-
*/
|
|
1737
|
-
function clComputeReachability(fileFacts, options) {
|
|
1738
|
-
const filePaths = new Set(fileFacts.keys());
|
|
1739
|
-
const packageEntries = options.packageEntryPoints ?? new Set();
|
|
1740
|
-
const importersOf = new Map();
|
|
1741
|
-
for (const [fp, info] of fileFacts) {
|
|
1742
|
-
for (const dep of info.imports) {
|
|
1743
|
-
const targets = clResolveImport(dep, fp, options);
|
|
1744
|
-
for (const t of targets) {
|
|
1745
|
-
if (t === fp)
|
|
1746
|
-
continue;
|
|
1747
|
-
if (!importersOf.has(t))
|
|
1748
|
-
importersOf.set(t, new Set());
|
|
1749
|
-
importersOf.get(t).add(fp);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
const reachability = new Map();
|
|
1754
|
-
for (const fp of filePaths) {
|
|
1755
|
-
const entry = clIsEntryPointFile(fp) || packageEntries.has(fp);
|
|
1756
|
-
const imported = (importersOf.get(fp)?.size ?? 0) > 0;
|
|
1757
|
-
reachability.set(fp, entry ? 1 : imported ? 0.5 : 0);
|
|
1758
|
-
}
|
|
1759
|
-
return { reachability, importersOf };
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* Spec 60 R1 — classify every non-test source module as `tested`,
|
|
1763
|
-
* `untested-live`, or `untested-dead` off the file-level import edges
|
|
1764
|
-
* (`importersOf`), not `function_dependencies`.
|
|
1765
|
-
*
|
|
1766
|
-
* A module is `tested` when a test file imports it *directly* (the edge set is
|
|
1767
|
-
* alias-aware — built by `clResolveImport` through `classifyImportSpecifier`).
|
|
1768
|
-
* `untested-dead` is the *post*-exception dead bucket: a file imported by
|
|
1769
|
-
* nothing *and* not a framework entry point. `deadPreException` is the
|
|
1770
|
-
* empty-importers count before the entry-point exception, so the drop
|
|
1771
|
-
* (`deadPreException - untestedDead`) attributes the entry points exempted.
|
|
1772
|
-
*
|
|
1773
|
-
* @param fileFacts - Per-file import/export info keyed by project-relative path.
|
|
1774
|
-
* @param importersOf - Map of file to the set of files that import it.
|
|
1775
|
-
* @param packageEntrySet - Framework/package entry points exempted from the dead bucket.
|
|
1776
|
-
* @returns The test-coverage classification report.
|
|
1777
|
-
*/
|
|
1778
|
-
export function classifyTestCoverage(fileFacts, importersOf, packageEntrySet) {
|
|
1779
|
-
const result = {
|
|
1780
|
-
tested: 0,
|
|
1781
|
-
untestedLive: 0,
|
|
1782
|
-
untestedDead: 0,
|
|
1783
|
-
total: 0,
|
|
1784
|
-
deadPreException: 0,
|
|
1785
|
-
deadPostException: 0,
|
|
1786
|
-
deadDrop: 0,
|
|
1787
|
-
entryPointExemptions: [],
|
|
1788
|
-
testedFiles: [],
|
|
1789
|
-
untestedLiveFiles: [],
|
|
1790
|
-
untestedDeadFiles: [],
|
|
1791
|
-
};
|
|
1792
|
-
for (const fp of fileFacts.keys()) {
|
|
1793
|
-
if (clIsTestFile(fp))
|
|
1794
|
-
continue;
|
|
1795
|
-
result.total++;
|
|
1796
|
-
const importers = importersOf.get(fp);
|
|
1797
|
-
if (!importers || importers.size === 0) {
|
|
1798
|
-
// Imported by nothing — dead *unless* it is a framework entry point.
|
|
1799
|
-
result.deadPreException++;
|
|
1800
|
-
const isPackageEntry = packageEntrySet.has(fp);
|
|
1801
|
-
const isEntry = isPackageEntry || clIsEntryPointFile(fp);
|
|
1802
|
-
if (isEntry) {
|
|
1803
|
-
result.untestedLive++;
|
|
1804
|
-
result.untestedLiveFiles.push(fp);
|
|
1805
|
-
result.entryPointExemptions.push({
|
|
1806
|
-
file: fp,
|
|
1807
|
-
kind: isPackageEntry ? 'package-entry' : 'entry-point',
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
else {
|
|
1811
|
-
result.untestedDead++;
|
|
1812
|
-
result.untestedDeadFiles.push(fp);
|
|
1813
|
-
}
|
|
1814
|
-
continue;
|
|
1815
|
-
}
|
|
1816
|
-
let hasTestImporter = false;
|
|
1817
|
-
for (const importer of importers) {
|
|
1818
|
-
if (clIsTestFile(importer)) {
|
|
1819
|
-
hasTestImporter = true;
|
|
1820
|
-
break;
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
if (hasTestImporter) {
|
|
1824
|
-
result.tested++;
|
|
1825
|
-
result.testedFiles.push(fp);
|
|
1826
|
-
}
|
|
1827
|
-
else {
|
|
1828
|
-
result.untestedLive++;
|
|
1829
|
-
result.untestedLiveFiles.push(fp);
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
result.deadPostException = result.untestedDead;
|
|
1833
|
-
result.deadDrop = result.deadPreException - result.untestedDead;
|
|
1834
|
-
return result;
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Spec 60 R3 — group unreferenced modules by basename, surfacing the pattern of
|
|
1838
|
-
* the same file name appearing in several sibling directories (e.g. two
|
|
1839
|
-
* `ErrorState.tsx`, three `services.ts` under separate `reports` folders). Only
|
|
1840
|
-
* basenames with ≥2 files become clusters; a singleton dead file is not a duplicate.
|
|
1841
|
-
*
|
|
1842
|
-
* @param deadFiles - Unreferenced module paths to cluster by basename.
|
|
1843
|
-
* @returns Dead-module clusters (only basenames with two or more files).
|
|
1844
|
-
*/
|
|
1845
|
-
export function clusterDeadModules(deadFiles) {
|
|
1846
|
-
const byBasename = new Map();
|
|
1847
|
-
for (const fp of deadFiles) {
|
|
1848
|
-
const segments = fp.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
1849
|
-
const basename = segments[segments.length - 1] ?? fp;
|
|
1850
|
-
const list = byBasename.get(basename) ?? [];
|
|
1851
|
-
list.push(fp);
|
|
1852
|
-
byBasename.set(basename, list);
|
|
1853
|
-
}
|
|
1854
|
-
const clusters = [];
|
|
1855
|
-
for (const [basename, files] of byBasename) {
|
|
1856
|
-
if (files.length >= 2) {
|
|
1857
|
-
clusters.push({ basename, count: files.length, files });
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
clusters.sort((a, b) => b.count - a.count || a.basename.localeCompare(b.basename));
|
|
1861
|
-
return clusters;
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* Build `calls` cross-references by resolving each entity's extracted callee
|
|
1865
|
-
* names against the corpus entity name index. Library calls (fetch, map, …)
|
|
1866
|
-
* resolve to nothing and are dropped — only real intra-corpus call edges become
|
|
1867
|
-
* references.
|
|
1868
|
-
*/
|
|
1869
|
-
function clBuildReferences(entities) {
|
|
1870
|
-
const byName = new Map();
|
|
1871
|
-
for (const e of entities) {
|
|
1872
|
-
const key = e.name.toLowerCase();
|
|
1873
|
-
const list = byName.get(key) ?? [];
|
|
1874
|
-
list.push(e);
|
|
1875
|
-
byName.set(key, list);
|
|
1876
|
-
}
|
|
1877
|
-
const refs = [];
|
|
1878
|
-
for (const e of entities) {
|
|
1879
|
-
const callees = e.metadata?.callees ?? [];
|
|
1880
|
-
const eDir = e.file.split('/').slice(0, -1).join('/');
|
|
1881
|
-
for (const callee of callees) {
|
|
1882
|
-
const name = (callee.split('.').pop() ?? callee).toLowerCase();
|
|
1883
|
-
// A free function that calls itself by bare name is recursing. Resolving
|
|
1884
|
-
// that name to a *different* same-named entity (a second `dfs` helper in
|
|
1885
|
-
// the same file) fabricates a spurious mutual-recursion cycle between two
|
|
1886
|
-
// unrelated functions and reports it as a circular dependency. Recursion
|
|
1887
|
-
// is a self-reference, not a module cycle — drop it rather than resolve
|
|
1888
|
-
// it sideways. (Methods are named `Class.method`, so a bare callee never
|
|
1889
|
-
// matches their own name and this only affects free functions.)
|
|
1890
|
-
if (name === e.name.toLowerCase())
|
|
1891
|
-
continue;
|
|
1892
|
-
const others = (byName.get(name) ?? []).filter((t) => t.id !== e.id);
|
|
1893
|
-
if (others.length === 0)
|
|
1894
|
-
continue;
|
|
1895
|
-
// A bare call-expression name (`log`) can collide with many entities
|
|
1896
|
-
// across the corpus. Resolving it to *every* entity of that name
|
|
1897
|
-
// fabricates a dense graph (125k edges / 5.9k false cycles on a ~7.8k
|
|
1898
|
-
// node corpus) that corrupts cycle/hub/coupling detection. Narrow the
|
|
1899
|
-
// resolution to an unambiguous match — same file, then same directory,
|
|
1900
|
-
// then a unique global match — and drop the edge when the name is still
|
|
1901
|
-
// ambiguous: a missing edge is safer than a fabricated one for
|
|
1902
|
-
// structural analysis.
|
|
1903
|
-
const sameFile = others.filter((t) => t.file === e.file);
|
|
1904
|
-
const sameDir = sameFile.length === 0
|
|
1905
|
-
? others.filter((t) => t.file.split('/').slice(0, -1).join('/') === eDir)
|
|
1906
|
-
: [];
|
|
1907
|
-
const uniqueGlobal = others.length === 1 ? others : [];
|
|
1908
|
-
const chosen = sameFile.length === 1
|
|
1909
|
-
? sameFile
|
|
1910
|
-
: sameDir.length === 1
|
|
1911
|
-
? sameDir
|
|
1912
|
-
: uniqueGlobal;
|
|
1913
|
-
for (const t of chosen) {
|
|
1914
|
-
refs.push({
|
|
1915
|
-
sourceId: e.id,
|
|
1916
|
-
targetId: t.id,
|
|
1917
|
-
type: 'calls',
|
|
1918
|
-
sourceLanguage: e.language,
|
|
1919
|
-
targetLanguage: t.language,
|
|
1920
|
-
confidence: 0.7,
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
return refs;
|
|
1926
|
-
}
|
|
1927
|
-
// ── schema-validator reducer (Stage 4) ───────────────────────────────────────
|
|
1928
|
-
/**
|
|
1929
|
-
* Create the schema-validator stage-4 reducer, which compares schemas across
|
|
1930
|
-
* language boundaries (TS interfaces vs Go structs).
|
|
1931
|
-
*
|
|
1932
|
-
* @returns The schema-validator stage-4 reducer.
|
|
1933
|
-
*/
|
|
1934
|
-
export function createSchemaValidatorReducer() {
|
|
1935
|
-
return {
|
|
1936
|
-
name: 'schema-validator',
|
|
1937
|
-
stage: 'derivedReducer',
|
|
1938
|
-
getRuleIds: () => getRuleIdsFor('schema-validator'),
|
|
1939
|
-
async reduce(allFacts, context) {
|
|
1940
|
-
if (context.isScoped) {
|
|
1941
|
-
return {
|
|
1942
|
-
violations: [],
|
|
1943
|
-
facts: {},
|
|
1944
|
-
notRunReason: 'schema-validator requires a full-corpus audit (cross-language schema comparison is unavailable on scoped/diff runs)',
|
|
1945
|
-
};
|
|
1946
|
-
}
|
|
1947
|
-
const entities = clFlattenEntities(allFacts);
|
|
1948
|
-
if (entities.length === 0)
|
|
1949
|
-
return { violations: [], facts: {}, factsConsumed: 0 };
|
|
1950
|
-
try {
|
|
1951
|
-
const { SchemaValidator, extractSchemas, countCrossLanguagePairs, getUnimplementedSchemaExtractions } = await import('./analyzers/cross-language/SchemaValidator.js');
|
|
1952
|
-
const schemas = extractSchemas(entities);
|
|
1953
|
-
const unimplemented = getUnimplementedSchemaExtractions(entities);
|
|
1954
|
-
// A zero here would otherwise surface as `clean` in coverage — which
|
|
1955
|
-
// reads as "compared everything and found no mismatch" when the truth
|
|
1956
|
-
// is "there is nothing to compare". Distinguish the two: no schemas at
|
|
1957
|
-
// all, or schemas all in one language, is `notApplicable`, not `clean`.
|
|
1958
|
-
// Fold in any schema languages that are present but not extracted, so
|
|
1959
|
-
// "protobuf extraction not implemented" reaches the report rather than
|
|
1960
|
-
// silently collapsing into a zero.
|
|
1961
|
-
const gapReason = unimplemented.map(u => u.reason).join(', ');
|
|
1962
|
-
if (schemas.length === 0) {
|
|
1963
|
-
const reason = gapReason
|
|
1964
|
-
? `${gapReason} (no interfaces/structs to compare)`
|
|
1965
|
-
: 'no schema definitions found (no interfaces/structs to compare)';
|
|
1966
|
-
return { violations: [], facts: {}, notRunReason: reason };
|
|
1967
|
-
}
|
|
1968
|
-
if (countCrossLanguagePairs(schemas) === 0) {
|
|
1969
|
-
const reason = gapReason
|
|
1970
|
-
? `no cross-language pairs found (${gapReason}; schema comparison requires ≥2 languages)`
|
|
1971
|
-
: 'no cross-language pairs found (schema comparison requires ≥2 languages)';
|
|
1972
|
-
return { violations: [], facts: {}, notRunReason: reason };
|
|
1973
|
-
}
|
|
1974
|
-
const validator = new SchemaValidator();
|
|
1975
|
-
const violations = await validator.validateSchemas(schemas);
|
|
1976
|
-
return { violations, facts: {}, factsConsumed: schemas.length };
|
|
1977
|
-
}
|
|
1978
|
-
catch (e) {
|
|
1979
|
-
console.error('[schema-validator reducer] error:', e.message);
|
|
1980
|
-
return { violations: [], facts: {}, factsConsumed: 0 };
|
|
1981
|
-
}
|
|
1982
|
-
},
|
|
1983
|
-
defaultConfig: {},
|
|
1984
|
-
description: 'Validates schema consistency across language boundaries (TS interfaces vs Go structs)',
|
|
1985
|
-
category: 'cross-language',
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
|
-
// ── api-contract reducer (Stage 4) ───────────────────────────────────────────
|
|
1989
|
-
/**
|
|
1990
|
-
* Create the api-contract stage-4 reducer, which compares frontend API calls
|
|
1991
|
-
* against backend endpoints across languages.
|
|
1992
|
-
*
|
|
1993
|
-
* @returns The api-contract stage-4 reducer.
|
|
1994
|
-
*/
|
|
1995
|
-
export function createAPIContractReducer() {
|
|
1996
|
-
return {
|
|
1997
|
-
name: 'api-contract',
|
|
1998
|
-
stage: 'derivedReducer',
|
|
1999
|
-
getRuleIds: () => getRuleIdsFor('api-contract'),
|
|
2000
|
-
async reduce(allFacts, context) {
|
|
2001
|
-
if (context.isScoped) {
|
|
2002
|
-
return {
|
|
2003
|
-
violations: [],
|
|
2004
|
-
facts: {},
|
|
2005
|
-
notRunReason: 'api-contract requires a full-corpus audit (cross-language API comparison is unavailable on scoped/diff runs)',
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
const entities = clFlattenEntities(allFacts);
|
|
2009
|
-
if (entities.length === 0)
|
|
2010
|
-
return { violations: [], facts: {}, factsConsumed: 0 };
|
|
2011
|
-
try {
|
|
2012
|
-
const { APIContractAnalyzer, extractEndpoints, extractAPICalls } = await import('./analyzers/cross-language/APIContractAnalyzer.js');
|
|
2013
|
-
const endpoints = extractEndpoints(entities);
|
|
2014
|
-
const calls = extractAPICalls(entities);
|
|
2015
|
-
const analyzer = new APIContractAnalyzer();
|
|
2016
|
-
const violations = await analyzer.analyzeContracts(endpoints, calls);
|
|
2017
|
-
return { violations, facts: {}, factsConsumed: endpoints.length + calls.length };
|
|
2018
|
-
}
|
|
2019
|
-
catch (e) {
|
|
2020
|
-
console.error('[api-contract reducer] error:', e.message);
|
|
2021
|
-
return { violations: [], facts: {}, factsConsumed: 0 };
|
|
2022
|
-
}
|
|
2023
|
-
},
|
|
2024
|
-
defaultConfig: {},
|
|
2025
|
-
description: 'Validates API contracts between frontend calls and backend endpoints across languages',
|
|
2026
|
-
category: 'cross-language',
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
// ── dependency-graph reducer (Stage 4) ───────────────────────────────────────
|
|
2030
|
-
/**
|
|
2031
|
-
* Persist a `graph_cache` edge set in one transaction: delete the graph's
|
|
2032
|
-
* existing rows, then insert `rows` as (node_key, neighbor_key, weight).
|
|
2033
|
-
* Advisory — a failure here only degrades the post-pipeline hotspot ranking,
|
|
2034
|
-
* so it is swallowed rather than surfaced as an audit error.
|
|
2035
|
-
*/
|
|
2036
|
-
function persistGraphEdges(rawDb, graphType, rows) {
|
|
2037
|
-
try {
|
|
2038
|
-
const db = rawDb;
|
|
2039
|
-
const del = db.prepare(`DELETE FROM graph_cache WHERE graph_type = '${graphType}'`);
|
|
2040
|
-
const ins = db.prepare(`INSERT OR REPLACE INTO graph_cache (graph_type, node_key, neighbor_key, weight) VALUES ('${graphType}', ?, ?, ?)`);
|
|
2041
|
-
const tx = db.transaction(() => {
|
|
2042
|
-
del.run();
|
|
2043
|
-
for (const [nodeKey, neighborKey, weight] of rows) {
|
|
2044
|
-
ins.run(nodeKey, neighborKey, weight);
|
|
2045
|
-
}
|
|
2046
|
-
});
|
|
2047
|
-
tx();
|
|
2048
|
-
}
|
|
2049
|
-
catch {
|
|
2050
|
-
// Advisory persistence — ranking falls back to a neutral default without it.
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
/**
|
|
2054
|
-
* Create the dependency-graph stage-4 reducer, which detects dependency cycles,
|
|
2055
|
-
* hubs, and orphaned nodes.
|
|
2056
|
-
*
|
|
2057
|
-
* @returns The dependency-graph stage-4 reducer.
|
|
2058
|
-
*/
|
|
2059
|
-
export function createDependencyGraphReducer() {
|
|
2060
|
-
return {
|
|
2061
|
-
name: 'dependency-graph',
|
|
2062
|
-
stage: 'derivedReducer',
|
|
2063
|
-
getRuleIds: () => getRuleIdsFor('dependency-graph'),
|
|
2064
|
-
async reduce(allFacts, context) {
|
|
2065
|
-
if (context.isScoped) {
|
|
2066
|
-
return {
|
|
2067
|
-
violations: [],
|
|
2068
|
-
facts: {},
|
|
2069
|
-
notRunReason: 'dependency-graph requires a full-corpus audit (cycle/SCC/hub/orphan detection is unavailable on scoped/diff runs)',
|
|
2070
|
-
};
|
|
2071
|
-
}
|
|
2072
|
-
const entities = clFlattenEntities(allFacts);
|
|
2073
|
-
if (entities.length === 0)
|
|
2074
|
-
return { violations: [], facts: {}, factsConsumed: 0 };
|
|
2075
|
-
try {
|
|
2076
|
-
const { DependencyGraphBuilder } = await import('./analyzers/cross-language/DependencyGraphBuilder.js');
|
|
2077
|
-
const builder = new DependencyGraphBuilder({ includeTestFiles: false });
|
|
2078
|
-
const references = clBuildReferences(entities);
|
|
2079
|
-
const graph = await builder.buildGraph(entities, references);
|
|
2080
|
-
const health = await builder.analyzeDependencyHealth(graph);
|
|
2081
|
-
const idToEntity = new Map(entities.map((e) => [e.id, e]));
|
|
2082
|
-
const violations = [];
|
|
2083
|
-
const diagnostics = [];
|
|
2084
|
-
for (const issue of health.issues) {
|
|
2085
|
-
if (issue.type === 'orphaned-nodes') {
|
|
2086
|
-
// Emit one violation per orphan so each is attributed to its own
|
|
2087
|
-
// file/line. The aggregated form anchored every orphan to the first
|
|
2088
|
-
// node's location while the message listed all of them — foreign
|
|
2089
|
-
// symbols leaked into the wrong file.
|
|
2090
|
-
for (const id of issue.affectedNodes) {
|
|
2091
|
-
const orphan = idToEntity.get(id);
|
|
2092
|
-
if (!orphan)
|
|
2093
|
-
continue;
|
|
2094
|
-
violations.push({
|
|
2095
|
-
file: orphan.file,
|
|
2096
|
-
line: orphan.startLine,
|
|
2097
|
-
severity: issue.severity,
|
|
2098
|
-
message: `Orphaned node "${orphan.name}" has no connections.`,
|
|
2099
|
-
rule: issue.type,
|
|
2100
|
-
type: issue.type, // dependency-graph rules match on field: 'type'
|
|
2101
|
-
analyzer: 'dependency-graph',
|
|
2102
|
-
category: 'cross-language-dependency',
|
|
2103
|
-
symbol: orphan.name,
|
|
2104
|
-
details: { orphanId: id },
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
const anchor = issue.affectedNodes.map((id) => idToEntity.get(id)).find(Boolean);
|
|
2110
|
-
violations.push({
|
|
2111
|
-
file: anchor?.file ?? '(unknown)',
|
|
2112
|
-
line: anchor?.startLine ?? 0,
|
|
2113
|
-
severity: issue.severity,
|
|
2114
|
-
message: issue.description,
|
|
2115
|
-
rule: issue.type,
|
|
2116
|
-
type: issue.type, // dependency-graph rules match on field: 'type'
|
|
2117
|
-
analyzer: 'dependency-graph',
|
|
2118
|
-
category: 'cross-language-dependency',
|
|
2119
|
-
details: issue.details,
|
|
2120
|
-
});
|
|
2121
|
-
}
|
|
2122
|
-
// ── File-level reachability: unreferenced modules + ranking axis ──
|
|
2123
|
-
const fileFacts = clFileFacts(allFacts);
|
|
2124
|
-
let testCoverage;
|
|
2125
|
-
let deadClusters = [];
|
|
2126
|
-
if (fileFacts.size > 0) {
|
|
2127
|
-
// `context.config` is the reducer's merged config (namespace + `_infra`);
|
|
2128
|
-
// a bare reducer context in a unit test omits it, so fall back to the
|
|
2129
|
-
// file facts themselves as the corpus and a no-alias classification.
|
|
2130
|
-
const infraConfig = (context.config ?? {});
|
|
2131
|
-
const packageEntrySet = new Set(infraConfig.packageEntryPoints ?? []);
|
|
2132
|
-
const { reachability, importersOf } = clComputeReachability(fileFacts, {
|
|
2133
|
-
corpusFiles: new Set(infraConfig.corpusFiles ?? [...fileFacts.keys()]),
|
|
2134
|
-
virtualModules: infraConfig.importVirtualModules ?? DEFAULT_VIRTUAL_MODULES,
|
|
2135
|
-
tsconfigAliases: infraConfig.tsconfigAliases,
|
|
2136
|
-
packageEntryPoints: packageEntrySet,
|
|
2137
|
-
projectRoot: context.projectRoot ?? '',
|
|
2138
|
-
});
|
|
2139
|
-
// Persist reachability to graph_cache so the post-pipeline reorder
|
|
2140
|
-
// (auditRunner hotspot block) can rank every finding by live/dead.
|
|
2141
|
-
if (context.indexHandle?.rawDb) {
|
|
2142
|
-
const reachabilityRows = [...reachability].map(([fp, score]) => [fp, '', score]);
|
|
2143
|
-
persistGraphEdges(context.indexHandle.rawDb, 'reachability', reachabilityRows);
|
|
2144
|
-
}
|
|
2145
|
-
// Spec 60 R1 — persist the file-level import edges (reverse adjacency:
|
|
2146
|
-
// module → its direct importers) so "what tests exercise this module"
|
|
2147
|
-
// is answerable downstream from `importersOf`, not the symbol-level
|
|
2148
|
-
// `function_dependencies` table.
|
|
2149
|
-
if (context.indexHandle?.rawDb) {
|
|
2150
|
-
const importerRows = [];
|
|
2151
|
-
for (const [fp, importers] of importersOf) {
|
|
2152
|
-
for (const importer of importers) {
|
|
2153
|
-
importerRows.push([fp, importer, 1]);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
persistGraphEdges(context.indexHandle.rawDb, 'importers', importerRows);
|
|
2157
|
-
}
|
|
2158
|
-
// Flag files that export symbols yet are imported by nothing and are
|
|
2159
|
-
// not framework entry points — a dead module. This is the signal the
|
|
2160
|
-
// orphaned-nodes check missed: a whole unreferenced file whose every
|
|
2161
|
-
// internal symbol still forms a connected component.
|
|
2162
|
-
const deadModuleFiles = [];
|
|
2163
|
-
for (const [fp, info] of fileFacts) {
|
|
2164
|
-
if (!info.hasExports)
|
|
2165
|
-
continue;
|
|
2166
|
-
if (clIsTestFile(fp))
|
|
2167
|
-
continue;
|
|
2168
|
-
if (clIsEntryPointFile(fp) || packageEntrySet.has(fp))
|
|
2169
|
-
continue;
|
|
2170
|
-
if ((importersOf.get(fp)?.size ?? 0) > 0)
|
|
2171
|
-
continue;
|
|
2172
|
-
deadModuleFiles.push(fp);
|
|
2173
|
-
violations.push({
|
|
2174
|
-
file: fp,
|
|
2175
|
-
line: 1,
|
|
2176
|
-
// A module that exports and is imported by nothing is a genuine
|
|
2177
|
-
// dead-code signal — anchored severe. Validated across four corpora.
|
|
2178
|
-
severity: 'severe',
|
|
2179
|
-
message: 'Module is not imported by any other file and is not a framework entry point — dead code candidate.',
|
|
2180
|
-
rule: 'unreferenced-module',
|
|
2181
|
-
type: 'unreferenced-module',
|
|
2182
|
-
analyzer: 'dependency-graph',
|
|
2183
|
-
category: 'cross-language-dependency',
|
|
2184
|
-
details: { imports: info.imports },
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
// Spec 60 R1 — classify each non-test module off the import edge set.
|
|
2188
|
-
testCoverage = classifyTestCoverage(fileFacts, importersOf, packageEntrySet);
|
|
2189
|
-
// Spec 60 R3 — cluster the unreferenced modules by basename so the
|
|
2190
|
-
// "dead-and-duplicated" pattern (same file name in sibling dirs) is
|
|
2191
|
-
// visible as a cluster rather than scattered one-per-file findings.
|
|
2192
|
-
deadClusters = clusterDeadModules(deadModuleFiles);
|
|
2193
|
-
// Spec 58 R2 — computed-specifier dynamic imports. `import(someVar)`
|
|
2194
|
-
// cannot be resolved into an edge, so any module in this corpus could
|
|
2195
|
-
// be its target and `unreferenced-module` is therefore not exhaustive.
|
|
2196
|
-
// This is a coverage diagnostic (the analyzer can't resolve the target),
|
|
2197
|
-
// not a finding (the code isn't wrong) — surfaced, not gated.
|
|
2198
|
-
for (const [fp, info] of fileFacts) {
|
|
2199
|
-
for (const dyn of info.unresolvedDynamicImports) {
|
|
2200
|
-
diagnostics.push({
|
|
2201
|
-
analyzerName: 'dependency-graph',
|
|
2202
|
-
kind: 'unresolved-dynamic-import',
|
|
2203
|
-
message: `Dynamic import/require has a computed specifier (${dyn.expression || '…'}) that cannot be resolved — its target module is unknown, so unreferenced-module may miss a live dependency.`,
|
|
2204
|
-
file: fp,
|
|
2205
|
-
line: dyn.line,
|
|
2206
|
-
details: { expression: dyn.expression },
|
|
2207
|
-
});
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
const HEALTH_SEVERITY = {
|
|
2212
|
-
'break-cycles': 'severe',
|
|
2213
|
-
'reduce-coupling': 'high',
|
|
2214
|
-
'review-orphans': 'severe',
|
|
2215
|
-
};
|
|
2216
|
-
for (const s of health.suggestions) {
|
|
2217
|
-
violations.push({
|
|
2218
|
-
file: '(multiple)',
|
|
2219
|
-
line: 0,
|
|
2220
|
-
severity: HEALTH_SEVERITY[s.type] ?? 'high',
|
|
2221
|
-
message: s.description,
|
|
2222
|
-
rule: s.type,
|
|
2223
|
-
type: s.type,
|
|
2224
|
-
analyzer: 'dependency-graph',
|
|
2225
|
-
category: 'cross-language-dependency',
|
|
2226
|
-
details: s.affectedNodes ? { affectedNodes: s.affectedNodes } : undefined,
|
|
2227
|
-
});
|
|
2228
|
-
}
|
|
2229
|
-
const facts = {};
|
|
2230
|
-
if (testCoverage)
|
|
2231
|
-
facts.testCoverage = testCoverage;
|
|
2232
|
-
if (deadClusters.length > 0)
|
|
2233
|
-
facts.deadClusters = deadClusters;
|
|
2234
|
-
return { violations, facts, factsConsumed: entities.length, ...(diagnostics.length > 0 && { diagnostics }) };
|
|
2235
|
-
}
|
|
2236
|
-
catch (e) {
|
|
2237
|
-
console.error('[dependency-graph reducer] error:', e.message);
|
|
2238
|
-
return { violations: [], facts: {}, factsConsumed: 0 };
|
|
2239
|
-
}
|
|
2240
|
-
},
|
|
2241
|
-
defaultConfig: {},
|
|
2242
|
-
description: 'Builds a cross-language dependency graph and detects cycles, hubs, orphans, and tight coupling',
|
|
2243
|
-
category: 'cross-language',
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
1169
|
// ── Invariants reducer (stage 3) ──────────────────────────────────────────────
|
|
2247
1170
|
/**
|
|
2248
1171
|
* Invariants reducer — enforces user-defined invariant rules from .codeauditor.json.
|
|
@@ -2329,41 +1252,6 @@ async function _getProvenanceModule() {
|
|
|
2329
1252
|
}
|
|
2330
1253
|
return _provenanceModule;
|
|
2331
1254
|
}
|
|
2332
|
-
/**
|
|
2333
|
-
* Schema SQL visitor (.sql files) — extracts ordered DDL operations for the
|
|
2334
|
-
* Stage 3 reducer to replay into the known-tables catalog. Emits the parsed
|
|
2335
|
-
* ops, not raw source, so the reducer retains only state transitions.
|
|
2336
|
-
*
|
|
2337
|
-
* @returns The schema-sql stage-2 visitor.
|
|
2338
|
-
*/
|
|
2339
|
-
export function createSchemaSqlVisitor() {
|
|
2340
|
-
const getMigrationExtractor = lazySingleton(() => import('./analyzers/universal/UniversalSchemaAnalyzer.js'));
|
|
2341
|
-
return {
|
|
2342
|
-
name: 'schema-sql',
|
|
2343
|
-
stage: 'visitor',
|
|
2344
|
-
extensions: ['.sql'],
|
|
2345
|
-
getRuleIds: () => [],
|
|
2346
|
-
async visit(_ast, _adapter, context, sourceCode) {
|
|
2347
|
-
const { extractMigrationOpsFromFile } = await getMigrationExtractor();
|
|
2348
|
-
const dialect = context.config.sqlDialect;
|
|
2349
|
-
const { ops, columns, tableColumns, skipped, bytes } = await extractMigrationOpsFromFile(context.filePath, sourceCode, dialect ?? null);
|
|
2350
|
-
return {
|
|
2351
|
-
violations: [],
|
|
2352
|
-
facts: {
|
|
2353
|
-
[context.filePath]: {
|
|
2354
|
-
ddlOps: ops,
|
|
2355
|
-
...(columns.length > 0 && { ddlColumns: columns }),
|
|
2356
|
-
...(Object.keys(tableColumns).length > 0 && { ddlTableColumns: tableColumns }),
|
|
2357
|
-
...(skipped && { skipped: true, bytes }),
|
|
2358
|
-
},
|
|
2359
|
-
},
|
|
2360
|
-
};
|
|
2361
|
-
},
|
|
2362
|
-
defaultConfig: {},
|
|
2363
|
-
description: 'Extracts SQL DDL operations for known-table catalog',
|
|
2364
|
-
category: 'database',
|
|
2365
|
-
};
|
|
2366
|
-
}
|
|
2367
1255
|
/** The shared lazy `UniversalSchemaAnalyzer` for the schema-code visitor. Hoisted
|
|
2368
1256
|
* to module scope so the factory below stays a thin descriptor and the analyzer
|
|
2369
1257
|
* is constructed once, not per factory call. */
|
|
@@ -2540,75 +1428,6 @@ export function createSchemaCodeVisitor() {
|
|
|
2540
1428
|
category: 'database',
|
|
2541
1429
|
};
|
|
2542
1430
|
}
|
|
2543
|
-
/**
|
|
2544
|
-
* Schema Prisma visitor (.prisma files) — extracts model names for known-table catalog.
|
|
2545
|
-
*
|
|
2546
|
-
* @returns The schema-prisma stage-2 visitor.
|
|
2547
|
-
*/
|
|
2548
|
-
export function createSchemaPrismaVisitor() {
|
|
2549
|
-
return {
|
|
2550
|
-
name: 'schema-prisma',
|
|
2551
|
-
stage: 'visitor',
|
|
2552
|
-
extensions: ['.prisma'],
|
|
2553
|
-
getRuleIds: () => [],
|
|
2554
|
-
async visit(_ast, _adapter, context, sourceCode) {
|
|
2555
|
-
const models = [];
|
|
2556
|
-
const modelRe = /model\s+(\w+)\s*\{/g;
|
|
2557
|
-
let m;
|
|
2558
|
-
while ((m = modelRe.exec(sourceCode)) !== null) {
|
|
2559
|
-
const modelName = m[1];
|
|
2560
|
-
// Scope @@map to this model's block by matching braces.
|
|
2561
|
-
// Depth starts at 1 for the opening { already consumed by the regex.
|
|
2562
|
-
const blockStart = m.index + m[0].length;
|
|
2563
|
-
let depth = 1;
|
|
2564
|
-
let blockEnd = blockStart;
|
|
2565
|
-
for (; blockEnd < sourceCode.length && depth > 0; blockEnd++) {
|
|
2566
|
-
if (sourceCode[blockEnd] === '{')
|
|
2567
|
-
depth++;
|
|
2568
|
-
else if (sourceCode[blockEnd] === '}')
|
|
2569
|
-
depth--;
|
|
2570
|
-
}
|
|
2571
|
-
const blockContent = sourceCode.slice(blockStart, blockEnd - 1);
|
|
2572
|
-
// @@map (block-level, double @) renames the table. @map (single @)
|
|
2573
|
-
// is field-level — column rename — and must not be treated as a table name.
|
|
2574
|
-
const mapMatch = blockContent.match(/@@map\s*\(\s*"([^"]+)"\s*\)/);
|
|
2575
|
-
models.push(mapMatch ? mapMatch[1] : modelName);
|
|
2576
|
-
}
|
|
2577
|
-
return {
|
|
2578
|
-
violations: [],
|
|
2579
|
-
facts: { [context.filePath]: { prismaModels: models } },
|
|
2580
|
-
};
|
|
2581
|
-
},
|
|
2582
|
-
defaultConfig: {},
|
|
2583
|
-
description: 'Extracts Prisma model names for known-table catalog',
|
|
2584
|
-
category: 'database',
|
|
2585
|
-
};
|
|
2586
|
-
}
|
|
2587
|
-
/**
|
|
2588
|
-
* Schema JSON visitor (.json files) — parses JSON for the Stage 3 reducer to validate.
|
|
2589
|
-
*
|
|
2590
|
-
* @returns The schema-json stage-2 visitor.
|
|
2591
|
-
*/
|
|
2592
|
-
export function createSchemaJsonVisitor() {
|
|
2593
|
-
return {
|
|
2594
|
-
name: 'schema-json',
|
|
2595
|
-
stage: 'visitor',
|
|
2596
|
-
extensions: ['.json'],
|
|
2597
|
-
getRuleIds: () => [],
|
|
2598
|
-
async visit(_ast, _adapter, context, _sourceCode) {
|
|
2599
|
-
// Emit only a lightweight marker — the Stage 3 reducer reads and parses
|
|
2600
|
-
// each JSON file on demand via context.readSource, so parsed objects are
|
|
2601
|
-
// transient rather than retained in allFacts through stage 4.
|
|
2602
|
-
return {
|
|
2603
|
-
violations: [],
|
|
2604
|
-
facts: { [context.filePath]: { isJson: true } },
|
|
2605
|
-
};
|
|
2606
|
-
},
|
|
2607
|
-
defaultConfig: {},
|
|
2608
|
-
description: 'Parses JSON files for schema validation',
|
|
2609
|
-
category: 'database',
|
|
2610
|
-
};
|
|
2611
|
-
}
|
|
2612
1431
|
/**
|
|
2613
1432
|
* Flatten `allFacts` (keyed by visitor name, each value `{ [filePath]: facts }`)
|
|
2614
1433
|
* into per-file `[filePath, fact]` pairs.
|
|
@@ -2925,57 +1744,4 @@ function validateJsonSchemas(opts) {
|
|
|
2925
1744
|
}
|
|
2926
1745
|
return violations;
|
|
2927
1746
|
}
|
|
2928
|
-
/**
|
|
2929
|
-
* Schema Stage 3 reducer — cross-file unknown-table detection and JSON schema validation.
|
|
2930
|
-
*
|
|
2931
|
-
* Consumes facts from schema-sql, schema-code, schema-prisma, and schema-json visitors.
|
|
2932
|
-
* Builds the complete known-tables catalog and checks all table references against it.
|
|
2933
|
-
*
|
|
2934
|
-
* @returns The schema stage-3 reducer.
|
|
2935
|
-
*/
|
|
2936
|
-
export function createSchemaReducer() {
|
|
2937
|
-
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSchemaAnalyzer.js').then((m) => ({
|
|
2938
|
-
analyzer: new m.UniversalSchemaAnalyzer(),
|
|
2939
|
-
defaults: m.DEFAULT_SCHEMA_CONFIG,
|
|
2940
|
-
analyzeJsonSchemas: m.analyzeJsonSchemas,
|
|
2941
|
-
})));
|
|
2942
|
-
return {
|
|
2943
|
-
name: 'schema',
|
|
2944
|
-
stage: 'reducer',
|
|
2945
|
-
consumes: [],
|
|
2946
|
-
getRuleIds: () => getRuleIdsFor('schema'),
|
|
2947
|
-
async reduce(allFacts, context) {
|
|
2948
|
-
const { analyzer: a, analyzeJsonSchemas } = await getAnalyzer();
|
|
2949
|
-
const violations = [];
|
|
2950
|
-
const schemaConfig = (context.config ?? {});
|
|
2951
|
-
const { knownTables, tableProvenances, dropProvenance, ddlColumns, tableColumns } = buildSchemaCatalog(allFacts);
|
|
2952
|
-
violations.push(...detectUnknownTables({
|
|
2953
|
-
knownTables,
|
|
2954
|
-
tableProvenances,
|
|
2955
|
-
dropProvenance,
|
|
2956
|
-
allFacts,
|
|
2957
|
-
schemaConfig,
|
|
2958
|
-
}));
|
|
2959
|
-
violations.push(...validateJsonSchemas({
|
|
2960
|
-
schemaConfig,
|
|
2961
|
-
allFacts,
|
|
2962
|
-
context,
|
|
2963
|
-
analyzeJsonSchemas,
|
|
2964
|
-
}));
|
|
2965
|
-
// ── 4. Build table catalog for metadata ───────────────────────────────
|
|
2966
|
-
const catalogEntries = [];
|
|
2967
|
-
for (const [table, sources] of tableProvenances) {
|
|
2968
|
-
catalogEntries.push({ table, sources });
|
|
2969
|
-
}
|
|
2970
|
-
return {
|
|
2971
|
-
violations,
|
|
2972
|
-
facts: { tableCatalog: catalogEntries, ddlColumns: [...ddlColumns].sort(), tableColumns },
|
|
2973
|
-
factsConsumed: [...iteratePerFileFacts(allFacts)].length,
|
|
2974
|
-
};
|
|
2975
|
-
},
|
|
2976
|
-
defaultConfig: {},
|
|
2977
|
-
description: 'Cross-file schema analysis: unknown-table detection and JSON schema validation',
|
|
2978
|
-
category: 'database',
|
|
2979
|
-
};
|
|
2980
|
-
}
|
|
2981
1747
|
//# sourceMappingURL=pipelineAdapters.js.map
|