octocode-cli 1.2.7 → 1.2.9
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/README.md +42 -35
- package/out/octocode-cli.js +36 -11719
- package/package.json +36 -36
- package/skills/README.md +42 -114
- package/skills/{octocode-code-engineer → octocode-engineer}/.claude/settings.local.json +2 -1
- package/skills/octocode-engineer/README.md +99 -0
- package/skills/octocode-engineer/SKILL.md +499 -0
- package/skills/octocode-engineer/build.mjs +29 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/eslint.config.mjs +3 -13
- package/skills/{octocode-code-engineer → octocode-engineer}/package.json +28 -27
- package/skills/octocode-engineer/references/ast-reference.md +166 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/references/cli-reference.md +80 -6
- package/skills/octocode-engineer/references/externals.md +86 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/references/output-files.md +46 -6
- package/skills/octocode-engineer/references/quality-indicators.md +202 -0
- package/skills/octocode-engineer/references/tool-workflows.md +298 -0
- package/skills/octocode-engineer/references/validation-playbooks.md +99 -0
- package/skills/octocode-engineer/scripts/ast/search.js +45 -0
- package/skills/octocode-engineer/scripts/ast/tree-search.js +27 -0
- package/skills/octocode-engineer/scripts/index.js +173 -0
- package/skills/octocode-engineer/scripts/run.js +179 -0
- package/skills/octocode-engineer/src/analysis/dependencies.ts +378 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.test.ts +57 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.ts +43 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.test.ts +113 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.ts +64 -1
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.test.ts +118 -2
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.ts +65 -3
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.test.ts +281 -1
- package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.ts +173 -3
- package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.test.ts +73 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.ts +62 -4
- package/skills/octocode-engineer/src/detector-gating.test.ts +59 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/code-quality.ts +342 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.ts +8 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/index.test.ts +565 -11
- package/skills/octocode-engineer/src/index.ts +468 -0
- package/skills/octocode-engineer/src/pipeline/affected.test.ts +147 -0
- package/skills/octocode-engineer/src/pipeline/affected.ts +68 -0
- package/skills/octocode-engineer/src/pipeline/baseline.test.ts +276 -0
- package/skills/octocode-engineer/src/pipeline/baseline.ts +76 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.test.ts +300 -53
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.ts +180 -36
- package/skills/octocode-engineer/src/pipeline/config-loader.test.ts +264 -0
- package/skills/octocode-engineer/src/pipeline/config-loader.ts +109 -0
- package/skills/octocode-engineer/src/pipeline/create-options.ts +55 -0
- package/skills/octocode-engineer/src/pipeline/health-score.test.ts +65 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.ts +130 -17
- package/skills/octocode-engineer/src/pipeline/progress.ts +51 -0
- package/skills/octocode-engineer/src/pipeline/reporters.test.ts +155 -0
- package/skills/octocode-engineer/src/pipeline/reporters.ts +64 -0
- package/skills/octocode-engineer/src/reporting/graph-features.test.ts +279 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/output-contract.test.ts +6 -0
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +1066 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +1604 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/writer.ts +136 -13
- package/skills/octocode-engineer/src/run.ts +78 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/sanity.test.ts +1 -1
- package/skills/octocode-engineer/src/types/analysis.ts +25 -0
- package/skills/octocode-engineer/src/types/collectors.ts +134 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/types/constants.ts +75 -41
- package/skills/octocode-engineer/src/types/core.ts +203 -0
- package/skills/octocode-engineer/src/types/dependency.ts +215 -0
- package/skills/octocode-engineer/src/types/file-entry.ts +108 -0
- package/skills/octocode-engineer/src/types/findings.ts +105 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/src/types/index.ts +60 -30
- package/skills/octocode-engineer/src/types/tree-sitter.ts +38 -0
- package/skills/{octocode-code-engineer → octocode-engineer}/tsconfig.json +1 -0
- package/skills/octocode-research/.octocode/scan/.cache/analysis-cache.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/architecture.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/ast-trees.txt +5566 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/code-quality.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/dead-code.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/file-inventory.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/findings.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/graph.md +189 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/security.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.md +265 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/architecture.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/ast-trees.txt +5555 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/code-quality.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/dead-code.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/file-inventory.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/findings.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/graph.md +190 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/security.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.json +1 -0
- package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.md +265 -0
- package/skills/octocode-research/CHANGELOG.md +60 -0
- package/skills/octocode-research/README.md +102 -388
- package/skills/octocode-research/SKILL.md +169 -498
- package/skills/octocode-research/package.json +19 -31
- package/skills/octocode-research/references/PARALLEL_AGENT_PROTOCOL.md +19 -0
- package/skills/octocode-research/references/SESSION_MANAGEMENT.md +38 -0
- package/skills/octocode-research/scripts/server-init.js +1 -1
- package/skills/octocode-research/scripts/server.d.ts +2 -1
- package/skills/octocode-research/scripts/server.js +329 -233
- package/skills/octocode-research/src/__tests__/integration/promptsRoutes.test.ts +180 -0
- package/skills/octocode-research/src/__tests__/integration/serverHttp.test.ts +221 -0
- package/skills/octocode-research/src/__tests__/integration/serverLifecycle.test.ts +194 -0
- package/skills/octocode-research/src/__tests__/integration/toolsRoutes.test.ts +501 -0
- package/skills/octocode-research/src/__tests__/unit/readiness.test.ts +61 -0
- package/skills/octocode-research/src/__tests__/unit/resilience.test.ts +192 -0
- package/skills/octocode-research/src/__tests__/unit/responseFactory.test.ts +172 -0
- package/skills/octocode-research/src/__tests__/unit/responseParser.test.ts +288 -0
- package/skills/octocode-research/src/__tests__/unit/schemas.test.ts +509 -0
- package/skills/octocode-research/src/index.ts +4 -124
- package/skills/octocode-research/src/middleware/queryParser.ts +0 -26
- package/skills/octocode-research/src/routes/lsp.ts +58 -59
- package/skills/octocode-research/src/routes/package.ts +35 -65
- package/skills/octocode-research/src/routes/prompts.ts +3 -3
- package/skills/octocode-research/src/routes/tools.ts +8 -20
- package/skills/octocode-research/src/server-init.ts +30 -237
- package/skills/octocode-research/src/server.ts +50 -23
- package/skills/octocode-research/src/types/errorGuards.ts +9 -80
- package/skills/octocode-research/src/types/guards.ts +0 -28
- package/skills/octocode-research/src/types/mcp.ts +11 -66
- package/skills/octocode-research/src/types/responses.ts +11 -129
- package/skills/octocode-research/src/utils/circuitBreaker.ts +0 -21
- package/skills/octocode-research/src/utils/logger.ts +1 -97
- package/skills/octocode-research/src/utils/resilience.ts +2 -12
- package/skills/octocode-research/src/utils/responseFactory.ts +0 -42
- package/skills/octocode-research/src/utils/responseParser.ts +3 -25
- package/skills/octocode-research/src/utils/retry.ts +0 -63
- package/skills/octocode-research/src/utils/routeFactory.ts +1 -1
- package/skills/octocode-research/src/validation/httpPreprocess.ts +0 -3
- package/skills/octocode-research/src/validation/index.ts +0 -1
- package/skills/octocode-research/src/validation/schemas.ts +0 -63
- package/skills/octocode-research/src/validation/toolCallSchema.ts +3 -3
- package/skills/octocode-research/tsdown.config.ts +4 -0
- package/skills/octocode-research/vitest.config.ts +3 -0
- package/skills/octocode-code-engineer/.plan/VALIDATED_PLAN.md +0 -223
- package/skills/octocode-code-engineer/README.md +0 -178
- package/skills/octocode-code-engineer/SKILL.md +0 -418
- package/skills/octocode-code-engineer/minify-scripts.mjs +0 -32
- package/skills/octocode-code-engineer/references/agent-ast-reading-rfc.md +0 -95
- package/skills/octocode-code-engineer/references/architecture-techniques.md +0 -121
- package/skills/octocode-code-engineer/references/ast-search.md +0 -210
- package/skills/octocode-code-engineer/references/ast-tree-search.md +0 -151
- package/skills/octocode-code-engineer/references/concepts.md +0 -107
- package/skills/octocode-code-engineer/references/finding-categories.md +0 -128
- package/skills/octocode-code-engineer/references/improvement-roadmap.md +0 -304
- package/skills/octocode-code-engineer/references/playbooks.md +0 -204
- package/skills/octocode-code-engineer/references/present-results.md +0 -136
- package/skills/octocode-code-engineer/references/tool-workflows.md +0 -566
- package/skills/octocode-code-engineer/references/validate-investigate.md +0 -225
- package/skills/octocode-code-engineer/scripts/analysis/dependencies.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/dependency-summary.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/discovery.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/graph-analytics.js +0 -1
- package/skills/octocode-code-engineer/scripts/analysis/semantic.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/helpers.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/metrics.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/search.js +0 -2
- package/skills/octocode-code-engineer/scripts/ast/tree-search.js +0 -2
- package/skills/octocode-code-engineer/scripts/ast/tree-sitter.js +0 -1
- package/skills/octocode-code-engineer/scripts/ast/ts-analyzer.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/chains.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/effects.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/input-sources.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/performance.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/prototype-pollution.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/security.js +0 -1
- package/skills/octocode-code-engineer/scripts/collectors/test-profile.js +0 -1
- package/skills/octocode-code-engineer/scripts/common/is-direct-run.js +0 -1
- package/skills/octocode-code-engineer/scripts/common/utils.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/code-quality.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/cohesion.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/coupling.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/cycle.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/dead-code.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/import-style.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/security.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/semantic.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/shared.js +0 -1
- package/skills/octocode-code-engineer/scripts/detectors/test-quality.js +0 -1
- package/skills/octocode-code-engineer/scripts/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/cache.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/cli.js +0 -1
- package/skills/octocode-code-engineer/scripts/pipeline/main.js +0 -2
- package/skills/octocode-code-engineer/scripts/reporting/analysis.js +0 -1
- package/skills/octocode-code-engineer/scripts/reporting/summary-md.js +0 -1
- package/skills/octocode-code-engineer/scripts/reporting/writer.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/constants.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/index.js +0 -1
- package/skills/octocode-code-engineer/scripts/types/interfaces.js +0 -1
- package/skills/octocode-code-engineer/src/analysis/dependencies.ts +0 -406
- package/skills/octocode-code-engineer/src/index.ts +0 -403
- package/skills/octocode-code-engineer/src/reporting/summary-md.test.ts +0 -421
- package/skills/octocode-code-engineer/src/reporting/summary-md.ts +0 -714
- package/skills/octocode-code-engineer/src/types/interfaces.ts +0 -682
- package/skills/octocode-research/src/types/toolTypes.ts +0 -33
- package/skills/octocode-research/src/utils/logEmoji.ts +0 -103
- /package/skills/{octocode-code-engineer → octocode-engineer}/.octocode/rfc/RFC-code-engineer-weakness-fixes.md +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/architecture.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-helpers.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-search.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/base.css +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/block-navigation.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cache.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cli.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/clover.xml +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-effects.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-input-sources.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-performance.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-prototype-pollution.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-security.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-test-profile.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/coverage-final.json +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependencies.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependency-summary.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/discovery.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/favicon.png +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/graph-analytics.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/metrics.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/pipeline.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.css +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-analysis.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-writer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/security-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sort-arrow-sprite.png +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sorter.js +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/summary-md.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/test-quality-detectors.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/tree-sitter-analyzer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ts-analyzer.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/types.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/utils.ts.html +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependencies.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/chains.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cohesion.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/coupling.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cycle.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/dead-code.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/import-style.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/semantic.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/shared.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.test.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.ts +0 -0
- /package/skills/{octocode-code-engineer → octocode-engineer}/vitest.config.ts +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { AnalysisOptions } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
type ConfigOverrides = Partial<Omit<AnalysisOptions, 'root' | 'packageRoot' | 'clearCache'>>;
|
|
7
|
+
|
|
8
|
+
const CONFIG_NAMES = ['.octocode-scan.json', '.octocode-scan.jsonc'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loads config from file, auto-discovered config, or package.json#octocode.
|
|
12
|
+
* CLI flags always win over config file values.
|
|
13
|
+
* Inspired by knip's .knip.json and eslint's flat config.
|
|
14
|
+
*/
|
|
15
|
+
export function loadConfigFile(
|
|
16
|
+
root: string,
|
|
17
|
+
explicitPath: string | null
|
|
18
|
+
): ConfigOverrides | null {
|
|
19
|
+
if (explicitPath) {
|
|
20
|
+
const abs = path.isAbsolute(explicitPath)
|
|
21
|
+
? explicitPath
|
|
22
|
+
: path.resolve(root, explicitPath);
|
|
23
|
+
return readJsonConfig(abs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const name of CONFIG_NAMES) {
|
|
27
|
+
const candidate = path.join(root, name);
|
|
28
|
+
if (fs.existsSync(candidate)) {
|
|
29
|
+
return readJsonConfig(candidate);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pkgJsonPath = path.join(root, 'package.json');
|
|
34
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
37
|
+
if (pkg.octocode && typeof pkg.octocode === 'object') {
|
|
38
|
+
return normalizeConfig(pkg.octocode);
|
|
39
|
+
}
|
|
40
|
+
} catch { /* skip */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJsonConfig(filePath: string): ConfigOverrides | null {
|
|
47
|
+
try {
|
|
48
|
+
let raw = fs.readFileSync(filePath, 'utf8');
|
|
49
|
+
raw = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
50
|
+
return normalizeConfig(JSON.parse(raw));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeConfig(obj: Record<string, unknown>): ConfigOverrides {
|
|
57
|
+
const result: Record<string, unknown> = {};
|
|
58
|
+
|
|
59
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
60
|
+
const camelKey = key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
61
|
+
|
|
62
|
+
if (camelKey === 'features' && typeof value === 'string') {
|
|
63
|
+
result[camelKey] = new Set(value.split(',').map(s => s.trim()));
|
|
64
|
+
} else if (camelKey === 'scope' && typeof value === 'string') {
|
|
65
|
+
result[camelKey] = value.split(',').map(s => s.trim());
|
|
66
|
+
} else if (camelKey === 'ignoreDirs' && Array.isArray(value)) {
|
|
67
|
+
result[camelKey] = new Set(value as string[]);
|
|
68
|
+
} else if (camelKey === 'thresholds' && typeof value === 'object' && value !== null) {
|
|
69
|
+
result[camelKey] = value;
|
|
70
|
+
} else {
|
|
71
|
+
result[camelKey] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result as ConfigOverrides;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Merges config file overrides into defaults, then CLI overrides on top.
|
|
80
|
+
* CLI args that differ from defaults always win.
|
|
81
|
+
*/
|
|
82
|
+
export function mergeConfigIntoDefaults(
|
|
83
|
+
defaults: AnalysisOptions,
|
|
84
|
+
config: ConfigOverrides,
|
|
85
|
+
cliArgs: AnalysisOptions
|
|
86
|
+
): AnalysisOptions {
|
|
87
|
+
const merged = { ...defaults };
|
|
88
|
+
|
|
89
|
+
for (const [key, value] of Object.entries(config)) {
|
|
90
|
+
if (key === 'thresholds' && typeof value === 'object' && value !== null) {
|
|
91
|
+
merged.thresholds = {
|
|
92
|
+
...merged.thresholds,
|
|
93
|
+
...(value as unknown as Record<string, number>),
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
(merged as Record<string, unknown>)[key] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const key of Object.keys(cliArgs)) {
|
|
101
|
+
const cliVal = (cliArgs as unknown as Record<string, unknown>)[key];
|
|
102
|
+
const defVal = (defaults as unknown as Record<string, unknown>)[key];
|
|
103
|
+
if (cliVal !== defVal) {
|
|
104
|
+
(merged as unknown as Record<string, unknown>)[key] = cliVal;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return merged;
|
|
109
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { ALL_CATEGORIES, PILLAR_CATEGORIES } from '../types/index.js';
|
|
4
|
+
|
|
5
|
+
import type { AnalysisOptions } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
export interface CreateOptionsInput {
|
|
8
|
+
args: AnalysisOptions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Transforms raw parsed CLI args into validated, normalized runtime options.
|
|
13
|
+
* This is Layer 2 in the 3-layer CLI pattern (args → options → engine).
|
|
14
|
+
*
|
|
15
|
+
* Responsible for:
|
|
16
|
+
* - Deriving computed fields (packageRoot)
|
|
17
|
+
* - Auto-enabling flags based on feature selection (test-quality → includeTests)
|
|
18
|
+
*
|
|
19
|
+
* Inspired by knip's create-options.ts, eslint's translate-cli-options.js,
|
|
20
|
+
* and dependency-cruiser's normalize-cli-options.mjs.
|
|
21
|
+
*/
|
|
22
|
+
export function createOptions({ args }: CreateOptionsInput): AnalysisOptions {
|
|
23
|
+
const opts = { ...args };
|
|
24
|
+
|
|
25
|
+
opts.packageRoot = path.join(opts.root, 'packages');
|
|
26
|
+
autoEnableTestQuality(opts);
|
|
27
|
+
|
|
28
|
+
return opts;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function autoEnableTestQuality(opts: AnalysisOptions): void {
|
|
32
|
+
if (opts.features === null) return;
|
|
33
|
+
|
|
34
|
+
const testQualityCats = new Set(PILLAR_CATEGORIES['test-quality']);
|
|
35
|
+
if ([...opts.features].some(f => testQualityCats.has(f))) {
|
|
36
|
+
opts.includeTests = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves `--exclude` into a features set by subtracting from ALL_CATEGORIES.
|
|
42
|
+
* Called during CLI arg parsing when --exclude is used.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveExcludeToFeatures(
|
|
45
|
+
excludeSet: Set<string>
|
|
46
|
+
): Set<string> {
|
|
47
|
+
return new Set([...ALL_CATEGORIES].filter(c => !excludeSet.has(c)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class OptionsError extends Error {
|
|
51
|
+
constructor(message: string) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'OptionsError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { computeGateScore } from './main.js';
|
|
4
|
+
|
|
5
|
+
describe('computeGateScore', () => {
|
|
6
|
+
it('returns 100 for 0 findings', () => {
|
|
7
|
+
expect(computeGateScore(0, 100)).toBe(100);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns 100 for 0 findings and 0 files', () => {
|
|
11
|
+
expect(computeGateScore(0, 0)).toBe(100);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('decreases as findings increase', () => {
|
|
15
|
+
const score10 = computeGateScore(10, 100);
|
|
16
|
+
const score50 = computeGateScore(50, 100);
|
|
17
|
+
const score200 = computeGateScore(200, 100);
|
|
18
|
+
|
|
19
|
+
expect(score10).toBeGreaterThan(score50);
|
|
20
|
+
expect(score50).toBeGreaterThan(score200);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns higher score for same findings with more files', () => {
|
|
24
|
+
const smallProject = computeGateScore(50, 10);
|
|
25
|
+
const bigProject = computeGateScore(50, 1000);
|
|
26
|
+
|
|
27
|
+
expect(bigProject).toBeGreaterThan(smallProject);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is always between 0 and 100', () => {
|
|
31
|
+
for (const [f, t] of [
|
|
32
|
+
[0, 1],
|
|
33
|
+
[1, 1],
|
|
34
|
+
[100, 10],
|
|
35
|
+
[1000, 50],
|
|
36
|
+
[10000, 100],
|
|
37
|
+
]) {
|
|
38
|
+
const score = computeGateScore(f, t);
|
|
39
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
40
|
+
expect(score).toBeLessThanOrEqual(100);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns reasonable score for typical project (10 findings / 100 files)', () => {
|
|
45
|
+
const score = computeGateScore(10, 100);
|
|
46
|
+
expect(score).toBeGreaterThan(90);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns low score for finding-heavy project (500 findings / 50 files)', () => {
|
|
50
|
+
const score = computeGateScore(500, 50);
|
|
51
|
+
expect(score).toBeLessThanOrEqual(50);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles totalFiles=0 without division by zero', () => {
|
|
55
|
+
expect(() => computeGateScore(10, 0)).not.toThrow();
|
|
56
|
+
const score = computeGateScore(10, 0);
|
|
57
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
58
|
+
expect(score).toBeLessThanOrEqual(100);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns integer values', () => {
|
|
62
|
+
const score = computeGateScore(17, 33);
|
|
63
|
+
expect(Number.isInteger(score)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
setCacheEntry,
|
|
16
16
|
} from './cache.js';
|
|
17
17
|
import { parseArgs } from './cli.js';
|
|
18
|
+
import { loadConfigFile, mergeConfigIntoDefaults } from './config-loader.js';
|
|
19
|
+
import { createOptions, OptionsError } from './create-options.js';
|
|
20
|
+
import { attachConsoleFeedback, bus } from './progress.js';
|
|
21
|
+
import { resolveAffectedFiles } from './affected.js';
|
|
22
|
+
import { saveBaseline, filterKnownFindings } from './baseline.js';
|
|
23
|
+
import { formatFindings } from './reporters.js';
|
|
18
24
|
import { collectDependencyProfile } from '../analysis/dependencies.js';
|
|
19
25
|
import { buildDependencySummary } from '../analysis/dependency-summary.js';
|
|
20
26
|
import {
|
|
@@ -52,6 +58,7 @@ import {
|
|
|
52
58
|
} from '../reporting/analysis.js';
|
|
53
59
|
import { diverseTopRecommendations } from '../reporting/summary-md.js';
|
|
54
60
|
import { generateMermaidGraph, writeMultiFileReport } from '../reporting/writer.js';
|
|
61
|
+
import type { GraphRenderOptions } from '../reporting/writer.js';
|
|
55
62
|
import { PILLAR_CATEGORIES, SEMANTIC_CATEGORIES } from '../types/index.js';
|
|
56
63
|
|
|
57
64
|
import type { SemanticProfile } from '../analysis/semantic.js';
|
|
@@ -211,8 +218,8 @@ function runSemanticPhase(
|
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
return runSemanticDetectors(semanticCtx, profiles, {
|
|
214
|
-
overrideChainThreshold: options.overrideChainThreshold,
|
|
215
|
-
shotgunThreshold: options.shotgunThreshold,
|
|
221
|
+
overrideChainThreshold: options.thresholds.overrideChainThreshold,
|
|
222
|
+
shotgunThreshold: options.thresholds.shotgunThreshold,
|
|
216
223
|
});
|
|
217
224
|
} catch (err: unknown) {
|
|
218
225
|
parseErrors.push({
|
|
@@ -415,8 +422,26 @@ interface ScanState {
|
|
|
415
422
|
treeSitterError: string | null;
|
|
416
423
|
}
|
|
417
424
|
|
|
418
|
-
async function initScanState(
|
|
419
|
-
|
|
425
|
+
async function initScanState(
|
|
426
|
+
prebuiltOptions?: AnalysisOptions
|
|
427
|
+
): Promise<ScanState | null> {
|
|
428
|
+
let options: AnalysisOptions;
|
|
429
|
+
if (prebuiltOptions) {
|
|
430
|
+
options = prebuiltOptions;
|
|
431
|
+
} else {
|
|
432
|
+
const { DEFAULT_OPTS } = await import('../types/constants.js');
|
|
433
|
+
const cliArgs = createOptions({ args: parseArgs(process.argv.slice(2)) });
|
|
434
|
+
const config = loadConfigFile(cliArgs.root, cliArgs.configFile);
|
|
435
|
+
options = config
|
|
436
|
+
? mergeConfigIntoDefaults(DEFAULT_OPTS, config, cliArgs)
|
|
437
|
+
: cliArgs;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!options.json && !prebuiltOptions) {
|
|
441
|
+
attachConsoleFeedback();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
bus.progress('startup', 'Options parsed', `root=${options.root}`);
|
|
420
445
|
|
|
421
446
|
if (options.clearCache) {
|
|
422
447
|
clearCache(options.root);
|
|
@@ -472,6 +497,8 @@ async function initScanState(): Promise<ScanState | null> {
|
|
|
472
497
|
}
|
|
473
498
|
}
|
|
474
499
|
|
|
500
|
+
bus.progress('discovery', `Found ${packages.length} package(s)`, packages.map(p => p.name).join(', '));
|
|
501
|
+
|
|
475
502
|
return {
|
|
476
503
|
options,
|
|
477
504
|
packages,
|
|
@@ -537,6 +564,7 @@ type CachedResult = {
|
|
|
537
564
|
|
|
538
565
|
function collectFileData(state: ScanState): void {
|
|
539
566
|
const { options, packages, useTreeSitter, summary, flowMap, controlMap, trees, fileSummaries, parseErrors, dependencyState, packageFileStats } = state;
|
|
567
|
+
bus.progress('cache-check', options.noCache ? 'Cache disabled' : 'Loading cache');
|
|
540
568
|
const cache = options.noCache ? null : loadCache(options.root);
|
|
541
569
|
const newCache = createEmptyCache(options.root);
|
|
542
570
|
|
|
@@ -785,13 +813,15 @@ function collectFileData(state: ScanState): void {
|
|
|
785
813
|
}
|
|
786
814
|
|
|
787
815
|
summary.totalDependencyFiles = dependencyState.files.size;
|
|
816
|
+
bus.progress('parse', `Parsed ${fileSummaries.length} files`, `${state.cacheHits} cache hits`);
|
|
788
817
|
}
|
|
789
818
|
|
|
790
|
-
function analyzeAndReport(state: ScanState):
|
|
819
|
+
function analyzeAndReport(state: ScanState): number {
|
|
820
|
+
bus.progress('detect', 'Running detectors');
|
|
791
821
|
const { options, effectiveParser, summary, flowMap, controlMap, trees, fileSummaries, parseErrors, dependencyState, allPkgJsonDeps, allPkgJsonDevDeps, isLegacyMode, outputDir, outputPath, treeSitterAvailable, treeSitterError } = state;
|
|
792
822
|
|
|
793
823
|
const { duplicateFunctions, redundantFlows, duplicateFlowHints } =
|
|
794
|
-
groupDuplicates(flowMap, controlMap, options.flowDupThreshold);
|
|
824
|
+
groupDuplicates(flowMap, controlMap, options.thresholds.flowDupThreshold);
|
|
795
825
|
|
|
796
826
|
const fileCriticalityByPath = new Map<string, FileCriticality>(
|
|
797
827
|
fileSummaries.map(item => [
|
|
@@ -804,6 +834,7 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
804
834
|
fileCriticalityByPath,
|
|
805
835
|
options
|
|
806
836
|
);
|
|
837
|
+
bus.progress('graph', 'Computing graph analytics');
|
|
807
838
|
const graphAnalytics = computeGraphAnalytics(
|
|
808
839
|
dependencyState,
|
|
809
840
|
dependencySummary,
|
|
@@ -813,6 +844,7 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
813
844
|
? buildAdvancedGraphFindings(graphAnalytics, dependencyState, fileSummaries)
|
|
814
845
|
: [];
|
|
815
846
|
|
|
847
|
+
bus.progress('semantic', options.semantic ? 'Running semantic analysis' : 'Skipping semantic (not requested)');
|
|
816
848
|
const semanticFindings = runSemanticPhase(
|
|
817
849
|
fileSummaries,
|
|
818
850
|
dependencyState,
|
|
@@ -863,6 +895,27 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
863
895
|
graphAnalytics,
|
|
864
896
|
{ flowEnabled: !!options.flow }
|
|
865
897
|
);
|
|
898
|
+
if (options.affected) {
|
|
899
|
+
const affectedPaths = resolveAffectedFiles(
|
|
900
|
+
options.root, options.affected, dependencyState
|
|
901
|
+
);
|
|
902
|
+
if (affectedPaths.length > 0) {
|
|
903
|
+
const affectedSet = new Set(affectedPaths);
|
|
904
|
+
findings = findings.filter(f => affectedSet.has(f.file));
|
|
905
|
+
bus.progress('detect', `--affected: ${affectedPaths.length} files in scope, ${findings.length} findings`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (options.ignoreKnown) {
|
|
910
|
+
const { filtered, suppressedCount } = filterKnownFindings(
|
|
911
|
+
findings, options.ignoreKnown, options.root
|
|
912
|
+
);
|
|
913
|
+
if (suppressedCount > 0) {
|
|
914
|
+
bus.progress('detect', `--ignore-known: suppressed ${suppressedCount} known findings`);
|
|
915
|
+
}
|
|
916
|
+
findings = filtered;
|
|
917
|
+
}
|
|
918
|
+
|
|
866
919
|
const reportAnalysis = computeReportAnalysisSummary(
|
|
867
920
|
findings,
|
|
868
921
|
enrichedFileSummaries,
|
|
@@ -948,7 +1001,19 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
948
1001
|
report.astTrees = trees;
|
|
949
1002
|
}
|
|
950
1003
|
|
|
951
|
-
|
|
1004
|
+
bus.progress('report', `${findings.length} findings generated`);
|
|
1005
|
+
|
|
1006
|
+
if (options.saveBaseline) {
|
|
1007
|
+
const baselinePath = saveBaseline(options.root, findings);
|
|
1008
|
+
if (!options.json) {
|
|
1009
|
+
console.error(`Baseline saved: ${path.relative(options.root, baselinePath)} (${findings.length} findings)`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (options.reporter !== 'default') {
|
|
1014
|
+
const formatted = formatFindings(findings, options.reporter, options.root);
|
|
1015
|
+
process.stdout.write(formatted + '\n');
|
|
1016
|
+
} else if (options.json) {
|
|
952
1017
|
console.log(JSON.stringify(report));
|
|
953
1018
|
} else {
|
|
954
1019
|
printConsoleResults(
|
|
@@ -972,10 +1037,16 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
972
1037
|
);
|
|
973
1038
|
}
|
|
974
1039
|
if (options.graph) {
|
|
1040
|
+
const gOpts: GraphRenderOptions = {
|
|
1041
|
+
focus: options.focus,
|
|
1042
|
+
focusDepth: options.focusDepth,
|
|
1043
|
+
collapse: options.collapse,
|
|
1044
|
+
};
|
|
975
1045
|
const graphMd = generateMermaidGraph(
|
|
976
1046
|
dependencyState,
|
|
977
1047
|
dependencySummary,
|
|
978
|
-
fileCriticalityByPath
|
|
1048
|
+
fileCriticalityByPath,
|
|
1049
|
+
gOpts
|
|
979
1050
|
);
|
|
980
1051
|
const graphPath = outputPath.replace(/\.json$/, '-graph.md');
|
|
981
1052
|
fs.writeFileSync(graphPath, graphMd, 'utf8');
|
|
@@ -986,13 +1057,20 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
986
1057
|
}
|
|
987
1058
|
}
|
|
988
1059
|
} else if (outputDir) {
|
|
1060
|
+
bus.progress('write', `Writing report to ${path.relative(options.root, outputDir)}`);
|
|
1061
|
+
const gOpts: GraphRenderOptions = {
|
|
1062
|
+
focus: options.focus,
|
|
1063
|
+
focusDepth: options.focusDepth,
|
|
1064
|
+
collapse: options.collapse,
|
|
1065
|
+
};
|
|
989
1066
|
const outputFiles = writeMultiFileReport(
|
|
990
1067
|
outputDir,
|
|
991
1068
|
report,
|
|
992
1069
|
options,
|
|
993
1070
|
dependencyState,
|
|
994
1071
|
dependencySummary,
|
|
995
|
-
fileCriticalityByPath
|
|
1072
|
+
fileCriticalityByPath,
|
|
1073
|
+
gOpts
|
|
996
1074
|
);
|
|
997
1075
|
if (!options.json) {
|
|
998
1076
|
const relDir = path.relative(options.root, outputDir);
|
|
@@ -1002,13 +1080,39 @@ function analyzeAndReport(state: ScanState): void {
|
|
|
1002
1080
|
}
|
|
1003
1081
|
}
|
|
1004
1082
|
}
|
|
1083
|
+
|
|
1084
|
+
bus.progress('done', 'Scan complete', `${findings.length} findings`);
|
|
1085
|
+
|
|
1086
|
+
if (options.atLeast != null) {
|
|
1087
|
+
const totalFiles = summary.totalFiles ?? 1;
|
|
1088
|
+
const gateScore = computeGateScore(findings.length, totalFiles);
|
|
1089
|
+
if (gateScore < options.atLeast) {
|
|
1090
|
+
console.error(
|
|
1091
|
+
`Gate score ${gateScore} is below --at-least threshold ${options.atLeast}`
|
|
1092
|
+
);
|
|
1093
|
+
return -1;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return findings.length;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export function computeGateScore(findingsCount: number, totalFiles: number): number {
|
|
1101
|
+
const ratio = findingsCount / Math.max(totalFiles, 1);
|
|
1102
|
+
return Math.round(100 / (1 + ratio / 10));
|
|
1005
1103
|
}
|
|
1006
1104
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1105
|
+
export const EXIT_SUCCESS = 0;
|
|
1106
|
+
export const EXIT_FINDINGS = 1;
|
|
1107
|
+
export const EXIT_ERROR = 2;
|
|
1108
|
+
|
|
1109
|
+
async function main(options?: AnalysisOptions): Promise<number> {
|
|
1110
|
+
const state = await initScanState(options);
|
|
1111
|
+
if (!state) return EXIT_SUCCESS;
|
|
1010
1112
|
collectFileData(state);
|
|
1011
|
-
analyzeAndReport(state);
|
|
1113
|
+
const findingsCount = analyzeAndReport(state);
|
|
1114
|
+
if (findingsCount < 0) return EXIT_FINDINGS;
|
|
1115
|
+
return findingsCount > 0 ? EXIT_FINDINGS : EXIT_SUCCESS;
|
|
1012
1116
|
}
|
|
1013
1117
|
|
|
1014
1118
|
export { main };
|
|
@@ -1067,8 +1171,17 @@ function buildFindingStats(
|
|
|
1067
1171
|
}
|
|
1068
1172
|
|
|
1069
1173
|
if (isDirectRun(import.meta.url)) {
|
|
1070
|
-
main()
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1174
|
+
main()
|
|
1175
|
+
.then(code => {
|
|
1176
|
+
process.exitCode = code;
|
|
1177
|
+
})
|
|
1178
|
+
.catch((error: unknown) => {
|
|
1179
|
+
if (error instanceof OptionsError) {
|
|
1180
|
+
console.error(error.message);
|
|
1181
|
+
process.exitCode = EXIT_ERROR;
|
|
1182
|
+
} else {
|
|
1183
|
+
console.error(error);
|
|
1184
|
+
process.exitCode = EXIT_ERROR;
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1074
1187
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
export type ProgressPhase =
|
|
4
|
+
| 'startup'
|
|
5
|
+
| 'cache-check'
|
|
6
|
+
| 'discovery'
|
|
7
|
+
| 'parse'
|
|
8
|
+
| 'dependencies'
|
|
9
|
+
| 'semantic'
|
|
10
|
+
| 'detect'
|
|
11
|
+
| 'graph'
|
|
12
|
+
| 'report'
|
|
13
|
+
| 'write'
|
|
14
|
+
| 'done';
|
|
15
|
+
|
|
16
|
+
export interface ProgressEvent {
|
|
17
|
+
phase: ProgressPhase;
|
|
18
|
+
message: string;
|
|
19
|
+
progress?: number;
|
|
20
|
+
detail?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ScanBus extends EventEmitter {
|
|
24
|
+
progress(phase: ProgressPhase, message: string, detail?: string): void {
|
|
25
|
+
this.emit('progress', { phase, message, detail } satisfies ProgressEvent);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
summary(message: string): void {
|
|
29
|
+
this.emit('summary', message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
error(message: string, detail?: string): void {
|
|
33
|
+
this.emit('error', { message, detail });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
reset(): void {
|
|
37
|
+
this.removeAllListeners();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const bus = new ScanBus();
|
|
42
|
+
|
|
43
|
+
export function attachConsoleFeedback(): void {
|
|
44
|
+
bus.on('progress', (event: ProgressEvent) => {
|
|
45
|
+
if (event.detail) {
|
|
46
|
+
process.stderr.write(`[${event.phase}] ${event.message}: ${event.detail}\n`);
|
|
47
|
+
} else {
|
|
48
|
+
process.stderr.write(`[${event.phase}] ${event.message}\n`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { formatFindings } from './reporters.js';
|
|
4
|
+
import type { Finding } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
function makeFinding(overrides: Partial<Finding>): Finding {
|
|
7
|
+
return {
|
|
8
|
+
id: 'f1',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
category: 'dead-export',
|
|
11
|
+
file: '/repo/src/a.ts',
|
|
12
|
+
lineStart: 10,
|
|
13
|
+
lineEnd: 15,
|
|
14
|
+
title: 'Unused export foo',
|
|
15
|
+
reason: 'No consumers',
|
|
16
|
+
files: ['/repo/src/a.ts'],
|
|
17
|
+
suggestedFix: { strategy: 'remove', steps: ['Delete'] },
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('formatFindings', () => {
|
|
23
|
+
describe('compact reporter', () => {
|
|
24
|
+
it('formats one-line per finding with line number', () => {
|
|
25
|
+
const findings = [
|
|
26
|
+
makeFinding({
|
|
27
|
+
severity: 'high',
|
|
28
|
+
file: '/repo/src/a.ts',
|
|
29
|
+
lineStart: 10,
|
|
30
|
+
category: 'dead-export',
|
|
31
|
+
title: 'Unused foo',
|
|
32
|
+
}),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const result = formatFindings(findings, 'compact', '/repo');
|
|
36
|
+
expect(result).toBe('high:src/a.ts:10 - [dead-export] Unused foo');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('omits line number when lineStart is 0', () => {
|
|
40
|
+
const findings = [
|
|
41
|
+
makeFinding({
|
|
42
|
+
severity: 'low',
|
|
43
|
+
file: '/repo/src/b.ts',
|
|
44
|
+
lineStart: 0,
|
|
45
|
+
category: 'god-function',
|
|
46
|
+
title: 'Big func',
|
|
47
|
+
}),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const result = formatFindings(findings, 'compact', '/repo');
|
|
51
|
+
expect(result).toBe('low:src/b.ts - [god-function] Big func');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles empty findings list', () => {
|
|
55
|
+
expect(formatFindings([], 'compact', '/repo')).toBe('');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('formats multiple findings separated by newlines', () => {
|
|
59
|
+
const findings = [
|
|
60
|
+
makeFinding({ severity: 'critical', title: 'A' }),
|
|
61
|
+
makeFinding({ severity: 'low', title: 'B' }),
|
|
62
|
+
];
|
|
63
|
+
const lines = formatFindings(findings, 'compact', '/repo').split('\n');
|
|
64
|
+
expect(lines).toHaveLength(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('handles file paths not under root', () => {
|
|
68
|
+
const findings = [
|
|
69
|
+
makeFinding({ file: '/other/place/x.ts', lineStart: 5, title: 'Out' }),
|
|
70
|
+
];
|
|
71
|
+
const result = formatFindings(findings, 'compact', '/repo');
|
|
72
|
+
expect(result).toContain('/other/place/x.ts:5');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('github-actions reporter', () => {
|
|
77
|
+
it('maps critical severity to ::error', () => {
|
|
78
|
+
const findings = [
|
|
79
|
+
makeFinding({ severity: 'critical', lineStart: 5, title: 'Leak' }),
|
|
80
|
+
];
|
|
81
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
82
|
+
expect(result.startsWith('::error ')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('maps high severity to ::error', () => {
|
|
86
|
+
const findings = [
|
|
87
|
+
makeFinding({ severity: 'high', lineStart: 5, title: 'Bad' }),
|
|
88
|
+
];
|
|
89
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
90
|
+
expect(result.startsWith('::error ')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('maps medium severity to ::warning', () => {
|
|
94
|
+
const findings = [
|
|
95
|
+
makeFinding({ severity: 'medium', lineStart: 5, title: 'Warn' }),
|
|
96
|
+
];
|
|
97
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
98
|
+
expect(result.startsWith('::warning ')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('maps low severity to ::notice', () => {
|
|
102
|
+
const findings = [
|
|
103
|
+
makeFinding({ severity: 'low', lineStart: 5, title: 'Info' }),
|
|
104
|
+
];
|
|
105
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
106
|
+
expect(result.startsWith('::notice ')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('maps info severity to ::warning (default)', () => {
|
|
110
|
+
const findings = [
|
|
111
|
+
makeFinding({ severity: 'info', lineStart: 5, title: 'Note' }),
|
|
112
|
+
];
|
|
113
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
114
|
+
expect(result.startsWith('::warning ')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('includes file and line in annotation', () => {
|
|
118
|
+
const findings = [
|
|
119
|
+
makeFinding({
|
|
120
|
+
severity: 'high',
|
|
121
|
+
file: '/repo/src/a.ts',
|
|
122
|
+
lineStart: 42,
|
|
123
|
+
title: 'Found it',
|
|
124
|
+
category: 'unsafe-any',
|
|
125
|
+
}),
|
|
126
|
+
];
|
|
127
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
128
|
+
expect(result).toBe(
|
|
129
|
+
'::error file=src/a.ts,line=42::Found it [unsafe-any]'
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('defaults to line 1 when lineStart is 0', () => {
|
|
134
|
+
const findings = [
|
|
135
|
+
makeFinding({ severity: 'medium', lineStart: 0, title: 'X' }),
|
|
136
|
+
];
|
|
137
|
+
const result = formatFindings(findings, 'github-actions', '/repo');
|
|
138
|
+
expect(result).toContain('line=1');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('handles empty findings list', () => {
|
|
142
|
+
expect(formatFindings([], 'github-actions', '/repo')).toBe('');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('default reporter', () => {
|
|
147
|
+
it('returns empty string for default format', () => {
|
|
148
|
+
expect(formatFindings([], 'default', '/repo')).toBe('');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns empty string even with findings', () => {
|
|
152
|
+
expect(formatFindings([makeFinding({})], 'default', '/repo')).toBe('');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|