octocode-cli 1.2.8 → 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 -11767
- 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,1604 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { isTestFile } from '../common/utils.js';
|
|
5
|
+
import { PILLAR_CATEGORIES, SEVERITY_ORDER } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
import type { ReportAnalysisSummary } from './analysis.js';
|
|
8
|
+
import type {
|
|
9
|
+
AgentOutputData,
|
|
10
|
+
FileEntry,
|
|
11
|
+
Finding,
|
|
12
|
+
FindingStats,
|
|
13
|
+
HotFile,
|
|
14
|
+
ScanSummaryData,
|
|
15
|
+
} from '../types/index.js';
|
|
16
|
+
|
|
17
|
+
const CATEGORY_PILLAR_MAP: Record<string, string> = Object.entries(
|
|
18
|
+
PILLAR_CATEGORIES
|
|
19
|
+
).reduce<Record<string, string>>((acc, [pillar, categories]) => {
|
|
20
|
+
for (const category of categories) acc[category] = pillar;
|
|
21
|
+
return acc;
|
|
22
|
+
}, {});
|
|
23
|
+
|
|
24
|
+
export function severityBreakdown(findings: Finding[]): Record<string, number> {
|
|
25
|
+
const counts: Record<string, number> = {
|
|
26
|
+
critical: 0,
|
|
27
|
+
high: 0,
|
|
28
|
+
medium: 0,
|
|
29
|
+
low: 0,
|
|
30
|
+
info: 0,
|
|
31
|
+
};
|
|
32
|
+
for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
33
|
+
return counts;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function categoryBreakdown(findings: Finding[]): Record<string, number> {
|
|
37
|
+
const counts: Record<string, number> = {};
|
|
38
|
+
for (const f of findings) counts[f.category] = (counts[f.category] || 0) + 1;
|
|
39
|
+
return counts;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function computeHealthScore(
|
|
43
|
+
findings: Finding[],
|
|
44
|
+
totalFiles: number
|
|
45
|
+
): number {
|
|
46
|
+
return computeHealthScoreFromSeverityBreakdown(
|
|
47
|
+
severityBreakdown(findings),
|
|
48
|
+
totalFiles
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function computeHealthScoreFromSeverityBreakdown(
|
|
53
|
+
breakdown: Record<string, number>,
|
|
54
|
+
totalFiles: number
|
|
55
|
+
): number {
|
|
56
|
+
if (totalFiles === 0) return 100;
|
|
57
|
+
const weights = { critical: 25, high: 10, medium: 3, low: 1, info: 0 };
|
|
58
|
+
let penalty = 0;
|
|
59
|
+
for (const [severity, count] of Object.entries(breakdown)) {
|
|
60
|
+
penalty += (weights[severity as keyof typeof weights] || 0) * count;
|
|
61
|
+
}
|
|
62
|
+
const weightedFindingsPerFile = penalty / totalFiles;
|
|
63
|
+
const rawScore = Math.max(
|
|
64
|
+
0,
|
|
65
|
+
Math.min(100, Math.round(100 / (1 + weightedFindingsPerFile / 10)))
|
|
66
|
+
);
|
|
67
|
+
if (penalty === 0) return rawScore;
|
|
68
|
+
if ((breakdown.critical ?? 0) > 0) return Math.min(rawScore, 95);
|
|
69
|
+
if ((breakdown.high ?? 0) > 0) return Math.min(rawScore, 98);
|
|
70
|
+
return Math.min(rawScore, 99);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function collectTagCloud(
|
|
74
|
+
findings: Finding[]
|
|
75
|
+
): { tag: string; count: number }[] {
|
|
76
|
+
const tagCounts = new Map<string, number>();
|
|
77
|
+
for (const f of findings) {
|
|
78
|
+
if (!f.tags) continue;
|
|
79
|
+
for (const tag of f.tags) {
|
|
80
|
+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [...tagCounts.entries()]
|
|
84
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
85
|
+
.sort((a, b) => b.count - a.count);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatFileSize(bytes: number): string {
|
|
89
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
90
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
91
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function summarizeActiveFeatures(activeFeatures: Set<string>): string[] {
|
|
95
|
+
const remaining = new Set(activeFeatures);
|
|
96
|
+
const labels: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const [pillar, categories] of Object.entries(PILLAR_CATEGORIES)) {
|
|
99
|
+
if (categories.length > 0 && categories.every(cat => remaining.has(cat))) {
|
|
100
|
+
labels.push(pillar);
|
|
101
|
+
for (const cat of categories) remaining.delete(cat);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [...labels, ...[...remaining].sort()];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isPillarActive(
|
|
109
|
+
pillarKey: string,
|
|
110
|
+
activeFeatures: Set<string> | null
|
|
111
|
+
): boolean {
|
|
112
|
+
if (!activeFeatures) return true;
|
|
113
|
+
const pillarCats = PILLAR_CATEGORIES[pillarKey] || [];
|
|
114
|
+
return pillarCats.some(cat => activeFeatures.has(cat));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type FindingLike = Omit<Finding, 'id'> & { id?: string };
|
|
118
|
+
|
|
119
|
+
export function diversifyFindings<T extends FindingLike>(
|
|
120
|
+
sorted: T[],
|
|
121
|
+
limit: number
|
|
122
|
+
): T[] {
|
|
123
|
+
if (!Number.isFinite(limit) || limit >= sorted.length) return sorted;
|
|
124
|
+
|
|
125
|
+
const groups = new Map<string, T[]>();
|
|
126
|
+
for (const f of sorted) {
|
|
127
|
+
const cat = f.category;
|
|
128
|
+
if (!groups.has(cat)) groups.set(cat, []);
|
|
129
|
+
groups.get(cat)!.push(f);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const categoryOrder = [...groups.entries()].sort((a, b) => {
|
|
133
|
+
const aTop = SEVERITY_ORDER[a[1][0].severity] ?? 0;
|
|
134
|
+
const bTop = SEVERITY_ORDER[b[1][0].severity] ?? 0;
|
|
135
|
+
return bTop - aTop;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result: T[] = [];
|
|
139
|
+
const cursors = new Map<string, number>();
|
|
140
|
+
for (const [cat] of categoryOrder) cursors.set(cat, 0);
|
|
141
|
+
|
|
142
|
+
while (result.length < limit) {
|
|
143
|
+
let picked = false;
|
|
144
|
+
for (const [cat, items] of categoryOrder) {
|
|
145
|
+
if (result.length >= limit) break;
|
|
146
|
+
const cursor = cursors.get(cat)!;
|
|
147
|
+
if (cursor < items.length) {
|
|
148
|
+
result.push(items[cursor]);
|
|
149
|
+
cursors.set(cat, cursor + 1);
|
|
150
|
+
picked = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!picked) break;
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function diverseTopRecommendations(
|
|
159
|
+
findings: Finding[],
|
|
160
|
+
limit: number = 20,
|
|
161
|
+
maxPerCategory: number = 2
|
|
162
|
+
): Finding[] {
|
|
163
|
+
const result: Finding[] = [];
|
|
164
|
+
const countByCategory = new Map<string, number>();
|
|
165
|
+
for (const f of findings) {
|
|
166
|
+
const catCount = countByCategory.get(f.category) || 0;
|
|
167
|
+
if (catCount >= maxPerCategory) continue;
|
|
168
|
+
result.push(f);
|
|
169
|
+
countByCategory.set(f.category, catCount + 1);
|
|
170
|
+
if (result.length >= limit) break;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SummaryMdOptions {
|
|
176
|
+
dir: string;
|
|
177
|
+
report: import('./writer.js').FullReport;
|
|
178
|
+
outputFiles: Record<string, string>;
|
|
179
|
+
architectureFindings: Finding[];
|
|
180
|
+
codeQualityFindings: Finding[];
|
|
181
|
+
deadCodeFindings: Finding[];
|
|
182
|
+
hotFiles?: import('../types/index.js').HotFile[];
|
|
183
|
+
activeFeatures?: Set<string> | null;
|
|
184
|
+
scope?: string[] | null;
|
|
185
|
+
root?: string;
|
|
186
|
+
scopeSymbols?: Map<string, string[]> | null;
|
|
187
|
+
semanticEnabled?: boolean;
|
|
188
|
+
securityFindings?: Finding[];
|
|
189
|
+
testQualityFindings?: Finding[];
|
|
190
|
+
reportAnalysis?: ReportAnalysisSummary;
|
|
191
|
+
fileInventory?: FileEntry[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatCliPath(filePath: string): string {
|
|
195
|
+
return JSON.stringify(filePath.replace(/\\/g, '/'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function generateSummaryMd(opts: SummaryMdOptions): string {
|
|
199
|
+
const {
|
|
200
|
+
dir,
|
|
201
|
+
report,
|
|
202
|
+
outputFiles,
|
|
203
|
+
architectureFindings,
|
|
204
|
+
codeQualityFindings,
|
|
205
|
+
deadCodeFindings,
|
|
206
|
+
hotFiles = [],
|
|
207
|
+
activeFeatures = null,
|
|
208
|
+
scope = null,
|
|
209
|
+
root = process.cwd(),
|
|
210
|
+
scopeSymbols = null,
|
|
211
|
+
semanticEnabled = false,
|
|
212
|
+
securityFindings = [],
|
|
213
|
+
testQualityFindings = [],
|
|
214
|
+
reportAnalysis = null,
|
|
215
|
+
fileInventory = report.fileInventory || [],
|
|
216
|
+
} = opts;
|
|
217
|
+
const allFindings = report.optimizationFindings || [];
|
|
218
|
+
const summary: ScanSummaryData = report.summary;
|
|
219
|
+
const agentOutput: AgentOutputData = report.agentOutput;
|
|
220
|
+
const findingStats: FindingStats | null = agentOutput?.findingStats ?? null;
|
|
221
|
+
const depGraph = report.dependencyGraph;
|
|
222
|
+
const relativeScanDir = path.relative(root, dir) || '.';
|
|
223
|
+
const exampleFileFilter = ((scope?.[0] ?? 'src/index').split(':')[0] || 'src/index')
|
|
224
|
+
.replace(/\\/g, '/');
|
|
225
|
+
const overallFindingStats = findingStats?.overall ?? {
|
|
226
|
+
totalFindings: allFindings.length,
|
|
227
|
+
severityBreakdown: severityBreakdown(allFindings),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const lines: string[] = [];
|
|
231
|
+
lines.push('# Code Quality Scan Report\n');
|
|
232
|
+
lines.push(`**Generated**: ${report.generatedAt} `);
|
|
233
|
+
lines.push(`**Root**: \`${report.repoRoot}\`\n`);
|
|
234
|
+
|
|
235
|
+
lines.push('## Scan Scope\n');
|
|
236
|
+
lines.push(`| Metric | Count |`);
|
|
237
|
+
lines.push(`|--------|-------|`);
|
|
238
|
+
lines.push(`| Files analyzed | ${summary.totalFiles ?? '—'} |`);
|
|
239
|
+
lines.push(`| Functions | ${summary.totalFunctions ?? '—'} |`);
|
|
240
|
+
lines.push(`| Flow nodes | ${summary.totalFlows ?? '—'} |`);
|
|
241
|
+
lines.push(`| Dependency files | ${summary.totalDependencyFiles ?? '—'} |`);
|
|
242
|
+
lines.push(`| Packages | ${summary.totalPackages ?? '—'} |`);
|
|
243
|
+
lines.push('');
|
|
244
|
+
|
|
245
|
+
lines.push('## Findings Overview\n');
|
|
246
|
+
lines.push(`| Severity | Count |`);
|
|
247
|
+
lines.push(`|----------|-------|`);
|
|
248
|
+
lines.push(`| Critical | ${overallFindingStats.severityBreakdown.critical ?? 0} |`);
|
|
249
|
+
lines.push(`| High | ${overallFindingStats.severityBreakdown.high ?? 0} |`);
|
|
250
|
+
lines.push(`| Medium | ${overallFindingStats.severityBreakdown.medium ?? 0} |`);
|
|
251
|
+
lines.push(`| Low | ${overallFindingStats.severityBreakdown.low ?? 0} |`);
|
|
252
|
+
lines.push(`| **Total** | **${overallFindingStats.totalFindings}** |`);
|
|
253
|
+
lines.push('');
|
|
254
|
+
|
|
255
|
+
renderScanAnnotations(lines, {
|
|
256
|
+
allFindings, overallFindingStats, agentOutput,
|
|
257
|
+
activeFeatures, scope, root, scopeSymbols, semanticEnabled,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const renderPillarCategories = (
|
|
261
|
+
pillarKey: string,
|
|
262
|
+
findings: Finding[]
|
|
263
|
+
): void => {
|
|
264
|
+
const breakdown = categoryBreakdown(findings);
|
|
265
|
+
const pillarCats = PILLAR_CATEGORIES[pillarKey] || [];
|
|
266
|
+
const isFiltered = activeFeatures !== null;
|
|
267
|
+
for (const cat of pillarCats) {
|
|
268
|
+
const count = breakdown[cat] || 0;
|
|
269
|
+
const skipped = isFiltered && !activeFeatures!.has(cat);
|
|
270
|
+
lines.push(skipped ? `- \`${cat}\`: — *(skipped)*` : `- \`${cat}\`: ${count}`);
|
|
271
|
+
}
|
|
272
|
+
lines.push('');
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const totalFiles = summary.totalFiles || 1;
|
|
276
|
+
const archStats = findingStats?.pillars?.['architecture'];
|
|
277
|
+
const qualStats = findingStats?.pillars?.['code-quality'];
|
|
278
|
+
const deadStats = findingStats?.pillars?.['dead-code'];
|
|
279
|
+
const secStats = findingStats?.pillars?.['security'];
|
|
280
|
+
const testStats = findingStats?.pillars?.['test-quality'];
|
|
281
|
+
|
|
282
|
+
const pillarHealth = computePillarHealthScores(totalFiles, overallFindingStats, {
|
|
283
|
+
archStats, qualStats, deadStats, secStats, testStats,
|
|
284
|
+
architectureFindings, codeQualityFindings, deadCodeFindings,
|
|
285
|
+
securityFindings, testQualityFindings,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const pushPillarSummary = buildPillarSummaryPusher(lines, activeFeatures, outputFiles);
|
|
289
|
+
const qualityRating = computeQualityAspectRatings(allFindings, {
|
|
290
|
+
fileInventory,
|
|
291
|
+
hotFiles,
|
|
292
|
+
reportAnalysis,
|
|
293
|
+
includeTests: Boolean(
|
|
294
|
+
(report.options as { includeTests?: boolean } | undefined)?.includeTests
|
|
295
|
+
),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
renderHealthScores(lines, pillarHealth, activeFeatures);
|
|
299
|
+
renderFeatureScores(
|
|
300
|
+
lines,
|
|
301
|
+
computeFeatureScores(allFindings, totalFiles, activeFeatures, { hotFiles })
|
|
302
|
+
);
|
|
303
|
+
renderQualityAspectRatings(lines, qualityRating);
|
|
304
|
+
|
|
305
|
+
renderTagCloud(lines, allFindings);
|
|
306
|
+
|
|
307
|
+
if (reportAnalysis) {
|
|
308
|
+
renderAnalysisSignals(lines, reportAnalysis);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
renderAgentInstructions(lines, outputFiles, allFindings);
|
|
312
|
+
|
|
313
|
+
lines.push('## Architecture Health\n');
|
|
314
|
+
pushPillarSummary(
|
|
315
|
+
'architecture',
|
|
316
|
+
archStats?.totalFindings ?? architectureFindings.length,
|
|
317
|
+
pillarHealth.archHealth,
|
|
318
|
+
'architecture',
|
|
319
|
+
'architecture.json'
|
|
320
|
+
);
|
|
321
|
+
if (depGraph) {
|
|
322
|
+
lines.push(`| Metric | Value |`);
|
|
323
|
+
lines.push(`|--------|-------|`);
|
|
324
|
+
lines.push(`| Modules | ${depGraph.totalModules} |`);
|
|
325
|
+
lines.push(`| Import edges | ${depGraph.totalEdges} |`);
|
|
326
|
+
lines.push(`| Cycles | ${depGraph.cycles?.length ?? 0} |`);
|
|
327
|
+
lines.push(`| Critical paths | ${depGraph.criticalPaths?.length ?? 0} |`);
|
|
328
|
+
lines.push(`| Root modules | ${depGraph.rootsCount} |`);
|
|
329
|
+
lines.push(`| Leaf modules | ${depGraph.leavesCount} |`);
|
|
330
|
+
lines.push(
|
|
331
|
+
`| Test-only modules | ${depGraph.testOnlyModules?.length ?? 0} |`
|
|
332
|
+
);
|
|
333
|
+
lines.push(`| Unresolved imports | ${depGraph.unresolvedEdgeCount} |`);
|
|
334
|
+
lines.push('');
|
|
335
|
+
}
|
|
336
|
+
renderPillarCategories('architecture', architectureFindings);
|
|
337
|
+
|
|
338
|
+
renderHotspots(lines, hotFiles);
|
|
339
|
+
|
|
340
|
+
renderPillarSections(lines, {
|
|
341
|
+
architectureFindings,
|
|
342
|
+
codeQualityFindings,
|
|
343
|
+
deadCodeFindings,
|
|
344
|
+
securityFindings,
|
|
345
|
+
testQualityFindings,
|
|
346
|
+
archStats,
|
|
347
|
+
qualStats,
|
|
348
|
+
deadStats,
|
|
349
|
+
secStats,
|
|
350
|
+
testStats,
|
|
351
|
+
...pillarHealth,
|
|
352
|
+
activeFeatures,
|
|
353
|
+
outputFiles,
|
|
354
|
+
renderPillarCategories,
|
|
355
|
+
pushPillarSummary,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
renderRecommendations(lines, agentOutput);
|
|
359
|
+
|
|
360
|
+
if (outputFiles.astTrees) {
|
|
361
|
+
renderAstTreesSection(lines, dir, outputFiles, root, relativeScanDir, exampleFileFilter);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
renderOutputFilesTable(lines, dir, outputFiles);
|
|
365
|
+
|
|
366
|
+
if (report.parseErrors?.length > 0) {
|
|
367
|
+
lines.push('## Parse Errors\n');
|
|
368
|
+
lines.push(`${report.parseErrors.length} file(s) failed to parse:\n`);
|
|
369
|
+
for (const err of report.parseErrors.slice(0, 10)) {
|
|
370
|
+
lines.push(`- \`${err.file}\`: ${err.message}`);
|
|
371
|
+
}
|
|
372
|
+
lines.push('');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
interface PillarHealthScores {
|
|
379
|
+
overallHealth: number;
|
|
380
|
+
archHealth: number;
|
|
381
|
+
qualHealth: number;
|
|
382
|
+
deadHealth: number;
|
|
383
|
+
secHealth: number;
|
|
384
|
+
testHealth: number;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface FeatureScoreRow {
|
|
388
|
+
category: string;
|
|
389
|
+
pillar: string;
|
|
390
|
+
findings: number;
|
|
391
|
+
affectedFiles: number;
|
|
392
|
+
hotspotHits: number;
|
|
393
|
+
hotspotMaxRisk: number;
|
|
394
|
+
contextPenalty: number;
|
|
395
|
+
severityBreakdown: Record<string, number>;
|
|
396
|
+
score: number;
|
|
397
|
+
grade: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface FeatureScoreContext {
|
|
401
|
+
hotFiles?: import('../types/index.js').HotFile[];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export interface QualityAspectSignal {
|
|
405
|
+
label: string;
|
|
406
|
+
value: string;
|
|
407
|
+
effect: 'positive' | 'negative' | 'neutral';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export interface QualityAspectRating {
|
|
411
|
+
aspect: string;
|
|
412
|
+
label: string;
|
|
413
|
+
weight: number;
|
|
414
|
+
score: number;
|
|
415
|
+
grade: string;
|
|
416
|
+
confidence: 'high' | 'medium' | 'low';
|
|
417
|
+
rationale: string;
|
|
418
|
+
signals: QualityAspectSignal[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface QualityRatingSummary {
|
|
422
|
+
model: string;
|
|
423
|
+
overallScore: number;
|
|
424
|
+
overallGrade: string;
|
|
425
|
+
aspects: QualityAspectRating[];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface QualityAspectContext {
|
|
429
|
+
fileInventory?: FileEntry[];
|
|
430
|
+
hotFiles?: HotFile[];
|
|
431
|
+
reportAnalysis?: ReportAnalysisSummary | null;
|
|
432
|
+
includeTests?: boolean;
|
|
433
|
+
includeGenerated?: boolean;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const SEVERITY_PRESSURE_WEIGHT: Record<Finding['severity'], number> = {
|
|
437
|
+
critical: 1.0,
|
|
438
|
+
high: 0.75,
|
|
439
|
+
medium: 0.45,
|
|
440
|
+
low: 0.2,
|
|
441
|
+
info: 0.05,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const CATEGORY_PRESSURE_WEIGHT: Record<string, number> = {
|
|
445
|
+
'dependency-critical-path': 0.45,
|
|
446
|
+
'broker-module': 0.55,
|
|
447
|
+
'bridge-module': 0.55,
|
|
448
|
+
'distance-from-main-sequence': 0.6,
|
|
449
|
+
'over-abstraction': 0.65,
|
|
450
|
+
'concrete-dependency': 0.65,
|
|
451
|
+
'move-to-caller': 0.35,
|
|
452
|
+
'similar-function-body': 0.55,
|
|
453
|
+
'dead-export': 0.65,
|
|
454
|
+
'semantic-dead-export': 0.6,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
function clampScore(score: number): number {
|
|
458
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function softPenalty(
|
|
462
|
+
ratio: number,
|
|
463
|
+
maxPenalty: number,
|
|
464
|
+
sensitivity: number = 1.4
|
|
465
|
+
): number {
|
|
466
|
+
if (ratio <= 0) return 0;
|
|
467
|
+
return Math.round(maxPenalty * (1 - Math.exp(-ratio * sensitivity)));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function confidenceFromSample(sampleSize: number): 'high' | 'medium' | 'low' {
|
|
471
|
+
if (sampleSize >= 25) return 'high';
|
|
472
|
+
if (sampleSize >= 8) return 'medium';
|
|
473
|
+
return 'low';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function average(values: number[]): number {
|
|
477
|
+
if (values.length === 0) return 0;
|
|
478
|
+
return values.reduce((acc, value) => acc + value, 0) / values.length;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function normalizeScanPath(filePath: string): string {
|
|
482
|
+
return filePath.replace(/\\/g, '/');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isGeneratedLikePath(filePath: string): boolean {
|
|
486
|
+
const normalized = normalizeScanPath(filePath).toLowerCase();
|
|
487
|
+
return (
|
|
488
|
+
/(?:^|\/)(?:dist|build|coverage|out|vendor|vendors|generated|gen|\.cache)(?:\/|$)/.test(
|
|
489
|
+
normalized
|
|
490
|
+
) ||
|
|
491
|
+
/\.min\.(?:js|jsx|mjs|cjs|css)$/i.test(normalized) ||
|
|
492
|
+
/\.bundle\./i.test(normalized)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function shouldIncludeQualityPath(
|
|
497
|
+
filePath: string,
|
|
498
|
+
opts: { includeTests: boolean; includeGenerated: boolean }
|
|
499
|
+
): boolean {
|
|
500
|
+
const normalized = normalizeScanPath(filePath);
|
|
501
|
+
if (!opts.includeTests && isTestFile(normalized)) return false;
|
|
502
|
+
if (!opts.includeGenerated && isGeneratedLikePath(normalized)) return false;
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function findingTouchesIncludedPath(
|
|
507
|
+
finding: Finding,
|
|
508
|
+
opts: { includeTests: boolean; includeGenerated: boolean }
|
|
509
|
+
): boolean {
|
|
510
|
+
const referenced = new Set<string>();
|
|
511
|
+
if (finding.file) referenced.add(finding.file);
|
|
512
|
+
for (const file of finding.files || []) referenced.add(file);
|
|
513
|
+
if (referenced.size === 0) return true;
|
|
514
|
+
for (const file of referenced) {
|
|
515
|
+
if (shouldIncludeQualityPath(file, opts)) return true;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function findingTouchesAnyFile(finding: Finding, files: Set<string>): boolean {
|
|
521
|
+
if (finding.file && files.has(finding.file)) return true;
|
|
522
|
+
return (finding.files || []).some(file => files.has(file));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function classifyFileNameStyle(baseName: string): string {
|
|
526
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(baseName)) return 'kebab';
|
|
527
|
+
if (/^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(baseName)) return 'snake';
|
|
528
|
+
if (/^[a-z]+(?:[A-Z][a-z0-9]*)+$/.test(baseName)) return 'camel';
|
|
529
|
+
if (/^[a-z0-9]+$/.test(baseName)) return 'flat';
|
|
530
|
+
return 'other';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function fileUniverse(
|
|
534
|
+
findings: Finding[],
|
|
535
|
+
fileInventory: FileEntry[]
|
|
536
|
+
): Set<string> {
|
|
537
|
+
const files = new Set<string>();
|
|
538
|
+
for (const entry of fileInventory) files.add(entry.file);
|
|
539
|
+
for (const finding of findings) {
|
|
540
|
+
if (finding.file) files.add(finding.file);
|
|
541
|
+
for (const file of finding.files || []) files.add(file);
|
|
542
|
+
}
|
|
543
|
+
return files;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function weightedFindingPressure(
|
|
547
|
+
findings: Finding[],
|
|
548
|
+
totalFiles: number,
|
|
549
|
+
options: { applyCategoryWeight?: boolean } = {}
|
|
550
|
+
): number {
|
|
551
|
+
if (findings.length === 0) return 0;
|
|
552
|
+
const weightedCount = findings.reduce((sum, finding) => {
|
|
553
|
+
const severityWeight = SEVERITY_PRESSURE_WEIGHT[finding.severity] ?? 0.2;
|
|
554
|
+
const categoryWeight = options.applyCategoryWeight
|
|
555
|
+
? (CATEGORY_PRESSURE_WEIGHT[finding.category] ?? 1)
|
|
556
|
+
: 1;
|
|
557
|
+
return sum + severityWeight * categoryWeight;
|
|
558
|
+
}, 0);
|
|
559
|
+
return weightedCount / Math.max(1, totalFiles);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function stripSharedPathPrefix(filePaths: string[]): string[] {
|
|
563
|
+
if (filePaths.length === 0) return [];
|
|
564
|
+
const segments = filePaths.map(file =>
|
|
565
|
+
normalizeScanPath(file).split('/').filter(Boolean)
|
|
566
|
+
);
|
|
567
|
+
let prefixLen = 0;
|
|
568
|
+
while (true) {
|
|
569
|
+
const token = segments[0][prefixLen];
|
|
570
|
+
if (!token) break;
|
|
571
|
+
if (segments.every(parts => parts[prefixLen] === token)) {
|
|
572
|
+
prefixLen += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return segments.map(parts => {
|
|
579
|
+
const trimmed = parts.slice(prefixLen);
|
|
580
|
+
if (trimmed.length > 0) return trimmed.join('/');
|
|
581
|
+
return parts.join('/');
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function computeQualityAspectRatings(
|
|
586
|
+
findings: Finding[],
|
|
587
|
+
context: QualityAspectContext = {}
|
|
588
|
+
): QualityRatingSummary {
|
|
589
|
+
const includeTests = context.includeTests ?? false;
|
|
590
|
+
const includeGenerated = context.includeGenerated ?? false;
|
|
591
|
+
const filteringOptions = { includeTests, includeGenerated };
|
|
592
|
+
const fileInventory = (context.fileInventory || []).filter(entry =>
|
|
593
|
+
shouldIncludeQualityPath(entry.file, filteringOptions)
|
|
594
|
+
);
|
|
595
|
+
const hotFiles = (context.hotFiles || []).filter(entry =>
|
|
596
|
+
shouldIncludeQualityPath(entry.file, filteringOptions)
|
|
597
|
+
);
|
|
598
|
+
const reportAnalysis = context.reportAnalysis || null;
|
|
599
|
+
const filteredFindings = findings.filter(finding =>
|
|
600
|
+
findingTouchesIncludedPath(finding, filteringOptions)
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const files = fileUniverse(filteredFindings, fileInventory);
|
|
604
|
+
const totalFiles = Math.max(1, files.size);
|
|
605
|
+
const functions = fileInventory.flatMap(entry => entry.functions || []);
|
|
606
|
+
const totalFunctions = Math.max(1, functions.length);
|
|
607
|
+
|
|
608
|
+
const findingsByPillar = {
|
|
609
|
+
architecture: filteredFindings.filter(
|
|
610
|
+
finding => (CATEGORY_PILLAR_MAP[finding.category] || 'unmapped') === 'architecture'
|
|
611
|
+
),
|
|
612
|
+
codeQuality: filteredFindings.filter(
|
|
613
|
+
finding => (CATEGORY_PILLAR_MAP[finding.category] || 'unmapped') === 'code-quality'
|
|
614
|
+
),
|
|
615
|
+
deadCode: filteredFindings.filter(
|
|
616
|
+
finding => (CATEGORY_PILLAR_MAP[finding.category] || 'unmapped') === 'dead-code'
|
|
617
|
+
),
|
|
618
|
+
testQuality: filteredFindings.filter(
|
|
619
|
+
finding => (CATEGORY_PILLAR_MAP[finding.category] || 'unmapped') === 'test-quality'
|
|
620
|
+
),
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const severeFindings = filteredFindings.filter(
|
|
624
|
+
finding => finding.severity === 'critical' || finding.severity === 'high'
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const aspects: QualityAspectRating[] = [];
|
|
628
|
+
|
|
629
|
+
const architectureSevereFindings = findingsByPillar.architecture.filter(
|
|
630
|
+
finding => finding.severity === 'critical' || finding.severity === 'high'
|
|
631
|
+
);
|
|
632
|
+
const architectureDensity = weightedFindingPressure(
|
|
633
|
+
findingsByPillar.architecture,
|
|
634
|
+
totalFiles,
|
|
635
|
+
{ applyCategoryWeight: true }
|
|
636
|
+
);
|
|
637
|
+
const architectureSevereDensity = weightedFindingPressure(
|
|
638
|
+
architectureSevereFindings,
|
|
639
|
+
totalFiles,
|
|
640
|
+
{ applyCategoryWeight: true }
|
|
641
|
+
);
|
|
642
|
+
const cycleDensity = weightedFindingPressure(
|
|
643
|
+
findingsByPillar.architecture.filter(
|
|
644
|
+
finding =>
|
|
645
|
+
finding.category === 'dependency-cycle'
|
|
646
|
+
|| finding.category === 'cycle-cluster'
|
|
647
|
+
),
|
|
648
|
+
totalFiles,
|
|
649
|
+
{ applyCategoryWeight: true }
|
|
650
|
+
);
|
|
651
|
+
const hotspotSample = [...hotFiles]
|
|
652
|
+
.sort((a, b) => b.riskScore - a.riskScore)
|
|
653
|
+
.slice(0, 8);
|
|
654
|
+
const hotspotPressure = Math.min(
|
|
655
|
+
1,
|
|
656
|
+
average(hotspotSample.map(entry => Math.max(0, entry.riskScore))) / 100
|
|
657
|
+
);
|
|
658
|
+
const signalConfidenceBoost = reportAnalysis?.strongestGraphSignal?.confidence === 'high'
|
|
659
|
+
? 3
|
|
660
|
+
: reportAnalysis?.strongestGraphSignal?.confidence === 'medium'
|
|
661
|
+
? 1
|
|
662
|
+
: 0;
|
|
663
|
+
const architectureScore = clampScore(
|
|
664
|
+
100
|
|
665
|
+
- softPenalty(architectureDensity, 24, 1.35)
|
|
666
|
+
- softPenalty(architectureSevereDensity, 22, 1.9)
|
|
667
|
+
- softPenalty(cycleDensity, 16, 2.2)
|
|
668
|
+
- softPenalty(hotspotPressure, 12, 1.3)
|
|
669
|
+
+ signalConfidenceBoost
|
|
670
|
+
);
|
|
671
|
+
aspects.push({
|
|
672
|
+
aspect: 'architecture-structure',
|
|
673
|
+
label: 'Architecture & Structure',
|
|
674
|
+
weight: 30,
|
|
675
|
+
score: architectureScore,
|
|
676
|
+
grade: gradeScore(architectureScore),
|
|
677
|
+
confidence: confidenceFromSample(findingsByPillar.architecture.length),
|
|
678
|
+
rationale:
|
|
679
|
+
'Rates structural integrity using architecture findings, severity concentration, cycle pressure, and hotspot intensity, then tempers it with AI graph-signal confidence.',
|
|
680
|
+
signals: [
|
|
681
|
+
{
|
|
682
|
+
label: 'Architecture findings / file',
|
|
683
|
+
value: architectureDensity.toFixed(2),
|
|
684
|
+
effect: findingsByPillar.architecture.length > 0 ? 'negative' : 'neutral',
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
label: 'Severe architecture findings',
|
|
688
|
+
value: architectureSevereDensity.toFixed(2),
|
|
689
|
+
effect: architectureSevereFindings.length > 0 ? 'negative' : 'neutral',
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
label: 'Hotspot pressure (avg risk top files)',
|
|
693
|
+
value: average(hotspotSample.map(entry => entry.riskScore)).toFixed(1),
|
|
694
|
+
effect: hotspotSample.length > 0 ? 'negative' : 'neutral',
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const filePaths = [...files];
|
|
700
|
+
const topologyPaths = stripSharedPathPrefix(filePaths);
|
|
701
|
+
const depthValues = topologyPaths.map(file =>
|
|
702
|
+
normalizeScanPath(file).split('/').filter(Boolean).length
|
|
703
|
+
);
|
|
704
|
+
const avgDepth = average(depthValues);
|
|
705
|
+
const topLevelCounts = new Map<string, number>();
|
|
706
|
+
const dirSegments = new Set<string>();
|
|
707
|
+
const vagueDirPattern = /^(util|utils|common|shared|misc|helper|helpers|tmp|temp)$/i;
|
|
708
|
+
for (const file of topologyPaths) {
|
|
709
|
+
const normalized = normalizeScanPath(file);
|
|
710
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
711
|
+
const top = parts[0] || '.';
|
|
712
|
+
topLevelCounts.set(top, (topLevelCounts.get(top) || 0) + 1);
|
|
713
|
+
for (const segment of parts.slice(0, -1)) dirSegments.add(segment);
|
|
714
|
+
}
|
|
715
|
+
const dominantRootRatio =
|
|
716
|
+
topLevelCounts.size === 0
|
|
717
|
+
? 0
|
|
718
|
+
: Math.max(...topLevelCounts.values()) / totalFiles;
|
|
719
|
+
const vagueDirRatio =
|
|
720
|
+
dirSegments.size === 0
|
|
721
|
+
? 0
|
|
722
|
+
: [...dirSegments].filter(segment => vagueDirPattern.test(segment)).length
|
|
723
|
+
/ dirSegments.size;
|
|
724
|
+
const folderScore = clampScore(
|
|
725
|
+
100
|
|
726
|
+
- softPenalty(Math.max(0, (avgDepth - 4) / 3), 20, 1.3)
|
|
727
|
+
- softPenalty(Math.max(0, dominantRootRatio - 0.55), 18, 2.0)
|
|
728
|
+
- softPenalty(vagueDirRatio, 24, 2.2)
|
|
729
|
+
);
|
|
730
|
+
aspects.push({
|
|
731
|
+
aspect: 'folder-topology',
|
|
732
|
+
label: 'Folder Topology',
|
|
733
|
+
weight: 15,
|
|
734
|
+
score: folderScore,
|
|
735
|
+
grade: gradeScore(folderScore),
|
|
736
|
+
confidence: confidenceFromSample(filePaths.length),
|
|
737
|
+
rationale:
|
|
738
|
+
'Rates how navigable the folder model is by blending depth balance, top-level concentration, and reliance on vague utility/common directories.',
|
|
739
|
+
signals: [
|
|
740
|
+
{
|
|
741
|
+
label: 'Average path depth',
|
|
742
|
+
value: avgDepth.toFixed(1),
|
|
743
|
+
effect: avgDepth > 6 ? 'negative' : 'neutral',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
label: 'Dominant root share',
|
|
747
|
+
value: `${Math.round(dominantRootRatio * 100)}%`,
|
|
748
|
+
effect: dominantRootRatio > 0.65 ? 'negative' : 'neutral',
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
label: 'Vague directory ratio',
|
|
752
|
+
value: `${Math.round(vagueDirRatio * 100)}%`,
|
|
753
|
+
effect: vagueDirRatio > 0.2 ? 'negative' : 'neutral',
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const genericNamePattern =
|
|
759
|
+
/^(foo|bar|baz|tmp|temp|data|value|handler|util|helper|thing|stuff|fn|func)$/i;
|
|
760
|
+
const genericFilePattern =
|
|
761
|
+
/^(utils?|helpers?|common|shared|misc|tmp|temp|new|old|types?)$/i;
|
|
762
|
+
const namedFunctions = functions
|
|
763
|
+
.map(fn => fn.name || fn.nameHint || '')
|
|
764
|
+
.filter(name => name.length > 0);
|
|
765
|
+
const anonymousCount = namedFunctions.filter(
|
|
766
|
+
name => name === '<anonymous>' || name === 'default'
|
|
767
|
+
).length;
|
|
768
|
+
const explicitNamed = namedFunctions.filter(
|
|
769
|
+
name => name !== '<anonymous>' && name !== 'default'
|
|
770
|
+
);
|
|
771
|
+
const genericFunctionCount = explicitNamed.filter(name =>
|
|
772
|
+
genericNamePattern.test(name)
|
|
773
|
+
).length;
|
|
774
|
+
const shortFunctionCount = explicitNamed.filter(name => name.length <= 2).length;
|
|
775
|
+
const fileBaseNames = filePaths.map(file => path.basename(file, path.extname(file)));
|
|
776
|
+
const genericFileCount = fileBaseNames.filter(name =>
|
|
777
|
+
genericFilePattern.test(name)
|
|
778
|
+
).length;
|
|
779
|
+
const namingScore = clampScore(
|
|
780
|
+
100
|
|
781
|
+
- softPenalty(anonymousCount / totalFunctions, 30, 2.3)
|
|
782
|
+
- softPenalty(genericFunctionCount / Math.max(1, explicitNamed.length), 24, 2.0)
|
|
783
|
+
- softPenalty(genericFileCount / totalFiles, 15, 1.8)
|
|
784
|
+
- softPenalty(shortFunctionCount / Math.max(1, explicitNamed.length), 10, 1.8)
|
|
785
|
+
);
|
|
786
|
+
aspects.push({
|
|
787
|
+
aspect: 'naming-quality',
|
|
788
|
+
label: 'Naming Quality',
|
|
789
|
+
weight: 15,
|
|
790
|
+
score: namingScore,
|
|
791
|
+
grade: gradeScore(namingScore),
|
|
792
|
+
confidence: confidenceFromSample(functions.length),
|
|
793
|
+
rationale:
|
|
794
|
+
'Rates naming clarity by balancing anonymous/generic function names, short ambiguous names, and generic file basenames.',
|
|
795
|
+
signals: [
|
|
796
|
+
{
|
|
797
|
+
label: 'Anonymous function share',
|
|
798
|
+
value: `${Math.round((anonymousCount / totalFunctions) * 100)}%`,
|
|
799
|
+
effect: anonymousCount > 0 ? 'negative' : 'neutral',
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
label: 'Generic function names',
|
|
803
|
+
value: String(genericFunctionCount),
|
|
804
|
+
effect: genericFunctionCount > 0 ? 'negative' : 'neutral',
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
label: 'Generic file names',
|
|
808
|
+
value: String(genericFileCount),
|
|
809
|
+
effect: genericFileCount > 0 ? 'negative' : 'neutral',
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const sharedPathPattern = /(^|\/)(common|shared|utils?|lib|core)(\/|$)/i;
|
|
815
|
+
const sharedFiles = fileInventory.filter(entry =>
|
|
816
|
+
sharedPathPattern.test(entry.file.replace(/\\/g, '/'))
|
|
817
|
+
);
|
|
818
|
+
const sharedFileSet = new Set(sharedFiles.map(entry => entry.file));
|
|
819
|
+
const sharedFindings = filteredFindings.filter(finding =>
|
|
820
|
+
findingTouchesAnyFile(finding, sharedFileSet)
|
|
821
|
+
);
|
|
822
|
+
const sharedSevere = sharedFindings.filter(
|
|
823
|
+
finding => finding.severity === 'critical' || finding.severity === 'high'
|
|
824
|
+
);
|
|
825
|
+
const sharedImportPressure = average(
|
|
826
|
+
sharedFiles.map(entry => {
|
|
827
|
+
const internalImports =
|
|
828
|
+
entry.symbolUsageSummary?.internalImportCount
|
|
829
|
+
?? entry.dependencyProfile.importedSymbols.filter(ref => !!ref.resolvedModule).length;
|
|
830
|
+
const declaredExports =
|
|
831
|
+
entry.symbolUsageSummary?.declaredExportCount
|
|
832
|
+
?? entry.dependencyProfile.declaredExports.length;
|
|
833
|
+
return internalImports / (declaredExports + 1);
|
|
834
|
+
})
|
|
835
|
+
);
|
|
836
|
+
const commonScore = sharedFiles.length === 0
|
|
837
|
+
? 88
|
|
838
|
+
: clampScore(
|
|
839
|
+
100
|
|
840
|
+
- softPenalty(sharedFindings.length / sharedFiles.length, 24, 1.6)
|
|
841
|
+
- softPenalty(sharedSevere.length / sharedFiles.length, 28, 2.2)
|
|
842
|
+
- softPenalty(sharedImportPressure, 18, 1.5)
|
|
843
|
+
);
|
|
844
|
+
aspects.push({
|
|
845
|
+
aspect: 'common-layer-health',
|
|
846
|
+
label: 'Common/Shared Layer Health',
|
|
847
|
+
weight: 15,
|
|
848
|
+
score: commonScore,
|
|
849
|
+
grade: gradeScore(commonScore),
|
|
850
|
+
confidence: confidenceFromSample(sharedFiles.length),
|
|
851
|
+
rationale:
|
|
852
|
+
sharedFiles.length === 0
|
|
853
|
+
? 'No explicit common/shared layer was detected, so this aspect is neutral-positive by default.'
|
|
854
|
+
: 'Rates whether shared/common code stays stable and lightweight by combining finding density, severe issue concentration, and internal dependency pressure.',
|
|
855
|
+
signals: [
|
|
856
|
+
{
|
|
857
|
+
label: 'Shared files',
|
|
858
|
+
value: String(sharedFiles.length),
|
|
859
|
+
effect: sharedFiles.length === 0 ? 'neutral' : 'positive',
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
label: 'Shared-layer findings',
|
|
863
|
+
value: String(sharedFindings.length),
|
|
864
|
+
effect: sharedFindings.length > 0 ? 'negative' : 'neutral',
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
label: 'Shared import pressure',
|
|
868
|
+
value: sharedImportPressure.toFixed(2),
|
|
869
|
+
effect: sharedImportPressure > 1 ? 'negative' : 'neutral',
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
const maintainabilityFindings = [
|
|
875
|
+
...findingsByPillar.codeQuality,
|
|
876
|
+
...findingsByPillar.deadCode,
|
|
877
|
+
...findingsByPillar.testQuality,
|
|
878
|
+
];
|
|
879
|
+
const testDebtCategories = new Set([
|
|
880
|
+
'test-no-assertion',
|
|
881
|
+
'low-assertion-density',
|
|
882
|
+
'excessive-mocking',
|
|
883
|
+
'missing-test-cleanup',
|
|
884
|
+
'focused-test',
|
|
885
|
+
'fake-timer-no-restore',
|
|
886
|
+
'missing-mock-restoration',
|
|
887
|
+
]);
|
|
888
|
+
const testDebtFindings = filteredFindings.filter(finding =>
|
|
889
|
+
testDebtCategories.has(finding.category)
|
|
890
|
+
);
|
|
891
|
+
const avgCognitiveComplexity = average(
|
|
892
|
+
functions.map(fn => fn.cognitiveComplexity || fn.complexity || 0)
|
|
893
|
+
);
|
|
894
|
+
const maintainabilityDensity = weightedFindingPressure(
|
|
895
|
+
maintainabilityFindings,
|
|
896
|
+
totalFiles,
|
|
897
|
+
{ applyCategoryWeight: true }
|
|
898
|
+
);
|
|
899
|
+
const severeDensity = weightedFindingPressure(severeFindings, totalFiles);
|
|
900
|
+
const testDebtDensity = weightedFindingPressure(testDebtFindings, totalFiles, {
|
|
901
|
+
applyCategoryWeight: true,
|
|
902
|
+
});
|
|
903
|
+
const maintainabilityScore = clampScore(
|
|
904
|
+
100
|
|
905
|
+
- softPenalty(maintainabilityDensity, 20, 1.3)
|
|
906
|
+
- softPenalty(severeDensity, 22, 1.8)
|
|
907
|
+
- softPenalty(Math.max(0, (avgCognitiveComplexity - 8) / 12), 22, 1.4)
|
|
908
|
+
- softPenalty(testDebtDensity, 12, 1.5)
|
|
909
|
+
);
|
|
910
|
+
aspects.push({
|
|
911
|
+
aspect: 'maintainability-evolvability',
|
|
912
|
+
label: 'Maintainability & Evolvability',
|
|
913
|
+
weight: 15,
|
|
914
|
+
score: maintainabilityScore,
|
|
915
|
+
grade: gradeScore(maintainabilityScore),
|
|
916
|
+
confidence: confidenceFromSample(maintainabilityFindings.length),
|
|
917
|
+
rationale:
|
|
918
|
+
'Rates how safely the codebase can evolve by blending quality/dead-code/test debt density, severe issue concentration, and cognitive complexity pressure.',
|
|
919
|
+
signals: [
|
|
920
|
+
{
|
|
921
|
+
label: 'Maintainability findings / file',
|
|
922
|
+
value: maintainabilityDensity.toFixed(2),
|
|
923
|
+
effect: maintainabilityFindings.length > 0 ? 'negative' : 'neutral',
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
label: 'Average cognitive complexity',
|
|
927
|
+
value: avgCognitiveComplexity.toFixed(1),
|
|
928
|
+
effect: avgCognitiveComplexity > 12 ? 'negative' : 'neutral',
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
label: 'Test debt findings',
|
|
932
|
+
value: String(testDebtFindings.length),
|
|
933
|
+
effect: testDebtFindings.length > 0 ? 'negative' : 'neutral',
|
|
934
|
+
},
|
|
935
|
+
],
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
const styleCounts = new Map<string, number>();
|
|
939
|
+
for (const baseName of fileBaseNames) {
|
|
940
|
+
const style = classifyFileNameStyle(baseName);
|
|
941
|
+
styleCounts.set(style, (styleCounts.get(style) || 0) + 1);
|
|
942
|
+
}
|
|
943
|
+
const dominantStyleRatio =
|
|
944
|
+
styleCounts.size === 0 ? 1 : Math.max(...styleCounts.values()) / totalFiles;
|
|
945
|
+
const tsFileCount = filePaths.filter(file => /\.(ts|tsx)$/i.test(file)).length;
|
|
946
|
+
const jsFileCount = filePaths.filter(file => /\.(js|jsx|mjs|cjs)$/i.test(file)).length;
|
|
947
|
+
const mixedExtensionRatio = (Math.min(tsFileCount, jsFileCount) / totalFiles) * 2;
|
|
948
|
+
const consistencyScore = clampScore(
|
|
949
|
+
100
|
|
950
|
+
- softPenalty(1 - dominantStyleRatio, 24, 2.0)
|
|
951
|
+
- softPenalty(mixedExtensionRatio, 12, 1.6)
|
|
952
|
+
- softPenalty(genericFileCount / totalFiles, 10, 1.6)
|
|
953
|
+
);
|
|
954
|
+
aspects.push({
|
|
955
|
+
aspect: 'codebase-consistency',
|
|
956
|
+
label: 'Codebase Consistency',
|
|
957
|
+
weight: 10,
|
|
958
|
+
score: consistencyScore,
|
|
959
|
+
grade: gradeScore(consistencyScore),
|
|
960
|
+
confidence: confidenceFromSample(filePaths.length),
|
|
961
|
+
rationale:
|
|
962
|
+
'Rates naming/structure consistency with soft penalties for mixed filename styles, mixed TS/JS surface area, and generic file naming concentration.',
|
|
963
|
+
signals: [
|
|
964
|
+
{
|
|
965
|
+
label: 'Dominant naming style',
|
|
966
|
+
value: `${Math.round(dominantStyleRatio * 100)}%`,
|
|
967
|
+
effect: dominantStyleRatio >= 0.7 ? 'positive' : 'negative',
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
label: 'TS/JS mix ratio',
|
|
971
|
+
value: `${Math.round((Math.min(tsFileCount, jsFileCount) / totalFiles) * 100)}%`,
|
|
972
|
+
effect: mixedExtensionRatio > 0.5 ? 'negative' : 'neutral',
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
label: 'Detected file naming styles',
|
|
976
|
+
value: String(styleCounts.size),
|
|
977
|
+
effect: styleCounts.size > 3 ? 'negative' : 'neutral',
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const totalWeight = aspects.reduce((sum, aspect) => sum + aspect.weight, 0) || 1;
|
|
983
|
+
const weightedScore =
|
|
984
|
+
aspects.reduce((sum, aspect) => sum + aspect.score * aspect.weight, 0)
|
|
985
|
+
/ totalWeight;
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
model: 'hybrid-ai-structure-v1',
|
|
989
|
+
overallScore: clampScore(weightedScore),
|
|
990
|
+
overallGrade: gradeScore(clampScore(weightedScore)),
|
|
991
|
+
aspects,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function estimatePillarFileCount(
|
|
996
|
+
totalFiles: number,
|
|
997
|
+
findings: Finding[]
|
|
998
|
+
): number {
|
|
999
|
+
if (totalFiles <= 0) return 0;
|
|
1000
|
+
const coveredFiles = new Set<string>();
|
|
1001
|
+
for (const finding of findings) {
|
|
1002
|
+
if (finding.file) coveredFiles.add(finding.file);
|
|
1003
|
+
for (const file of finding.files ?? []) coveredFiles.add(file);
|
|
1004
|
+
}
|
|
1005
|
+
if (coveredFiles.size === 0) return totalFiles;
|
|
1006
|
+
const floor = Math.max(1, Math.ceil(totalFiles * 0.1));
|
|
1007
|
+
return Math.max(floor, Math.min(totalFiles, coveredFiles.size));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function computePillarHealthScores(
|
|
1011
|
+
totalFiles: number,
|
|
1012
|
+
overallFindingStats: { totalFindings: number; severityBreakdown: Record<string, number> },
|
|
1013
|
+
ctx: {
|
|
1014
|
+
archStats?: { severityBreakdown: Record<string, number> };
|
|
1015
|
+
qualStats?: { severityBreakdown: Record<string, number> };
|
|
1016
|
+
deadStats?: { severityBreakdown: Record<string, number> };
|
|
1017
|
+
secStats?: { severityBreakdown: Record<string, number> };
|
|
1018
|
+
testStats?: { severityBreakdown: Record<string, number> };
|
|
1019
|
+
architectureFindings: Finding[];
|
|
1020
|
+
codeQualityFindings: Finding[];
|
|
1021
|
+
deadCodeFindings: Finding[];
|
|
1022
|
+
securityFindings: Finding[];
|
|
1023
|
+
testQualityFindings: Finding[];
|
|
1024
|
+
}
|
|
1025
|
+
): PillarHealthScores {
|
|
1026
|
+
const score = (
|
|
1027
|
+
stats: { severityBreakdown: Record<string, number> } | undefined,
|
|
1028
|
+
fallback: Finding[],
|
|
1029
|
+
pillarFiles: number
|
|
1030
|
+
) => computeHealthScoreFromSeverityBreakdown(
|
|
1031
|
+
stats?.severityBreakdown ?? severityBreakdown(fallback),
|
|
1032
|
+
pillarFiles
|
|
1033
|
+
);
|
|
1034
|
+
const archFiles = estimatePillarFileCount(totalFiles, ctx.architectureFindings);
|
|
1035
|
+
const qualFiles = estimatePillarFileCount(totalFiles, ctx.codeQualityFindings);
|
|
1036
|
+
const deadFiles = estimatePillarFileCount(totalFiles, ctx.deadCodeFindings);
|
|
1037
|
+
const secFiles = estimatePillarFileCount(totalFiles, ctx.securityFindings);
|
|
1038
|
+
const testFiles = estimatePillarFileCount(totalFiles, ctx.testQualityFindings);
|
|
1039
|
+
return {
|
|
1040
|
+
overallHealth: computeHealthScoreFromSeverityBreakdown(overallFindingStats.severityBreakdown, totalFiles),
|
|
1041
|
+
archHealth: score(ctx.archStats, ctx.architectureFindings, archFiles),
|
|
1042
|
+
qualHealth: score(ctx.qualStats, ctx.codeQualityFindings, qualFiles),
|
|
1043
|
+
deadHealth: score(ctx.deadStats, ctx.deadCodeFindings, deadFiles),
|
|
1044
|
+
secHealth: score(ctx.secStats, ctx.securityFindings, secFiles),
|
|
1045
|
+
testHealth: score(ctx.testStats, ctx.testQualityFindings, testFiles),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function gradeScore(s: number): string {
|
|
1050
|
+
return s >= 80 ? 'A' : s >= 60 ? 'B' : s >= 40 ? 'C' : s >= 20 ? 'D' : 'F';
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function resolveScoredCategories(activeFeatures: Set<string> | null): string[] {
|
|
1054
|
+
const ordered = Object.values(PILLAR_CATEGORIES).flat();
|
|
1055
|
+
if (!activeFeatures) return ordered;
|
|
1056
|
+
return ordered.filter(category => activeFeatures.has(category));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function computeFeatureScores(
|
|
1060
|
+
findings: Finding[],
|
|
1061
|
+
totalFiles: number,
|
|
1062
|
+
activeFeatures: Set<string> | null,
|
|
1063
|
+
context: FeatureScoreContext = {}
|
|
1064
|
+
): FeatureScoreRow[] {
|
|
1065
|
+
const hotFileRisk = new Map<string, number>();
|
|
1066
|
+
for (const hf of context.hotFiles || []) {
|
|
1067
|
+
hotFileRisk.set(hf.file, hf.riskScore);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const categories = resolveScoredCategories(activeFeatures);
|
|
1071
|
+
const seenCategories = new Set(categories);
|
|
1072
|
+
for (const finding of findings) {
|
|
1073
|
+
if (!seenCategories.has(finding.category)) {
|
|
1074
|
+
if (!activeFeatures || activeFeatures.has(finding.category)) {
|
|
1075
|
+
categories.push(finding.category);
|
|
1076
|
+
seenCategories.add(finding.category);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const findingsByCategory = new Map<string, Finding[]>();
|
|
1082
|
+
for (const finding of findings) {
|
|
1083
|
+
if (!findingsByCategory.has(finding.category)) {
|
|
1084
|
+
findingsByCategory.set(finding.category, []);
|
|
1085
|
+
}
|
|
1086
|
+
findingsByCategory.get(finding.category)!.push(finding);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return categories
|
|
1090
|
+
.map(category => {
|
|
1091
|
+
const categoryFindings = findingsByCategory.get(category) || [];
|
|
1092
|
+
const breakdown = severityBreakdown(categoryFindings);
|
|
1093
|
+
const affected = new Set<string>();
|
|
1094
|
+
for (const finding of categoryFindings) {
|
|
1095
|
+
if (finding.file) affected.add(finding.file);
|
|
1096
|
+
for (const file of finding.files ?? []) affected.add(file);
|
|
1097
|
+
}
|
|
1098
|
+
const denominator =
|
|
1099
|
+
affected.size > 0 ? Math.max(1, affected.size) : Math.max(1, totalFiles);
|
|
1100
|
+
const baseScore = computeHealthScoreFromSeverityBreakdown(
|
|
1101
|
+
breakdown,
|
|
1102
|
+
denominator
|
|
1103
|
+
);
|
|
1104
|
+
let hotspotHits = 0;
|
|
1105
|
+
let hotspotMaxRisk = 0;
|
|
1106
|
+
for (const file of affected) {
|
|
1107
|
+
const risk = hotFileRisk.get(file) || 0;
|
|
1108
|
+
if (risk <= 0) continue;
|
|
1109
|
+
hotspotHits += 1;
|
|
1110
|
+
hotspotMaxRisk = Math.max(hotspotMaxRisk, risk);
|
|
1111
|
+
}
|
|
1112
|
+
const overlapRatio = affected.size > 0 ? hotspotHits / affected.size : 0;
|
|
1113
|
+
const riskWeight = hotspotMaxRisk >= 90
|
|
1114
|
+
? 10
|
|
1115
|
+
: hotspotMaxRisk >= 75
|
|
1116
|
+
? 7
|
|
1117
|
+
: hotspotMaxRisk >= 60
|
|
1118
|
+
? 4
|
|
1119
|
+
: 2;
|
|
1120
|
+
const contextPenalty = hotspotHits === 0
|
|
1121
|
+
? 0
|
|
1122
|
+
: Math.min(20, Math.round(overlapRatio * 10 + riskWeight));
|
|
1123
|
+
const score = Math.max(0, baseScore - contextPenalty);
|
|
1124
|
+
return {
|
|
1125
|
+
category,
|
|
1126
|
+
pillar: CATEGORY_PILLAR_MAP[category] || 'unmapped',
|
|
1127
|
+
findings: categoryFindings.length,
|
|
1128
|
+
affectedFiles: affected.size,
|
|
1129
|
+
hotspotHits,
|
|
1130
|
+
hotspotMaxRisk,
|
|
1131
|
+
contextPenalty,
|
|
1132
|
+
severityBreakdown: breakdown,
|
|
1133
|
+
score,
|
|
1134
|
+
grade: gradeScore(score),
|
|
1135
|
+
};
|
|
1136
|
+
})
|
|
1137
|
+
.sort((a, b) => {
|
|
1138
|
+
if (a.score !== b.score) return a.score - b.score;
|
|
1139
|
+
if (a.findings !== b.findings) return b.findings - a.findings;
|
|
1140
|
+
return a.category.localeCompare(b.category);
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function renderHealthScores(
|
|
1145
|
+
lines: string[],
|
|
1146
|
+
health: PillarHealthScores,
|
|
1147
|
+
activeFeatures: Set<string> | null
|
|
1148
|
+
): void {
|
|
1149
|
+
lines.push('## Health Scores\n');
|
|
1150
|
+
lines.push('| Pillar | Score | Grade |');
|
|
1151
|
+
lines.push('|--------|-------|-------|');
|
|
1152
|
+
const pushRow = (label: string, pillarKey: string, score: number): void => {
|
|
1153
|
+
if (!isPillarActive(pillarKey, activeFeatures)) {
|
|
1154
|
+
lines.push(`| ${label} | — | skipped |`);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
lines.push(`| ${label} | ${score}/100 | ${gradeScore(score)} |`);
|
|
1158
|
+
};
|
|
1159
|
+
lines.push(
|
|
1160
|
+
`| **Overall** | **${health.overallHealth}/100** | **${gradeScore(health.overallHealth)}** |`
|
|
1161
|
+
);
|
|
1162
|
+
pushRow('Architecture', 'architecture', health.archHealth);
|
|
1163
|
+
pushRow('Code Quality', 'code-quality', health.qualHealth);
|
|
1164
|
+
pushRow('Dead Code & Hygiene', 'dead-code', health.deadHealth);
|
|
1165
|
+
pushRow('Security', 'security', health.secHealth);
|
|
1166
|
+
pushRow('Test Quality', 'test-quality', health.testHealth);
|
|
1167
|
+
lines.push('');
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function renderFeatureScores(lines: string[], rows: FeatureScoreRow[]): void {
|
|
1171
|
+
lines.push('## Feature Scores\n');
|
|
1172
|
+
lines.push(
|
|
1173
|
+
'Per-category scoring for all active features (or all categories when unfiltered).\n'
|
|
1174
|
+
);
|
|
1175
|
+
lines.push(
|
|
1176
|
+
'| Category | Pillar | Findings | Affected Files | Hotspot Hits | Context Penalty | Score | Grade |'
|
|
1177
|
+
);
|
|
1178
|
+
lines.push(
|
|
1179
|
+
'|----------|--------|----------|----------------|--------------|-----------------|-------|-------|'
|
|
1180
|
+
);
|
|
1181
|
+
for (const row of rows) {
|
|
1182
|
+
lines.push(
|
|
1183
|
+
`| \`${row.category}\` | ${row.pillar} | ${row.findings} | ${row.affectedFiles} | ${row.hotspotHits} | -${row.contextPenalty} | ${row.score}/100 | ${row.grade} |`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
lines.push('');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function renderQualityAspectRatings(
|
|
1190
|
+
lines: string[],
|
|
1191
|
+
rating: QualityRatingSummary
|
|
1192
|
+
): void {
|
|
1193
|
+
lines.push('## AI + Structure Ratings\n');
|
|
1194
|
+
lines.push(
|
|
1195
|
+
'Hybrid, soft-signal scoring that blends structural findings with architecture context, naming quality, folder topology, and shared-layer health.\n'
|
|
1196
|
+
);
|
|
1197
|
+
lines.push(
|
|
1198
|
+
`**Overall Hybrid Rating**: ${rating.overallScore}/100 (${rating.overallGrade}) `
|
|
1199
|
+
);
|
|
1200
|
+
lines.push(`**Model**: \`${rating.model}\`\n`);
|
|
1201
|
+
lines.push(
|
|
1202
|
+
'| Aspect | Weight | Score | Grade | Confidence | Why it scored this way |'
|
|
1203
|
+
);
|
|
1204
|
+
lines.push(
|
|
1205
|
+
'|--------|--------|-------|-------|------------|------------------------|'
|
|
1206
|
+
);
|
|
1207
|
+
for (const aspect of rating.aspects) {
|
|
1208
|
+
lines.push(
|
|
1209
|
+
`| ${aspect.label} | ${aspect.weight}% | ${aspect.score}/100 | ${aspect.grade} | ${aspect.confidence} | ${aspect.rationale} |`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
lines.push('');
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function buildPillarSummaryPusher(
|
|
1216
|
+
lines: string[],
|
|
1217
|
+
activeFeatures: Set<string> | null,
|
|
1218
|
+
outputFiles: Record<string, string>
|
|
1219
|
+
): (pillarKey: string, count: number, score: number, artifactKey?: string, artifactName?: string) => void {
|
|
1220
|
+
return (pillarKey, findingsCount, score, artifactKey, artifactName): void => {
|
|
1221
|
+
if (!isPillarActive(pillarKey, activeFeatures)) {
|
|
1222
|
+
lines.push('> skipped by feature filter\n');
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (artifactKey && outputFiles[artifactKey]) {
|
|
1226
|
+
lines.push(
|
|
1227
|
+
`> ${findingsCount} findings (score: ${score}/100) — see [\`${artifactName}\`](./${outputFiles[artifactKey]})\n`
|
|
1228
|
+
);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (artifactName) {
|
|
1232
|
+
lines.push(
|
|
1233
|
+
`> ${findingsCount} findings (score: ${score}/100) — no \`${artifactName}\` written for this scan\n`
|
|
1234
|
+
);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
lines.push(`> ${findingsCount} findings (score: ${score}/100)\n`);
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function renderScanAnnotations(
|
|
1242
|
+
lines: string[],
|
|
1243
|
+
ctx: {
|
|
1244
|
+
allFindings: Finding[];
|
|
1245
|
+
overallFindingStats: { totalFindings: number; severityBreakdown: Record<string, number> };
|
|
1246
|
+
agentOutput: AgentOutputData;
|
|
1247
|
+
activeFeatures: Set<string> | null;
|
|
1248
|
+
scope: string[] | null;
|
|
1249
|
+
root: string;
|
|
1250
|
+
scopeSymbols: Map<string, string[]> | null;
|
|
1251
|
+
semanticEnabled: boolean;
|
|
1252
|
+
}
|
|
1253
|
+
): void {
|
|
1254
|
+
const { allFindings, overallFindingStats, agentOutput } = ctx;
|
|
1255
|
+
const totalBefore: number | undefined =
|
|
1256
|
+
overallFindingStats.totalFindings || agentOutput?.totalBeforeTruncation;
|
|
1257
|
+
const dropped = agentOutput?.droppedCategories;
|
|
1258
|
+
if (totalBefore && totalBefore > allFindings.length) {
|
|
1259
|
+
lines.push(
|
|
1260
|
+
`> **Truncated**: Showing ${allFindings.length} of ${totalBefore} findings (\`--findings-limit ${allFindings.length}\`).`
|
|
1261
|
+
);
|
|
1262
|
+
if (dropped && dropped.length > 0) {
|
|
1263
|
+
lines.push(`> Dropped categories: ${dropped.map(c => `\`${c}\``).join(', ')}`);
|
|
1264
|
+
}
|
|
1265
|
+
lines.push('');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (ctx.activeFeatures) {
|
|
1269
|
+
const featureLabels = summarizeActiveFeatures(ctx.activeFeatures);
|
|
1270
|
+
lines.push(`> **Features filter**: \`--features=${featureLabels.join(',')}\``);
|
|
1271
|
+
lines.push('');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (ctx.scope && ctx.scope.length > 0) {
|
|
1275
|
+
const scopeDisplay = ctx.scope.map(s => path.relative(ctx.root, s)).filter(Boolean);
|
|
1276
|
+
if (scopeDisplay.length > 0) {
|
|
1277
|
+
let scopeLabel = scopeDisplay.map(p => `\`${p}\``).join(', ');
|
|
1278
|
+
if (ctx.scopeSymbols && ctx.scopeSymbols.size > 0) {
|
|
1279
|
+
const symParts: string[] = [];
|
|
1280
|
+
for (const [absFile, names] of ctx.scopeSymbols) {
|
|
1281
|
+
const rel = path.relative(ctx.root, absFile);
|
|
1282
|
+
symParts.push(...names.map(n => `\`${rel}:${n}\``));
|
|
1283
|
+
}
|
|
1284
|
+
scopeLabel = symParts.join(', ');
|
|
1285
|
+
}
|
|
1286
|
+
lines.push(`> **Scoped scan**: Only showing findings for: ${scopeLabel}`);
|
|
1287
|
+
lines.push('');
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (ctx.semanticEnabled) {
|
|
1292
|
+
lines.push(
|
|
1293
|
+
'> **Semantic analysis**: TypeChecker + LanguageService enabled (14 additional categories)'
|
|
1294
|
+
);
|
|
1295
|
+
lines.push('');
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function renderTagCloud(lines: string[], allFindings: Finding[]): void {
|
|
1300
|
+
const tagCloud = collectTagCloud(allFindings);
|
|
1301
|
+
if (tagCloud.length === 0) return;
|
|
1302
|
+
lines.push('## Top Concern Tags\n');
|
|
1303
|
+
lines.push(
|
|
1304
|
+
'Searchable tags across all findings — use to filter `findings.json` with `jq`.\n'
|
|
1305
|
+
);
|
|
1306
|
+
for (const { tag, count } of tagCloud.slice(0, 12)) {
|
|
1307
|
+
lines.push(`- \`${tag}\`: ${count} findings`);
|
|
1308
|
+
}
|
|
1309
|
+
lines.push('');
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function renderAnalysisSignals(
|
|
1313
|
+
lines: string[],
|
|
1314
|
+
reportAnalysis: ReportAnalysisSummary
|
|
1315
|
+
): void {
|
|
1316
|
+
lines.push('## Analysis Signals\n');
|
|
1317
|
+
lines.push(
|
|
1318
|
+
`- **Graph Signal**: ${reportAnalysis.strongestGraphSignal?.summary || 'No dominant graph signal in this scan.'}`
|
|
1319
|
+
);
|
|
1320
|
+
lines.push(
|
|
1321
|
+
`- **AST Signal**: ${reportAnalysis.strongestAstSignal?.summary || 'No dominant AST signal in this scan.'}`
|
|
1322
|
+
);
|
|
1323
|
+
lines.push(
|
|
1324
|
+
`- **Combined Interpretation**: ${reportAnalysis.combinedInterpretation?.summary || 'No combined interpretation available yet.'}`
|
|
1325
|
+
);
|
|
1326
|
+
lines.push(
|
|
1327
|
+
`- **Confidence**: ${reportAnalysis.combinedInterpretation?.confidence || reportAnalysis.strongestGraphSignal?.confidence || reportAnalysis.strongestAstSignal?.confidence || 'low'}`
|
|
1328
|
+
);
|
|
1329
|
+
const validationSummary = reportAnalysis.recommendedValidation
|
|
1330
|
+
? `${reportAnalysis.recommendedValidation.summary} (tools: ${reportAnalysis.recommendedValidation.tools.join(' -> ')})`
|
|
1331
|
+
: 'Use Octocode local tools to confirm the strongest signal before presenting it as fact.';
|
|
1332
|
+
lines.push(`- **Recommended Validation**: ${validationSummary}`);
|
|
1333
|
+
const megaFolderSignal = reportAnalysis.graphSignals.find(
|
|
1334
|
+
signal => signal.kind === 'mega-folder-cluster'
|
|
1335
|
+
);
|
|
1336
|
+
if (megaFolderSignal) {
|
|
1337
|
+
lines.push(`- **Structural Layout Alert**: ${megaFolderSignal.summary}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (reportAnalysis.investigationPrompts.length > 0) {
|
|
1340
|
+
lines.push('');
|
|
1341
|
+
lines.push('**Investigation Prompts**');
|
|
1342
|
+
for (const prompt of reportAnalysis.investigationPrompts.slice(0, 4)) {
|
|
1343
|
+
lines.push(`- ${prompt}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
lines.push('');
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function renderAgentInstructions(
|
|
1350
|
+
lines: string[],
|
|
1351
|
+
outputFiles: Record<string, string>,
|
|
1352
|
+
allFindings: Finding[]
|
|
1353
|
+
): void {
|
|
1354
|
+
const hasCriticalOrHigh = allFindings.some(
|
|
1355
|
+
f => f.severity === 'critical' || f.severity === 'high'
|
|
1356
|
+
);
|
|
1357
|
+
lines.push('## Agent Instructions — Validate Before Presenting\n');
|
|
1358
|
+
lines.push(
|
|
1359
|
+
'> **Core rule**: Findings are hypotheses from deterministic AST/graph detectors. '
|
|
1360
|
+
+ 'Validate with Octocode local + LSP tools before presenting any finding as fact.\n'
|
|
1361
|
+
);
|
|
1362
|
+
|
|
1363
|
+
lines.push('### Triage Order\n');
|
|
1364
|
+
lines.push('1. **This file first** — health scores + analysis signals drive triage priority');
|
|
1365
|
+
if (hasCriticalOrHigh) {
|
|
1366
|
+
lines.push(
|
|
1367
|
+
'2. **High/critical findings** — filter `findings.json`: '
|
|
1368
|
+
+ '`jq \'.optimizationFindings[] | select(.severity == "critical" or .severity == "high")\' findings.json`'
|
|
1369
|
+
);
|
|
1370
|
+
} else {
|
|
1371
|
+
lines.push('2. **Findings by severity** — start from the top of `findings.json` (already sorted by severity)');
|
|
1372
|
+
}
|
|
1373
|
+
lines.push('3. **Pillar JSONs** — drill into `architecture.json`, `code-quality.json`, etc. only for categories that need investigation');
|
|
1374
|
+
lines.push('4. **`file-inventory.json`** — per-file deep dives: functions, flows, `effectProfile`, `cfgFlags`, `dependencyProfile`');
|
|
1375
|
+
lines.push('');
|
|
1376
|
+
|
|
1377
|
+
lines.push('### Validation Tool Chain\n');
|
|
1378
|
+
lines.push('Each finding includes `lspHints[]`, `correlatedSignals[]`, and `recommendedValidation`. Use them.\n');
|
|
1379
|
+
lines.push('```');
|
|
1380
|
+
lines.push('Finding → localSearchCode (get lineHint) → LSP tool → localGetFileContent → verdict');
|
|
1381
|
+
lines.push('```\n');
|
|
1382
|
+
lines.push('| Step | Tool | Purpose |');
|
|
1383
|
+
lines.push('|------|------|---------|');
|
|
1384
|
+
lines.push('| 1. Search | `localSearchCode(pattern, path)` | **Always first** — get `lineHint` for LSP. Never guess lineHint. |');
|
|
1385
|
+
lines.push('| 2. Locate | `lspGotoDefinition(lineHint)` | Jump to definition across files |');
|
|
1386
|
+
lines.push('| 3. Consumers | `lspFindReferences(lineHint)` | Count usages, split test/prod with `includePattern`/`excludePattern` |');
|
|
1387
|
+
lines.push('| 4. Call flow | `lspCallHierarchy(lineHint, incoming/outgoing)` | Trace call chains — **functions only**, fails on types/vars |');
|
|
1388
|
+
lines.push('| 5. Read code | `localGetFileContent(path, matchString=...)` | Confirm code at reported location |');
|
|
1389
|
+
lines.push('| 6. AST proof | `ast/search.js -p <pattern> --root <path>` | Structural proof on **live source** — zero false positives |');
|
|
1390
|
+
if (outputFiles.astTrees) {
|
|
1391
|
+
lines.push('| 7. AST triage | `ast/tree-search.js -i <scan-dir> -k <Kind>` | Fast triage on scan snapshot — `-k FunctionDeclaration`, `-p \'IfStatement\\|ForStatement\'`, `--file` filter, `-C 2` context |');
|
|
1392
|
+
}
|
|
1393
|
+
lines.push('');
|
|
1394
|
+
|
|
1395
|
+
lines.push('### False Positive Checklist\n');
|
|
1396
|
+
lines.push('Before reporting a finding to the user:\n');
|
|
1397
|
+
lines.push('- [ ] Ran `lspHints[]` from the finding — result matches expectation?');
|
|
1398
|
+
lines.push('- [ ] Code exists at reported `file:lineStart` — confirmed with `localGetFileContent`?');
|
|
1399
|
+
lines.push('- [ ] Pattern confirmed in live source — `ast/search.js -p` or `localSearchCode`?');
|
|
1400
|
+
lines.push('- [ ] Not in generated, vendored, or test-only code?');
|
|
1401
|
+
lines.push('- [ ] `correlatedSignals[]` — multiple signals on same file strengthen confidence');
|
|
1402
|
+
lines.push('- [ ] Consumer count verified with `lspFindReferences` — matches claimed impact?');
|
|
1403
|
+
lines.push('');
|
|
1404
|
+
lines.push('**Rate each finding**: `confirmed` (evidence supports) · `dismissed` (explain why) · `uncertain` (state what\'s missing)\n');
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function renderHotspots(
|
|
1408
|
+
lines: string[],
|
|
1409
|
+
hotFiles: SummaryMdOptions['hotFiles']
|
|
1410
|
+
): void {
|
|
1411
|
+
if (!hotFiles || hotFiles.length === 0) return;
|
|
1412
|
+
lines.push('## Change Risk Hotspots\n');
|
|
1413
|
+
lines.push(
|
|
1414
|
+
'Files most dangerous to change — high fan-in, complexity, or cycle membership.\n'
|
|
1415
|
+
);
|
|
1416
|
+
lines.push(
|
|
1417
|
+
'| File | Risk | Fan-In | Fan-Out | Complexity | Exports | Cycle | Critical Path |'
|
|
1418
|
+
);
|
|
1419
|
+
lines.push(
|
|
1420
|
+
'|------|------|--------|---------|------------|---------|-------|---------------|'
|
|
1421
|
+
);
|
|
1422
|
+
for (const hf of hotFiles.slice(0, 15)) {
|
|
1423
|
+
lines.push(
|
|
1424
|
+
`| \`${hf.file}\` | ${hf.riskScore} | ${hf.fanIn} | ${hf.fanOut} | ${hf.complexityScore} | ${hf.exportCount} | ${hf.inCycle ? 'Y' : '-'} | ${hf.onCriticalPath ? 'Y' : '-'} |`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
lines.push('');
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function renderPillarSections(
|
|
1431
|
+
lines: string[],
|
|
1432
|
+
ctx: {
|
|
1433
|
+
architectureFindings: Finding[];
|
|
1434
|
+
codeQualityFindings: Finding[];
|
|
1435
|
+
deadCodeFindings: Finding[];
|
|
1436
|
+
securityFindings: Finding[];
|
|
1437
|
+
testQualityFindings: Finding[];
|
|
1438
|
+
archStats: { totalFindings: number; severityBreakdown: Record<string, number> } | undefined;
|
|
1439
|
+
qualStats: { totalFindings: number; severityBreakdown: Record<string, number> } | undefined;
|
|
1440
|
+
deadStats: { totalFindings: number; severityBreakdown: Record<string, number> } | undefined;
|
|
1441
|
+
secStats: { totalFindings: number; severityBreakdown: Record<string, number> } | undefined;
|
|
1442
|
+
testStats: { totalFindings: number; severityBreakdown: Record<string, number> } | undefined;
|
|
1443
|
+
archHealth: number;
|
|
1444
|
+
qualHealth: number;
|
|
1445
|
+
deadHealth: number;
|
|
1446
|
+
secHealth: number;
|
|
1447
|
+
testHealth: number;
|
|
1448
|
+
activeFeatures: Set<string> | null;
|
|
1449
|
+
outputFiles: Record<string, string>;
|
|
1450
|
+
renderPillarCategories: (pillarKey: string, findings: Finding[]) => void;
|
|
1451
|
+
pushPillarSummary: (pillarKey: string, count: number, score: number, artifactKey?: string, artifactName?: string) => void;
|
|
1452
|
+
}
|
|
1453
|
+
): void {
|
|
1454
|
+
const { architectureFindings, codeQualityFindings, deadCodeFindings, securityFindings, testQualityFindings } = ctx;
|
|
1455
|
+
const { qualStats, deadStats, secStats, testStats } = ctx;
|
|
1456
|
+
const { qualHealth, deadHealth, secHealth, testHealth } = ctx;
|
|
1457
|
+
const { renderPillarCategories, pushPillarSummary } = ctx;
|
|
1458
|
+
|
|
1459
|
+
lines.push('## Code Quality\n');
|
|
1460
|
+
pushPillarSummary(
|
|
1461
|
+
'code-quality',
|
|
1462
|
+
qualStats?.totalFindings ?? codeQualityFindings.length,
|
|
1463
|
+
qualHealth,
|
|
1464
|
+
'codeQuality',
|
|
1465
|
+
'code-quality.json'
|
|
1466
|
+
);
|
|
1467
|
+
renderPillarCategories('code-quality', codeQualityFindings);
|
|
1468
|
+
|
|
1469
|
+
lines.push('## Dead Code & Hygiene\n');
|
|
1470
|
+
pushPillarSummary(
|
|
1471
|
+
'dead-code',
|
|
1472
|
+
deadStats?.totalFindings ?? deadCodeFindings.length,
|
|
1473
|
+
deadHealth,
|
|
1474
|
+
'deadCode',
|
|
1475
|
+
'dead-code.json'
|
|
1476
|
+
);
|
|
1477
|
+
renderPillarCategories('dead-code', deadCodeFindings);
|
|
1478
|
+
|
|
1479
|
+
lines.push('## Security\n');
|
|
1480
|
+
pushPillarSummary(
|
|
1481
|
+
'security',
|
|
1482
|
+
secStats?.totalFindings ?? securityFindings.length,
|
|
1483
|
+
secHealth,
|
|
1484
|
+
'security',
|
|
1485
|
+
'security.json'
|
|
1486
|
+
);
|
|
1487
|
+
renderPillarCategories('security', securityFindings);
|
|
1488
|
+
|
|
1489
|
+
lines.push('## Test Quality\n');
|
|
1490
|
+
pushPillarSummary(
|
|
1491
|
+
'test-quality',
|
|
1492
|
+
testStats?.totalFindings ?? testQualityFindings.length,
|
|
1493
|
+
testHealth,
|
|
1494
|
+
'testQuality',
|
|
1495
|
+
'test-quality.json'
|
|
1496
|
+
);
|
|
1497
|
+
renderPillarCategories('test-quality', testQualityFindings);
|
|
1498
|
+
|
|
1499
|
+
const untestedCount = architectureFindings.filter(
|
|
1500
|
+
f => f.category === 'untested-critical-code'
|
|
1501
|
+
).length;
|
|
1502
|
+
if (
|
|
1503
|
+
untestedCount > 0 &&
|
|
1504
|
+
(testStats?.totalFindings ?? testQualityFindings.length) === 0
|
|
1505
|
+
) {
|
|
1506
|
+
lines.push(
|
|
1507
|
+
`> **Note**: Test Quality reflects analyzed test files only. ${untestedCount} modules flagged as \`untested-critical-code\` (architecture pillar) have no test coverage — use \`--include-tests\` for test-quality analysis.\n`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function renderRecommendations(
|
|
1513
|
+
lines: string[],
|
|
1514
|
+
agentOutput: AgentOutputData
|
|
1515
|
+
): void {
|
|
1516
|
+
const topRecs = agentOutput?.topRecommendations ?? [];
|
|
1517
|
+
if (topRecs.length > 0) {
|
|
1518
|
+
lines.push('## Top Recommendations\n');
|
|
1519
|
+
for (const rec of topRecs.slice(0, 10)) {
|
|
1520
|
+
lines.push(
|
|
1521
|
+
`- **[${rec.severity.toUpperCase()}]** \`${rec.file}\` — ${rec.title} *(${rec.category})* `
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
lines.push('');
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function renderAstTreesSection(
|
|
1529
|
+
lines: string[],
|
|
1530
|
+
dir: string,
|
|
1531
|
+
outputFiles: Record<string, string>,
|
|
1532
|
+
root: string,
|
|
1533
|
+
relativeScanDir: string,
|
|
1534
|
+
exampleFileFilter: string
|
|
1535
|
+
): void {
|
|
1536
|
+
const astTreePath = path.resolve(dir, outputFiles.astTrees);
|
|
1537
|
+
const astTreeArg = formatCliPath(astTreePath);
|
|
1538
|
+
lines.push('## AST Trees (`ast-trees.txt`)\n');
|
|
1539
|
+
lines.push(
|
|
1540
|
+
'Compact indented text format — each node is `Kind[startLine:endLine]`, nesting = indentation.\n'
|
|
1541
|
+
);
|
|
1542
|
+
lines.push(
|
|
1543
|
+
`Run these commands from the skill directory. Current scan: \`${relativeScanDir}\`.\n`
|
|
1544
|
+
);
|
|
1545
|
+
lines.push('```');
|
|
1546
|
+
lines.push('SourceFile[1:152]');
|
|
1547
|
+
lines.push(' ImportDeclaration[1]');
|
|
1548
|
+
lines.push(' FunctionDeclaration[3:20]');
|
|
1549
|
+
lines.push(' Block[4:19]');
|
|
1550
|
+
lines.push(' IfStatement[5:12] ...');
|
|
1551
|
+
lines.push('```\n');
|
|
1552
|
+
lines.push('**Smart navigation:**\n');
|
|
1553
|
+
lines.push(
|
|
1554
|
+
`- Find functions: \`node scripts/ast/tree-search.js -i ${astTreeArg} -k function_declaration --limit 25\``
|
|
1555
|
+
);
|
|
1556
|
+
lines.push(
|
|
1557
|
+
`- Find classes: \`node scripts/ast/tree-search.js -i ${astTreeArg} -k class_declaration --limit 25\``
|
|
1558
|
+
);
|
|
1559
|
+
lines.push(
|
|
1560
|
+
`- Find control flow: \`node scripts/ast/tree-search.js -i ${astTreeArg} -p 'IfStatement|SwitchStatement|ForStatement|WhileStatement' --limit 25\``
|
|
1561
|
+
);
|
|
1562
|
+
lines.push(
|
|
1563
|
+
`- Narrow to one file: \`node scripts/ast/tree-search.js -i ${astTreeArg} --file "${exampleFileFilter}" -k function_declaration --limit 10\``
|
|
1564
|
+
);
|
|
1565
|
+
lines.push(
|
|
1566
|
+
`- Raw text fallback: \`rg 'FunctionDeclaration|IfStatement' ${astTreeArg}\``
|
|
1567
|
+
);
|
|
1568
|
+
lines.push('');
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function renderOutputFilesTable(
|
|
1572
|
+
lines: string[],
|
|
1573
|
+
dir: string,
|
|
1574
|
+
outputFiles: Record<string, string>
|
|
1575
|
+
): void {
|
|
1576
|
+
lines.push('## Output Files\n');
|
|
1577
|
+
lines.push('| File | Size | Description |');
|
|
1578
|
+
lines.push('|------|------|-------------|');
|
|
1579
|
+
const descriptions: Record<string, string> = {
|
|
1580
|
+
summary: 'Scan metadata, agent output, parse errors',
|
|
1581
|
+
architecture:
|
|
1582
|
+
'Dependency graph, cycles, critical paths, architecture findings',
|
|
1583
|
+
codeQuality: 'Duplicate detection, complexity, god modules/functions',
|
|
1584
|
+
deadCode: 'Dead files/exports/re-exports, unused deps, boundary violations',
|
|
1585
|
+
fileInventory: 'Per-file function/flow/dependency details',
|
|
1586
|
+
findings: 'All findings across all categories (master list)',
|
|
1587
|
+
graph: 'Mermaid dependency graph',
|
|
1588
|
+
astTrees:
|
|
1589
|
+
'AST tree snapshots (compact indented text — grep/regex friendly)',
|
|
1590
|
+
summaryMd: 'This file — human-readable overview',
|
|
1591
|
+
};
|
|
1592
|
+
for (const [key, file] of Object.entries(outputFiles)) {
|
|
1593
|
+
let size = '—';
|
|
1594
|
+
try {
|
|
1595
|
+
size = formatFileSize(fs.statSync(path.join(dir, file)).size);
|
|
1596
|
+
} catch {
|
|
1597
|
+
size = '—';
|
|
1598
|
+
}
|
|
1599
|
+
lines.push(
|
|
1600
|
+
`| [\`${file}\`](./${file}) | ${size} | ${descriptions[key] || key} |`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
lines.push('');
|
|
1604
|
+
}
|