@monoes/monograph 1.2.0 → 1.2.2
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/.monomind/loops/mastermind-review-1782162423447.json +16 -0
- package/__tests__/mcp-tools/cypher.test.ts +10 -6
- package/__tests__/mcp-tools/detect-changes.test.ts +14 -11
- package/__tests__/pipeline/phases/wildcard-synthesis.test.ts +4 -4
- package/__tests__/search/embed-batch-config.test.ts +7 -7
- package/__tests__/search/hybrid-query.test.ts +3 -1
- package/__tests__/web/api.test.ts +1 -1
- package/dist/src/analysis/churn-cache.d.ts.map +1 -1
- package/dist/src/analysis/churn-cache.js +19 -6
- package/dist/src/analysis/churn-cache.js.map +1 -1
- package/dist/src/analysis/churn-trend.d.ts.map +1 -1
- package/dist/src/analysis/churn-trend.js +50 -6
- package/dist/src/analysis/churn-trend.js.map +1 -1
- package/dist/src/analysis/cluster-quality.d.ts +3 -0
- package/dist/src/analysis/cluster-quality.d.ts.map +1 -1
- package/dist/src/analysis/cluster-quality.js +18 -12
- package/dist/src/analysis/cluster-quality.js.map +1 -1
- package/dist/src/analysis/coverage-gaps.d.ts +3 -0
- package/dist/src/analysis/coverage-gaps.d.ts.map +1 -1
- package/dist/src/analysis/coverage-gaps.js +40 -1
- package/dist/src/analysis/coverage-gaps.js.map +1 -1
- package/dist/src/analysis/dep-classification.d.ts +2 -0
- package/dist/src/analysis/dep-classification.d.ts.map +1 -1
- package/dist/src/analysis/dep-classification.js +30 -0
- package/dist/src/analysis/dep-classification.js.map +1 -1
- package/dist/src/analysis/dependency-health.d.ts +4 -0
- package/dist/src/analysis/dependency-health.d.ts.map +1 -1
- package/dist/src/analysis/dependency-health.js +28 -0
- package/dist/src/analysis/dependency-health.js.map +1 -1
- package/dist/src/analysis/duplicate-exports.d.ts +9 -5
- package/dist/src/analysis/duplicate-exports.d.ts.map +1 -1
- package/dist/src/analysis/duplicate-exports.js +26 -2
- package/dist/src/analysis/duplicate-exports.js.map +1 -1
- package/dist/src/analysis/feature-flags.d.ts.map +1 -1
- package/dist/src/analysis/feature-flags.js +36 -17
- package/dist/src/analysis/feature-flags.js.map +1 -1
- package/dist/src/analysis/node-similarity.d.ts +19 -0
- package/dist/src/analysis/node-similarity.d.ts.map +1 -1
- package/dist/src/analysis/node-similarity.js +34 -3
- package/dist/src/analysis/node-similarity.js.map +1 -1
- package/dist/src/analysis/private-type-leaks.d.ts +4 -0
- package/dist/src/analysis/private-type-leaks.d.ts.map +1 -1
- package/dist/src/analysis/private-type-leaks.js +43 -2
- package/dist/src/analysis/private-type-leaks.js.map +1 -1
- package/dist/src/analysis/project-state.d.ts +2 -0
- package/dist/src/analysis/project-state.d.ts.map +1 -1
- package/dist/src/analysis/project-state.js +18 -7
- package/dist/src/analysis/project-state.js.map +1 -1
- package/dist/src/analysis/suppressions.d.ts.map +1 -1
- package/dist/src/analysis/suppressions.js +3 -1
- package/dist/src/analysis/suppressions.js.map +1 -1
- package/dist/src/analysis/trace.d.ts +5 -3
- package/dist/src/analysis/trace.d.ts.map +1 -1
- package/dist/src/analysis/trace.js +59 -53
- package/dist/src/analysis/trace.js.map +1 -1
- package/dist/src/analysis/unused-class-members.d.ts.map +1 -1
- package/dist/src/analysis/unused-class-members.js +7 -1
- package/dist/src/analysis/unused-class-members.js.map +1 -1
- package/dist/src/analysis/worker-pool.d.ts +3 -0
- package/dist/src/analysis/worker-pool.d.ts.map +1 -1
- package/dist/src/analysis/worker-pool.js +30 -8
- package/dist/src/analysis/worker-pool.js.map +1 -1
- package/dist/src/cli/augment.d.ts.map +1 -1
- package/dist/src/cli/augment.js +10 -3
- package/dist/src/cli/augment.js.map +1 -1
- package/dist/src/cli/skill-gen.js +3 -0
- package/dist/src/cli/skill-gen.js.map +1 -1
- package/dist/src/export/sarif.js +5 -5
- package/dist/src/export/sarif.js.map +1 -1
- package/dist/src/graph/analyzer.d.ts.map +1 -1
- package/dist/src/graph/analyzer.js +17 -2
- package/dist/src/graph/analyzer.js.map +1 -1
- package/dist/src/graph/author-analytics.d.ts +2 -0
- package/dist/src/graph/author-analytics.d.ts.map +1 -1
- package/dist/src/graph/author-analytics.js +39 -5
- package/dist/src/graph/author-analytics.js.map +1 -1
- package/dist/src/graph/baseline.d.ts +5 -0
- package/dist/src/graph/baseline.d.ts.map +1 -1
- package/dist/src/graph/baseline.js +65 -18
- package/dist/src/graph/baseline.js.map +1 -1
- package/dist/src/graph/clone-detector.d.ts.map +1 -1
- package/dist/src/graph/clone-detector.js +19 -33
- package/dist/src/graph/clone-detector.js.map +1 -1
- package/dist/src/graph/clone-families.d.ts +2 -0
- package/dist/src/graph/clone-families.d.ts.map +1 -1
- package/dist/src/graph/clone-families.js +38 -10
- package/dist/src/graph/clone-families.js.map +1 -1
- package/dist/src/graph/codeowners-extended.d.ts +17 -0
- package/dist/src/graph/codeowners-extended.d.ts.map +1 -1
- package/dist/src/graph/codeowners-extended.js +42 -0
- package/dist/src/graph/codeowners-extended.js.map +1 -1
- package/dist/src/graph/codeowners-gitlab.d.ts +21 -0
- package/dist/src/graph/codeowners-gitlab.d.ts.map +1 -1
- package/dist/src/graph/codeowners-gitlab.js +37 -0
- package/dist/src/graph/codeowners-gitlab.js.map +1 -1
- package/dist/src/graph/codeowners.d.ts +10 -0
- package/dist/src/graph/codeowners.d.ts.map +1 -1
- package/dist/src/graph/codeowners.js +24 -2
- package/dist/src/graph/codeowners.js.map +1 -1
- package/dist/src/graph/complexity.d.ts +10 -0
- package/dist/src/graph/complexity.d.ts.map +1 -1
- package/dist/src/graph/complexity.js +93 -43
- package/dist/src/graph/complexity.js.map +1 -1
- package/dist/src/graph/cross-reference-findings.d.ts.map +1 -1
- package/dist/src/graph/cross-reference-findings.js +19 -2
- package/dist/src/graph/cross-reference-findings.js.map +1 -1
- package/dist/src/graph/cross-reference.d.ts +4 -0
- package/dist/src/graph/cross-reference.d.ts.map +1 -1
- package/dist/src/graph/cross-reference.js +23 -0
- package/dist/src/graph/cross-reference.js.map +1 -1
- package/dist/src/graph/cycle-detection.d.ts +2 -0
- package/dist/src/graph/cycle-detection.d.ts.map +1 -1
- package/dist/src/graph/cycle-detection.js +26 -3
- package/dist/src/graph/cycle-detection.js.map +1 -1
- package/dist/src/graph/cycles.d.ts +11 -3
- package/dist/src/graph/cycles.d.ts.map +1 -1
- package/dist/src/graph/cycles.js +121 -83
- package/dist/src/graph/cycles.js.map +1 -1
- package/dist/src/graph/dead-code.d.ts +23 -0
- package/dist/src/graph/dead-code.d.ts.map +1 -1
- package/dist/src/graph/dead-code.js +53 -13
- package/dist/src/graph/dead-code.js.map +1 -1
- package/dist/src/graph/dependency-closure.d.ts.map +1 -1
- package/dist/src/graph/dependency-closure.js +20 -4
- package/dist/src/graph/dependency-closure.js.map +1 -1
- package/dist/src/graph/dfs.d.ts +14 -0
- package/dist/src/graph/dfs.d.ts.map +1 -1
- package/dist/src/graph/dfs.js +61 -0
- package/dist/src/graph/dfs.js.map +1 -1
- package/dist/src/graph/duplication-stats.d.ts.map +1 -1
- package/dist/src/graph/duplication-stats.js +19 -3
- package/dist/src/graph/duplication-stats.js.map +1 -1
- package/dist/src/graph/edge-filter.d.ts +2 -0
- package/dist/src/graph/edge-filter.d.ts.map +1 -1
- package/dist/src/graph/edge-filter.js +26 -12
- package/dist/src/graph/edge-filter.js.map +1 -1
- package/dist/src/graph/health-score.d.ts +8 -0
- package/dist/src/graph/health-score.d.ts.map +1 -1
- package/dist/src/graph/health-score.js +48 -17
- package/dist/src/graph/health-score.js.map +1 -1
- package/dist/src/graph/hotspots.d.ts +7 -0
- package/dist/src/graph/hotspots.d.ts.map +1 -1
- package/dist/src/graph/hotspots.js +50 -5
- package/dist/src/graph/hotspots.js.map +1 -1
- package/dist/src/graph/import-chain.d.ts +17 -1
- package/dist/src/graph/import-chain.d.ts.map +1 -1
- package/dist/src/graph/import-chain.js +68 -19
- package/dist/src/graph/import-chain.js.map +1 -1
- package/dist/src/graph/loader.d.ts.map +1 -1
- package/dist/src/graph/loader.js +9 -10
- package/dist/src/graph/loader.js.map +1 -1
- package/dist/src/graph/maintainability.d.ts +8 -0
- package/dist/src/graph/maintainability.d.ts.map +1 -1
- package/dist/src/graph/maintainability.js +65 -10
- package/dist/src/graph/maintainability.js.map +1 -1
- package/dist/src/graph/mirrored-dirs.d.ts +4 -0
- package/dist/src/graph/mirrored-dirs.d.ts.map +1 -1
- package/dist/src/graph/mirrored-dirs.js +48 -13
- package/dist/src/graph/mirrored-dirs.js.map +1 -1
- package/dist/src/graph/narrowing.d.ts +15 -0
- package/dist/src/graph/narrowing.d.ts.map +1 -1
- package/dist/src/graph/narrowing.js +33 -0
- package/dist/src/graph/narrowing.js.map +1 -1
- package/dist/src/graph/node-search.d.ts +3 -0
- package/dist/src/graph/node-search.d.ts.map +1 -1
- package/dist/src/graph/node-search.js +37 -4
- package/dist/src/graph/node-search.js.map +1 -1
- package/dist/src/graph/normalize-id.d.ts +4 -0
- package/dist/src/graph/normalize-id.d.ts.map +1 -1
- package/dist/src/graph/normalize-id.js +25 -2
- package/dist/src/graph/normalize-id.js.map +1 -1
- package/dist/src/graph/pagerank.d.ts +7 -0
- package/dist/src/graph/pagerank.d.ts.map +1 -1
- package/dist/src/graph/pagerank.js +49 -2
- package/dist/src/graph/pagerank.js.map +1 -1
- package/dist/src/graph/prune.d.ts +31 -0
- package/dist/src/graph/prune.d.ts.map +1 -1
- package/dist/src/graph/prune.js +65 -0
- package/dist/src/graph/prune.js.map +1 -1
- package/dist/src/graph/regex-search.d.ts +10 -2
- package/dist/src/graph/regex-search.d.ts.map +1 -1
- package/dist/src/graph/regex-search.js +33 -4
- package/dist/src/graph/regex-search.js.map +1 -1
- package/dist/src/graph/regression.d.ts +5 -0
- package/dist/src/graph/regression.d.ts.map +1 -1
- package/dist/src/graph/regression.js +45 -23
- package/dist/src/graph/regression.js.map +1 -1
- package/dist/src/graph/ripple-impact.d.ts +27 -0
- package/dist/src/graph/ripple-impact.d.ts.map +1 -1
- package/dist/src/graph/ripple-impact.js +55 -7
- package/dist/src/graph/ripple-impact.js.map +1 -1
- package/dist/src/graph/risk-profile.d.ts +7 -0
- package/dist/src/graph/risk-profile.d.ts.map +1 -1
- package/dist/src/graph/risk-profile.js +82 -48
- package/dist/src/graph/risk-profile.js.map +1 -1
- package/dist/src/graph/shingle-filter.d.ts.map +1 -1
- package/dist/src/graph/shingle-filter.js +25 -26
- package/dist/src/graph/shingle-filter.js.map +1 -1
- package/dist/src/graph/statistics.d.ts +19 -0
- package/dist/src/graph/statistics.d.ts.map +1 -1
- package/dist/src/graph/statistics.js +53 -40
- package/dist/src/graph/statistics.js.map +1 -1
- package/dist/src/graph/stats.d.ts +2 -0
- package/dist/src/graph/stats.d.ts.map +1 -1
- package/dist/src/graph/stats.js +54 -8
- package/dist/src/graph/stats.js.map +1 -1
- package/dist/src/graph/subgraph.d.ts +5 -0
- package/dist/src/graph/subgraph.d.ts.map +1 -1
- package/dist/src/graph/subgraph.js +73 -9
- package/dist/src/graph/subgraph.js.map +1 -1
- package/dist/src/graph/topo-sort.d.ts +15 -0
- package/dist/src/graph/topo-sort.d.ts.map +1 -1
- package/dist/src/graph/topo-sort.js +83 -10
- package/dist/src/graph/topo-sort.js.map +1 -1
- package/dist/src/graph/wcc.d.ts +29 -0
- package/dist/src/graph/wcc.d.ts.map +1 -1
- package/dist/src/graph/wcc.js +122 -39
- package/dist/src/graph/wcc.js.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp-tools/api-impact.d.ts.map +1 -1
- package/dist/src/mcp-tools/api-impact.js +13 -6
- package/dist/src/mcp-tools/api-impact.js.map +1 -1
- package/dist/src/mcp-tools/context.d.ts.map +1 -1
- package/dist/src/mcp-tools/context.js +19 -29
- package/dist/src/mcp-tools/context.js.map +1 -1
- package/dist/src/mcp-tools/cypher.d.ts +11 -0
- package/dist/src/mcp-tools/cypher.d.ts.map +1 -1
- package/dist/src/mcp-tools/cypher.js +13 -1
- package/dist/src/mcp-tools/cypher.js.map +1 -1
- package/dist/src/mcp-tools/detect-changes.d.ts.map +1 -1
- package/dist/src/mcp-tools/detect-changes.js +35 -20
- package/dist/src/mcp-tools/detect-changes.js.map +1 -1
- package/dist/src/mcp-tools/explain.d.ts.map +1 -1
- package/dist/src/mcp-tools/explain.js +4 -1
- package/dist/src/mcp-tools/explain.js.map +1 -1
- package/dist/src/mcp-tools/graph-query.d.ts +2 -0
- package/dist/src/mcp-tools/graph-query.d.ts.map +1 -1
- package/dist/src/mcp-tools/graph-query.js +20 -3
- package/dist/src/mcp-tools/graph-query.js.map +1 -1
- package/dist/src/mcp-tools/group-list.d.ts +1 -0
- package/dist/src/mcp-tools/group-list.d.ts.map +1 -1
- package/dist/src/mcp-tools/group-list.js +7 -9
- package/dist/src/mcp-tools/group-list.js.map +1 -1
- package/dist/src/mcp-tools/impact.d.ts.map +1 -1
- package/dist/src/mcp-tools/impact.js +20 -59
- package/dist/src/mcp-tools/impact.js.map +1 -1
- package/dist/src/mcp-tools/neighbors.d.ts.map +1 -1
- package/dist/src/mcp-tools/neighbors.js +24 -33
- package/dist/src/mcp-tools/neighbors.js.map +1 -1
- package/dist/src/mcp-tools/query.d.ts +2 -0
- package/dist/src/mcp-tools/query.d.ts.map +1 -1
- package/dist/src/mcp-tools/query.js +1 -0
- package/dist/src/mcp-tools/query.js.map +1 -1
- package/dist/src/mcp-tools/rename.d.ts.map +1 -1
- package/dist/src/mcp-tools/rename.js +12 -7
- package/dist/src/mcp-tools/rename.js.map +1 -1
- package/dist/src/mcp-tools/route-map.d.ts.map +1 -1
- package/dist/src/mcp-tools/route-map.js +19 -23
- package/dist/src/mcp-tools/route-map.js.map +1 -1
- package/dist/src/mcp-tools/shape-check.d.ts.map +1 -1
- package/dist/src/mcp-tools/shape-check.js +17 -13
- package/dist/src/mcp-tools/shape-check.js.map +1 -1
- package/dist/src/parsers/loader.js +6 -5
- package/dist/src/parsers/loader.js.map +1 -1
- package/dist/src/pipeline/cluster-only.d.ts.map +1 -1
- package/dist/src/pipeline/cluster-only.js +8 -4
- package/dist/src/pipeline/cluster-only.js.map +1 -1
- package/dist/src/pipeline/orchestrator.d.ts +2 -0
- package/dist/src/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/src/pipeline/orchestrator.js +19 -0
- package/dist/src/pipeline/orchestrator.js.map +1 -1
- package/dist/src/pipeline/phases/boundary.d.ts +2 -0
- package/dist/src/pipeline/phases/boundary.d.ts.map +1 -1
- package/dist/src/pipeline/phases/boundary.js +31 -2
- package/dist/src/pipeline/phases/boundary.js.map +1 -1
- package/dist/src/pipeline/phases/communities.d.ts +14 -0
- package/dist/src/pipeline/phases/communities.d.ts.map +1 -1
- package/dist/src/pipeline/phases/communities.js +58 -5
- package/dist/src/pipeline/phases/communities.js.map +1 -1
- package/dist/src/pipeline/phases/contextual-proximity.d.ts.map +1 -1
- package/dist/src/pipeline/phases/contextual-proximity.js +21 -14
- package/dist/src/pipeline/phases/contextual-proximity.js.map +1 -1
- package/dist/src/pipeline/phases/cross-file.d.ts.map +1 -1
- package/dist/src/pipeline/phases/cross-file.js +35 -14
- package/dist/src/pipeline/phases/cross-file.js.map +1 -1
- package/dist/src/pipeline/phases/field-access.d.ts.map +1 -1
- package/dist/src/pipeline/phases/field-access.js +9 -4
- package/dist/src/pipeline/phases/field-access.js.map +1 -1
- package/dist/src/pipeline/phases/god-nodes.d.ts +4 -0
- package/dist/src/pipeline/phases/god-nodes.d.ts.map +1 -1
- package/dist/src/pipeline/phases/god-nodes.js +43 -9
- package/dist/src/pipeline/phases/god-nodes.js.map +1 -1
- package/dist/src/pipeline/phases/import-resolver.d.ts +11 -0
- package/dist/src/pipeline/phases/import-resolver.d.ts.map +1 -1
- package/dist/src/pipeline/phases/import-resolver.js +34 -7
- package/dist/src/pipeline/phases/import-resolver.js.map +1 -1
- package/dist/src/pipeline/phases/leiden.d.ts.map +1 -1
- package/dist/src/pipeline/phases/leiden.js +27 -19
- package/dist/src/pipeline/phases/leiden.js.map +1 -1
- package/dist/src/pipeline/phases/markdown.d.ts.map +1 -1
- package/dist/src/pipeline/phases/markdown.js +10 -5
- package/dist/src/pipeline/phases/markdown.js.map +1 -1
- package/dist/src/pipeline/phases/pdf-parse.d.ts.map +1 -1
- package/dist/src/pipeline/phases/pdf-parse.js +18 -3
- package/dist/src/pipeline/phases/pdf-parse.js.map +1 -1
- package/dist/src/pipeline/phases/processes.d.ts.map +1 -1
- package/dist/src/pipeline/phases/processes.js +26 -9
- package/dist/src/pipeline/phases/processes.js.map +1 -1
- package/dist/src/pipeline/phases/re-export-propagation.d.ts.map +1 -1
- package/dist/src/pipeline/phases/re-export-propagation.js +33 -14
- package/dist/src/pipeline/phases/re-export-propagation.js.map +1 -1
- package/dist/src/pipeline/phases/reachability.d.ts.map +1 -1
- package/dist/src/pipeline/phases/reachability.js +42 -26
- package/dist/src/pipeline/phases/reachability.js.map +1 -1
- package/dist/src/pipeline/phases/routes.d.ts.map +1 -1
- package/dist/src/pipeline/phases/routes.js +30 -21
- package/dist/src/pipeline/phases/routes.js.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.js +93 -63
- package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
- package/dist/src/pipeline/runner.d.ts +4 -0
- package/dist/src/pipeline/runner.d.ts.map +1 -1
- package/dist/src/pipeline/runner.js +30 -11
- package/dist/src/pipeline/runner.js.map +1 -1
- package/dist/src/query/cypher-parser.d.ts +20 -0
- package/dist/src/query/cypher-parser.d.ts.map +1 -1
- package/dist/src/query/cypher-parser.js +73 -0
- package/dist/src/query/cypher-parser.js.map +1 -1
- package/dist/src/search/embed-batch.d.ts.map +1 -1
- package/dist/src/search/embed-batch.js +4 -1
- package/dist/src/search/embed-batch.js.map +1 -1
- package/dist/src/search/hybrid-query.d.ts +4 -0
- package/dist/src/search/hybrid-query.d.ts.map +1 -1
- package/dist/src/search/hybrid-query.js +5 -1
- package/dist/src/search/hybrid-query.js.map +1 -1
- package/dist/src/staleness/git-staleness.js +3 -3
- package/dist/src/staleness/git-staleness.js.map +1 -1
- package/dist/src/storage/db.d.ts.map +1 -1
- package/dist/src/storage/db.js +12 -0
- package/dist/src/storage/db.js.map +1 -1
- package/dist/src/storage/edge-store.d.ts +2 -0
- package/dist/src/storage/edge-store.d.ts.map +1 -1
- package/dist/src/storage/edge-store.js +26 -1
- package/dist/src/storage/edge-store.js.map +1 -1
- package/dist/src/storage/embedding-store.d.ts +16 -0
- package/dist/src/storage/embedding-store.d.ts.map +1 -1
- package/dist/src/storage/embedding-store.js +26 -2
- package/dist/src/storage/embedding-store.js.map +1 -1
- package/dist/src/storage/file-cache.d.ts +5 -0
- package/dist/src/storage/file-cache.d.ts.map +1 -1
- package/dist/src/storage/file-cache.js +35 -9
- package/dist/src/storage/file-cache.js.map +1 -1
- package/dist/src/storage/fts-store.d.ts +4 -0
- package/dist/src/storage/fts-store.d.ts.map +1 -1
- package/dist/src/storage/fts-store.js +8 -3
- package/dist/src/storage/fts-store.js.map +1 -1
- package/dist/src/storage/node-store.d.ts +2 -0
- package/dist/src/storage/node-store.d.ts.map +1 -1
- package/dist/src/storage/node-store.js +28 -1
- package/dist/src/storage/node-store.js.map +1 -1
- package/dist/src/wiki/wiki-generator.d.ts.map +1 -1
- package/dist/src/wiki/wiki-generator.js +5 -1
- package/dist/src/wiki/wiki-generator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/pipeline/runner.parallel.test.ts +2 -2
- package/src/analysis/churn-cache.ts +18 -7
- package/src/analysis/churn-trend.ts +35 -6
- package/src/analysis/cluster-quality.ts +20 -11
- package/src/analysis/coverage-gaps.ts +44 -1
- package/src/analysis/dep-classification.ts +34 -0
- package/src/analysis/dependency-health.ts +25 -0
- package/src/analysis/duplicate-exports.ts +40 -4
- package/src/analysis/feature-flags.ts +37 -17
- package/src/analysis/node-similarity.ts +45 -4
- package/src/analysis/private-type-leaks.ts +49 -2
- package/src/analysis/project-state.ts +19 -9
- package/src/analysis/suppressions.ts +4 -1
- package/src/analysis/trace.ts +73 -54
- package/src/analysis/unused-class-members.ts +8 -1
- package/src/analysis/worker-pool.ts +40 -8
- package/src/cli/augment.ts +10 -3
- package/src/cli/skill-gen.ts +3 -0
- package/src/export/sarif.ts +5 -5
- package/src/graph/analyzer.ts +26 -2
- package/src/graph/author-analytics.ts +45 -5
- package/src/graph/baseline.ts +78 -24
- package/src/graph/clone-detector.ts +22 -36
- package/src/graph/clone-families.ts +34 -10
- package/src/graph/codeowners-extended.ts +54 -0
- package/src/graph/codeowners-gitlab.ts +46 -0
- package/src/graph/codeowners.ts +33 -2
- package/src/graph/complexity.ts +102 -47
- package/src/graph/cross-reference-findings.ts +23 -2
- package/src/graph/cross-reference.ts +26 -0
- package/src/graph/cycle-detection.ts +22 -3
- package/src/graph/cycles.ts +155 -96
- package/src/graph/dead-code.ts +76 -17
- package/src/graph/dependency-closure.ts +24 -7
- package/src/graph/dfs.ts +78 -0
- package/src/graph/duplication-stats.ts +17 -6
- package/src/graph/edge-filter.ts +26 -12
- package/src/graph/health-score.ts +47 -17
- package/src/graph/hotspots.ts +57 -8
- package/src/graph/import-chain.ts +89 -19
- package/src/graph/loader.ts +10 -7
- package/src/graph/maintainability.ts +72 -22
- package/src/graph/mirrored-dirs.ts +45 -14
- package/src/graph/narrowing.ts +44 -0
- package/src/graph/node-search.ts +50 -4
- package/src/graph/normalize-id.ts +26 -2
- package/src/graph/pagerank.ts +70 -2
- package/src/graph/prune.ts +76 -0
- package/src/graph/regex-search.ts +38 -4
- package/src/graph/regression.ts +48 -23
- package/src/graph/ripple-impact.ts +71 -11
- package/src/graph/risk-profile.ts +98 -53
- package/src/graph/shingle-filter.ts +33 -27
- package/src/graph/statistics.ts +73 -45
- package/src/graph/stats.ts +50 -16
- package/src/graph/subgraph.ts +97 -25
- package/src/graph/topo-sort.ts +101 -16
- package/src/graph/wcc.ts +146 -37
- package/src/index.ts +1 -1
- package/src/mcp-tools/api-impact.ts +13 -6
- package/src/mcp-tools/context.ts +31 -40
- package/src/mcp-tools/cypher.ts +14 -1
- package/src/mcp-tools/detect-changes.ts +38 -23
- package/src/mcp-tools/explain.ts +4 -1
- package/src/mcp-tools/graph-query.ts +24 -3
- package/src/mcp-tools/group-list.ts +9 -11
- package/src/mcp-tools/impact.ts +26 -66
- package/src/mcp-tools/neighbors.ts +34 -39
- package/src/mcp-tools/query.ts +3 -0
- package/src/mcp-tools/rename.ts +9 -8
- package/src/mcp-tools/route-map.ts +21 -23
- package/src/mcp-tools/shape-check.ts +18 -14
- package/src/parsers/loader.ts +5 -5
- package/src/pipeline/cluster-only.ts +8 -4
- package/src/pipeline/orchestrator.ts +22 -0
- package/src/pipeline/phases/boundary.ts +40 -2
- package/src/pipeline/phases/communities.ts +66 -5
- package/src/pipeline/phases/contextual-proximity.ts +15 -14
- package/src/pipeline/phases/cross-file.ts +35 -14
- package/src/pipeline/phases/field-access.ts +11 -4
- package/src/pipeline/phases/god-nodes.ts +47 -10
- package/src/pipeline/phases/import-resolver.ts +37 -5
- package/src/pipeline/phases/leiden.ts +25 -18
- package/src/pipeline/phases/markdown.ts +12 -7
- package/src/pipeline/phases/pdf-parse.ts +19 -2
- package/src/pipeline/phases/processes.ts +27 -15
- package/src/pipeline/phases/re-export-propagation.ts +40 -18
- package/src/pipeline/phases/reachability.ts +39 -24
- package/src/pipeline/phases/routes.ts +32 -21
- package/src/pipeline/phases/scope-resolution.ts +110 -71
- package/src/pipeline/runner.ts +28 -11
- package/src/query/cypher-parser.ts +83 -0
- package/src/search/embed-batch.ts +4 -1
- package/src/search/hybrid-query.ts +11 -1
- package/src/staleness/git-staleness.ts +3 -3
- package/src/storage/db.ts +11 -0
- package/src/storage/edge-store.ts +34 -1
- package/src/storage/embedding-store.ts +38 -6
- package/src/storage/file-cache.ts +35 -21
- package/src/storage/fts-store.ts +12 -3
- package/src/storage/node-store.ts +39 -1
- package/src/wiki/wiki-generator.ts +5 -1
- package/src/coverage/runtime.ts +0 -249
package/src/graph/dead-code.ts
CHANGED
|
@@ -11,35 +11,94 @@ import type { MonographDb } from '../storage/db.js';
|
|
|
11
11
|
* @returns Array of node ids that are considered dead code.
|
|
12
12
|
*/
|
|
13
13
|
export function detectDeadCode(db: MonographDb): string[] {
|
|
14
|
-
|
|
14
|
+
return detectDeadCodeNodes(db).map(n => n.id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface DeadCodeNode {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
filePath: string | null;
|
|
23
|
+
startLine: number | null;
|
|
24
|
+
label: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Like detectDeadCode() but returns rich node objects with filePath and
|
|
29
|
+
* startLine so callers can render file:line navigation hints.
|
|
30
|
+
*
|
|
31
|
+
* @param db - The MonographDb instance
|
|
32
|
+
* @returns Array of dead-code nodes with location metadata.
|
|
33
|
+
*/
|
|
34
|
+
export function detectDeadCodeNodes(db: MonographDb): DeadCodeNode[] {
|
|
35
|
+
const nodeRows = db
|
|
36
|
+
.prepare('SELECT id, name, file_path, start_line, label, is_exported FROM nodes')
|
|
37
|
+
.all() as {
|
|
15
38
|
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
file_path: string | null;
|
|
41
|
+
start_line: number | null;
|
|
42
|
+
label: string;
|
|
16
43
|
is_exported: number;
|
|
17
44
|
}[];
|
|
18
45
|
|
|
19
46
|
if (nodeRows.length === 0) return [];
|
|
20
47
|
|
|
21
|
-
// Count in-degrees
|
|
48
|
+
// Count in-degrees via a single SQL aggregation — avoids loading all edges into JS
|
|
49
|
+
const inDegreeRows = db
|
|
50
|
+
.prepare(
|
|
51
|
+
'SELECT target_id, COUNT(*) AS cnt FROM edges WHERE source_id != target_id GROUP BY target_id',
|
|
52
|
+
)
|
|
53
|
+
.all() as { target_id: string; cnt: number }[];
|
|
54
|
+
|
|
22
55
|
const inDegree = new Map<string, number>();
|
|
23
|
-
for (const {
|
|
24
|
-
inDegree.set(
|
|
56
|
+
for (const { target_id, cnt } of inDegreeRows) {
|
|
57
|
+
inDegree.set(target_id, cnt);
|
|
25
58
|
}
|
|
26
59
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
const dead: DeadCodeNode[] = [];
|
|
61
|
+
for (const row of nodeRows) {
|
|
62
|
+
if ((inDegree.get(row.id) ?? 0) === 0 && row.is_exported !== 1) {
|
|
63
|
+
dead.push({
|
|
64
|
+
id: row.id,
|
|
65
|
+
name: row.name,
|
|
66
|
+
filePath: row.file_path,
|
|
67
|
+
startLine: row.start_line,
|
|
68
|
+
label: row.label,
|
|
69
|
+
});
|
|
34
70
|
}
|
|
35
71
|
}
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
73
|
+
return dead;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format dead-code nodes as structured text for LLM consumption.
|
|
78
|
+
* Each entry includes a file:line navigation hint where available.
|
|
79
|
+
*
|
|
80
|
+
* @param nodes - Result from detectDeadCodeNodes()
|
|
81
|
+
* @returns Multi-line string suitable for injection into LLM context
|
|
82
|
+
*/
|
|
83
|
+
export function formatDeadCode(nodes: DeadCodeNode[]): string {
|
|
84
|
+
if (nodes.length === 0) {
|
|
85
|
+
return 'Dead code: none detected.';
|
|
42
86
|
}
|
|
43
87
|
|
|
44
|
-
|
|
88
|
+
const lines: string[] = [
|
|
89
|
+
`Dead code: ${nodes.length} unreachable node${nodes.length === 1 ? '' : 's'} detected`,
|
|
90
|
+
'',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
const loc =
|
|
95
|
+
node.filePath != null
|
|
96
|
+
? node.startLine != null
|
|
97
|
+
? `${node.filePath}:${node.startLine}`
|
|
98
|
+
: node.filePath
|
|
99
|
+
: '(unknown location)';
|
|
100
|
+
lines.push(` [${node.label}] ${node.name} — ${loc}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
45
104
|
}
|
|
@@ -17,6 +17,8 @@ export interface DepClosureReport {
|
|
|
17
17
|
deepDependencyFiles: DepClosureResult[]; // files with depDepth > 5
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const SQLITE_VAR_LIMIT = 200; // safe batch size for IN clauses
|
|
21
|
+
|
|
20
22
|
export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepClosureReport {
|
|
21
23
|
// Get File nodes sorted by degree (most connected first)
|
|
22
24
|
const fileNodes = (db.prepare(`
|
|
@@ -38,6 +40,12 @@ export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepCl
|
|
|
38
40
|
`).all() as Array<{ file_path: string }>).map(r => r.file_path)
|
|
39
41
|
);
|
|
40
42
|
|
|
43
|
+
// Hoist prepared statement outside the per-node BFS loop to avoid recompiling
|
|
44
|
+
// on every iteration (N×maxNodes calls previously).
|
|
45
|
+
const edgeStmt = db.prepare(
|
|
46
|
+
`SELECT target_id FROM edges WHERE source_id = ? AND relation = 'IMPORTS'`,
|
|
47
|
+
);
|
|
48
|
+
|
|
41
49
|
const results: DepClosureResult[] = [];
|
|
42
50
|
|
|
43
51
|
for (const fileNode of fileNodes) {
|
|
@@ -53,10 +61,7 @@ export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepCl
|
|
|
53
61
|
depth++;
|
|
54
62
|
const next: string[] = [];
|
|
55
63
|
for (const id of frontier) {
|
|
56
|
-
const edges =
|
|
57
|
-
`SELECT target_id FROM edges WHERE source_id = ? AND relation = 'IMPORTS'`
|
|
58
|
-
).all(id) as Array<{ target_id: string }>;
|
|
59
|
-
|
|
64
|
+
const edges = edgeStmt.all(id) as Array<{ target_id: string }>;
|
|
60
65
|
for (const e of edges) {
|
|
61
66
|
if (!visited.has(e.target_id)) {
|
|
62
67
|
visited.add(e.target_id);
|
|
@@ -73,11 +78,23 @@ export function computeDependencyClosure(db: MonographDb, maxNodes = 100): DepCl
|
|
|
73
78
|
frontier = next;
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
// Batch-resolve file_paths for all transitive deps in one chunked SQL query
|
|
82
|
+
// instead of one SELECT per dep (N+1 → O(deps/CHUNK) queries).
|
|
83
|
+
const depFilePathMap = new Map<string, string | null>();
|
|
84
|
+
for (let i = 0; i < transitiveDeps.length; i += SQLITE_VAR_LIMIT) {
|
|
85
|
+
const chunk = transitiveDeps.slice(i, i + SQLITE_VAR_LIMIT);
|
|
86
|
+
const ph = chunk.map(() => '?').join(',');
|
|
87
|
+
const rows = db
|
|
88
|
+
.prepare(`SELECT id, file_path FROM nodes WHERE id IN (${ph})`)
|
|
89
|
+
.all(...chunk) as Array<{ id: string; file_path: string | null }>;
|
|
90
|
+
for (const row of rows) depFilePathMap.set(row.id, row.file_path);
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
// unusedTransitiveDeps: transitive deps whose file_path is NOT in importedFilePaths
|
|
77
94
|
const unusedTransitiveDeps = transitiveDeps.filter(depId => {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
80
|
-
return !importedFilePaths.has(
|
|
95
|
+
const fp = depFilePathMap.get(depId);
|
|
96
|
+
if (!fp) return false;
|
|
97
|
+
return !importedFilePaths.has(fp);
|
|
81
98
|
});
|
|
82
99
|
|
|
83
100
|
results.push({
|
package/src/graph/dfs.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface DfsOptions {
|
|
|
7
7
|
maxDepth?: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface DfsResult {
|
|
11
|
+
visited: DfsNode[];
|
|
12
|
+
maxDepth: number;
|
|
13
|
+
nodeCount: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export function dfsTraversal(
|
|
11
17
|
startId: string,
|
|
12
18
|
adjacency: Map<string, string[]>,
|
|
@@ -38,3 +44,75 @@ export function dfsTraversal(
|
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
47
|
+
|
|
48
|
+
/** Collect all reachable nodes from startId via DFS, returning structured result. */
|
|
49
|
+
export function dfsCollect(
|
|
50
|
+
startId: string,
|
|
51
|
+
adjacency: Map<string, string[]>,
|
|
52
|
+
options: DfsOptions = {},
|
|
53
|
+
): DfsResult {
|
|
54
|
+
const visited: DfsNode[] = [];
|
|
55
|
+
let maxReachedDepth = 0;
|
|
56
|
+
dfsTraversal(startId, adjacency, (node) => {
|
|
57
|
+
visited.push(node);
|
|
58
|
+
if (node.depth > maxReachedDepth) maxReachedDepth = node.depth;
|
|
59
|
+
}, options);
|
|
60
|
+
return { visited, maxDepth: maxReachedDepth, nodeCount: visited.length };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** BFS level-order traversal — useful when shortest-path distance matters more than DFS order. */
|
|
64
|
+
export function bfsTraversal(
|
|
65
|
+
startId: string,
|
|
66
|
+
adjacency: Map<string, string[]>,
|
|
67
|
+
visitor: (node: DfsNode) => void,
|
|
68
|
+
options: DfsOptions = {},
|
|
69
|
+
): void {
|
|
70
|
+
if (!adjacency.has(startId)) return;
|
|
71
|
+
|
|
72
|
+
const { maxDepth = Infinity } = options;
|
|
73
|
+
const visited = new Set<string>([startId]);
|
|
74
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
|
|
75
|
+
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
const { id, depth } = queue.shift()!;
|
|
78
|
+
visitor({ id, depth });
|
|
79
|
+
|
|
80
|
+
if (depth < maxDepth) {
|
|
81
|
+
const neighbors = adjacency.get(id) ?? [];
|
|
82
|
+
for (let i = 0; i < neighbors.length; i++) {
|
|
83
|
+
const neighbor = neighbors[i]!;
|
|
84
|
+
if (!visited.has(neighbor)) {
|
|
85
|
+
visited.add(neighbor);
|
|
86
|
+
queue.push({ id: neighbor, depth: depth + 1 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format a DfsResult as structured text for LLM consumption.
|
|
95
|
+
* Groups nodes by depth level for easy reading.
|
|
96
|
+
*/
|
|
97
|
+
export function formatDfsResult(result: DfsResult, startId: string): string {
|
|
98
|
+
if (result.nodeCount === 0) return `No nodes reachable from "${startId}".`;
|
|
99
|
+
|
|
100
|
+
const byDepth = new Map<number, string[]>();
|
|
101
|
+
for (const node of result.visited) {
|
|
102
|
+
let bucket = byDepth.get(node.depth);
|
|
103
|
+
if (!bucket) { bucket = []; byDepth.set(node.depth, bucket); }
|
|
104
|
+
bucket.push(node.id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines: string[] = [
|
|
108
|
+
`DFS from "${startId}": ${result.nodeCount} nodes, max depth ${result.maxDepth}`,
|
|
109
|
+
'',
|
|
110
|
+
];
|
|
111
|
+
// Iterate in depth order
|
|
112
|
+
const depths = Array.from(byDepth.keys()).sort((a, b) => a - b);
|
|
113
|
+
for (const depth of depths) {
|
|
114
|
+
const ids = byDepth.get(depth)!;
|
|
115
|
+
lines.push(` depth ${depth}: ${ids.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
@@ -39,11 +39,15 @@ export function computeDuplicationStats(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const filesWithClones = fileLines.size;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Single-pass line count — avoids spread allocation over all file Sets
|
|
43
|
+
let duplicatedLines = 0;
|
|
44
|
+
for (const set of fileLines.values()) duplicatedLines += set.size;
|
|
45
|
+
// Single-pass token sum — avoids nested reduce and intermediate arrays
|
|
46
|
+
let rawTokens = 0;
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
for (const inst of group.instances) rawTokens += inst.tokenCount ?? 0;
|
|
49
|
+
}
|
|
50
|
+
const duplicatedTokens = Math.min(totalTokens, rawTokens);
|
|
47
51
|
|
|
48
52
|
return {
|
|
49
53
|
totalFiles: allFilePaths.length,
|
|
@@ -59,10 +63,17 @@ export function computeDuplicationStats(
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
export function formatDuplicationStats(stats: DuplicationStats): string {
|
|
66
|
+
const pct = stats.duplicationPct;
|
|
67
|
+
const grade = pct < 5 ? 'A' : pct < 10 ? 'B' : pct < 20 ? 'C' : pct < 30 ? 'D' : 'F';
|
|
68
|
+
const tokenPct = stats.totalTokens > 0
|
|
69
|
+
? ((stats.duplicatedTokens / stats.totalTokens) * 100).toFixed(1)
|
|
70
|
+
: '0.0';
|
|
62
71
|
return [
|
|
72
|
+
`Duplication grade: ${grade} (${pct.toFixed(1)}% duplicated lines)`,
|
|
63
73
|
`Clone groups: ${stats.cloneGroups}`,
|
|
64
74
|
`Clone instances: ${stats.cloneInstances}`,
|
|
65
75
|
`Files with clones: ${stats.filesWithClones} / ${stats.totalFiles}`,
|
|
66
|
-
`Duplicated lines: ${stats.duplicatedLines} / ${stats.totalLines} (${
|
|
76
|
+
`Duplicated lines: ${stats.duplicatedLines} / ${stats.totalLines} (${pct.toFixed(1)}%)`,
|
|
77
|
+
`Duplicated tokens: ${stats.duplicatedTokens} / ${stats.totalTokens} (${tokenPct}%)`,
|
|
67
78
|
].join('\n');
|
|
68
79
|
}
|
package/src/graph/edge-filter.ts
CHANGED
|
@@ -85,19 +85,33 @@ export function filterEdgesInMemory(
|
|
|
85
85
|
edges: MonographEdge[],
|
|
86
86
|
options: EdgeFilterOptions = {},
|
|
87
87
|
): MonographEdge[] {
|
|
88
|
+
// Convert arrays to Sets once before the loop for O(1) membership checks
|
|
89
|
+
const relationSet = options.relations && options.relations.length > 0
|
|
90
|
+
? new Set(options.relations) : null;
|
|
91
|
+
const confidenceSet = options.confidences && options.confidences.length > 0
|
|
92
|
+
? new Set(options.confidences) : null;
|
|
93
|
+
const minScore = options.minConfidenceScore;
|
|
94
|
+
const maxScore = options.maxConfidenceScore;
|
|
95
|
+
|
|
88
96
|
return edges.filter(e => {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
if (!options.confidences.includes(e.confidence)) return false;
|
|
94
|
-
}
|
|
95
|
-
if (options.minConfidenceScore !== undefined && e.confidenceScore < options.minConfidenceScore) {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
if (options.maxConfidenceScore !== undefined && e.confidenceScore > options.maxConfidenceScore) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
97
|
+
if (relationSet && !relationSet.has(e.relation)) return false;
|
|
98
|
+
if (confidenceSet && !confidenceSet.has(e.confidence)) return false;
|
|
99
|
+
if (minScore !== undefined && e.confidenceScore < minScore) return false;
|
|
100
|
+
if (maxScore !== undefined && e.confidenceScore > maxScore) return false;
|
|
101
101
|
return true;
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
// ── Formatting ─────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/** Format a list of filtered edges as structured text for LLM consumption. */
|
|
108
|
+
export function formatFilteredEdges(edges: MonographEdge[]): string {
|
|
109
|
+
if (edges.length === 0) return 'No edges match the filter.';
|
|
110
|
+
const lines: string[] = [`Filtered edges (${edges.length}):`];
|
|
111
|
+
for (const e of edges) {
|
|
112
|
+
const score = e.confidenceScore !== undefined ? ` [${e.confidenceScore.toFixed(2)}]` : '';
|
|
113
|
+
const reason = e.reason ? ` — ${e.reason}` : '';
|
|
114
|
+
lines.push(` ${e.sourceId} -[${e.relation}/${e.confidence}${score}]-> ${e.targetId}${reason}`);
|
|
115
|
+
}
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
@@ -75,23 +75,13 @@ export function computeHealthScore(db: Database.Database): HealthScoreResult {
|
|
|
75
75
|
const circularEdges = circularRow.c * 2;
|
|
76
76
|
|
|
77
77
|
// ── Hotspot files (churnScore > 0.5 in properties) ───────────────────────
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for (const row of hotspotCandidates) {
|
|
86
|
-
if (!row.properties) continue;
|
|
87
|
-
try {
|
|
88
|
-
const props = JSON.parse(row.properties) as Record<string, unknown>;
|
|
89
|
-
const churn = props['churnScore'];
|
|
90
|
-
if (typeof churn === 'number' && churn > 0.5) hotspotCount++;
|
|
91
|
-
} catch {
|
|
92
|
-
// ignore malformed JSON
|
|
93
|
-
}
|
|
94
|
-
}
|
|
78
|
+
// Push the numeric filter into SQLite via json_extract() — avoids O(N) JS JSON.parse loop
|
|
79
|
+
const hotspotRow = db.prepare(`
|
|
80
|
+
SELECT COUNT(*) as c FROM nodes
|
|
81
|
+
WHERE label = 'File'
|
|
82
|
+
AND CAST(json_extract(properties, '$.churnScore') AS REAL) > 0.5
|
|
83
|
+
`).get() as { c: number };
|
|
84
|
+
const hotspotCount = hotspotRow.c;
|
|
95
85
|
|
|
96
86
|
// ── Isolated nodes (degree = 0) ───────────────────────────────────────────
|
|
97
87
|
const isolatedRow = db.prepare(`
|
|
@@ -150,3 +140,43 @@ export function computeHealthScore(db: Database.Database): HealthScoreResult {
|
|
|
150
140
|
|
|
151
141
|
return { score, grade, penalties, summary };
|
|
152
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format a HealthScoreResult as structured text for LLM navigation.
|
|
146
|
+
* Provides a concise grade breakdown that an LLM can parse and act on.
|
|
147
|
+
*
|
|
148
|
+
* @param result - HealthScoreResult from computeHealthScore()
|
|
149
|
+
* @returns structured text suitable for LLM consumption
|
|
150
|
+
*/
|
|
151
|
+
export function formatHealthScore(result: HealthScoreResult): string {
|
|
152
|
+
const { score, grade, penalties } = result;
|
|
153
|
+
|
|
154
|
+
const fmt = (val: number): string => val.toFixed(1);
|
|
155
|
+
|
|
156
|
+
const lines: string[] = [
|
|
157
|
+
`health_score: ${score}/100 grade: ${grade}`,
|
|
158
|
+
'',
|
|
159
|
+
'penalties:',
|
|
160
|
+
` unreachable_files: ${fmt(penalties.unreachableFilePct)}% (max -25)`,
|
|
161
|
+
` god_nodes: ${fmt(penalties.godNodePct)}% (max -20)`,
|
|
162
|
+
` circular_edges: ${fmt(penalties.circularEdgePct)}% (max -20)`,
|
|
163
|
+
` hotspot_files: ${fmt(penalties.hotspotPct)}% (max -15)`,
|
|
164
|
+
` isolated_nodes: ${fmt(penalties.isolatedNodePct)}% (max -10)`,
|
|
165
|
+
` cross_community_edges: ${fmt(penalties.crossCommunityEdgePct)}% (max -10)`,
|
|
166
|
+
'',
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// Actionable guidance for grades below B
|
|
170
|
+
if (grade === 'F' || grade === 'D') {
|
|
171
|
+
lines.push('action_required: yes');
|
|
172
|
+
if (penalties.circularEdgePct > 5) lines.push(' - break circular import cycles (monograph_context for file dependencies)');
|
|
173
|
+
if (penalties.godNodePct > 5) lines.push(' - split god nodes (monograph_god_nodes for candidates)');
|
|
174
|
+
if (penalties.unreachableFilePct > 10) lines.push(' - investigate unreachable files (monograph_detect_changes to surface dead paths)');
|
|
175
|
+
} else if (grade === 'C') {
|
|
176
|
+
lines.push('action_suggested: yes');
|
|
177
|
+
if (penalties.hotspotPct > 10) lines.push(' - reduce churn on hotspot files');
|
|
178
|
+
if (penalties.isolatedNodePct > 10) lines.push(' - connect or remove isolated nodes');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return lines.join('\n');
|
|
182
|
+
}
|
package/src/graph/hotspots.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import type Database from 'better-sqlite3';
|
|
3
3
|
|
|
4
|
+
// ── Process-scoped TTL cache for expensive git log calls ─────────────────────
|
|
5
|
+
// Key: `${projectDir}:${windowDays}` → { output, ts }
|
|
6
|
+
const GIT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
|
+
const gitLogCache = new Map<string, { output: string; ts: number }>();
|
|
8
|
+
|
|
4
9
|
export interface HotspotResult {
|
|
5
10
|
nodeId: string;
|
|
6
11
|
nodeName: string;
|
|
@@ -47,16 +52,23 @@ export function computeHotspots(
|
|
|
47
52
|
const now = Date.now();
|
|
48
53
|
const HALF_LIFE_DAYS = 90;
|
|
49
54
|
|
|
50
|
-
// ── Step 1: Get git log with dates
|
|
55
|
+
// ── Step 1: Get git log with dates (TTL-cached per projectDir+window) ────
|
|
51
56
|
// Format: ISO-date\tfile-path (one per changed file per commit)
|
|
52
57
|
let gitOutput = '';
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const cacheKey = `${projectDir}:${windowDays}`;
|
|
59
|
+
const cached = gitLogCache.get(cacheKey);
|
|
60
|
+
if (cached && now - cached.ts < GIT_CACHE_TTL_MS) {
|
|
61
|
+
gitOutput = cached.output;
|
|
62
|
+
} else {
|
|
63
|
+
try {
|
|
64
|
+
gitOutput = execSync(
|
|
65
|
+
`git log --since="${windowDays} days ago" --name-only --pretty=format:"%ci" -- .`,
|
|
66
|
+
{ cwd: projectDir, maxBuffer: 10 * 1024 * 1024 },
|
|
67
|
+
).toString();
|
|
68
|
+
gitLogCache.set(cacheKey, { output: gitOutput, ts: now });
|
|
69
|
+
} catch {
|
|
70
|
+
return []; // not a git repo or git not available
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
// ── Step 2: Parse git log and compute churn scores ────────────────────────
|
|
@@ -164,3 +176,40 @@ export function computeHotspots(
|
|
|
164
176
|
.sort((a, b) => b.hotspotScore - a.hotspotScore)
|
|
165
177
|
.slice(0, limit);
|
|
166
178
|
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Format hotspot results as structured text with file:line hints for LLM navigation.
|
|
182
|
+
*
|
|
183
|
+
* @param hotspots - Results from computeHotspots()
|
|
184
|
+
* @returns structured text suitable for LLM consumption
|
|
185
|
+
*/
|
|
186
|
+
export function formatHotspots(hotspots: HotspotResult[]): string {
|
|
187
|
+
if (hotspots.length === 0) {
|
|
188
|
+
return 'hotspots: none found (insufficient git history or no matching file nodes)\n';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const trendMark = (t: HotspotResult['trend']): string => {
|
|
192
|
+
if (t === 'accelerating') return '+';
|
|
193
|
+
if (t === 'cooling') return '-';
|
|
194
|
+
return '~';
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const lines: string[] = [
|
|
198
|
+
`hotspots: ${hotspots.length} high-risk files (churn x centrality)`,
|
|
199
|
+
'',
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
hotspots.forEach((h, i) => {
|
|
203
|
+
lines.push(`[${i + 1}] ${h.nodeName} trend:${trendMark(h.trend)}`);
|
|
204
|
+
lines.push(` file: ${h.filePath}:1`);
|
|
205
|
+
lines.push(` score: ${h.hotspotScore.toFixed(2)} churn: ${h.churnScore.toFixed(2)} centrality: ${h.centralityScore.toFixed(2)}`);
|
|
206
|
+
lines.push(` commits: ${h.rawCommitCount} last: ${h.lastCommitDate ?? 'unknown'}`);
|
|
207
|
+
lines.push('');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const accelerating = hotspots.filter(h => h.trend === 'accelerating').length;
|
|
211
|
+
const cooling = hotspots.filter(h => h.trend === 'cooling').length;
|
|
212
|
+
lines.push(`summary: ${accelerating} accelerating, ${cooling} cooling, ${hotspots.length - accelerating - cooling} stable`);
|
|
213
|
+
|
|
214
|
+
return lines.join('\n');
|
|
215
|
+
}
|
|
@@ -10,7 +10,9 @@ export interface ImportChainOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Trace all import chains (paths) from `sourceId` to `targetId`.
|
|
12
12
|
*
|
|
13
|
-
* Uses BFS
|
|
13
|
+
* Uses frontier-based BFS with lazy edge loading: only fetches outgoing edges
|
|
14
|
+
* for nodes as they are visited, avoiding loading the full edge table when
|
|
15
|
+
* the graph is large. Returns all simple paths up to `maxDepth`.
|
|
14
16
|
*
|
|
15
17
|
* @param db - The MonographDb instance
|
|
16
18
|
* @param sourceId - Starting node id
|
|
@@ -26,28 +28,27 @@ export function traceImportChain(
|
|
|
26
28
|
): string[][] {
|
|
27
29
|
const { maxDepth = 10, maxPaths = 100 } = options;
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const nodeSet = new Set(nodeRows.map(r => r.id));
|
|
33
|
-
if (!nodeSet.has(sourceId) || !nodeSet.has(targetId)) return [];
|
|
31
|
+
// Quick existence check — avoids loading any edge data for invalid IDs
|
|
32
|
+
const exists = db.prepare('SELECT 1 FROM nodes WHERE id = ? LIMIT 1');
|
|
33
|
+
if (!exists.get(sourceId) || !exists.get(targetId)) return [];
|
|
34
34
|
if (sourceId === targetId) return [[sourceId]];
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Lazy adjacency cache: only loaded for nodes we actually visit
|
|
37
|
+
const adjCache = new Map<string, string[]>();
|
|
38
|
+
const edgeStmt = db.prepare(
|
|
39
|
+
'SELECT target_id FROM edges WHERE source_id = ? AND source_id != target_id',
|
|
40
|
+
);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
function getNeighbors(nodeId: string): string[] {
|
|
43
|
+
let neighbors = adjCache.get(nodeId);
|
|
44
|
+
if (neighbors === undefined) {
|
|
45
|
+
neighbors = (edgeStmt.all(nodeId) as { target_id: string }[]).map(r => r.target_id);
|
|
46
|
+
adjCache.set(nodeId, neighbors);
|
|
47
|
+
}
|
|
48
|
+
return neighbors;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// DFS with path tracking
|
|
51
|
+
// DFS with path tracking (iterative to avoid stack overflow on deep graphs)
|
|
51
52
|
const results: string[][] = [];
|
|
52
53
|
const stack: Array<{ node: string; path: string[]; visited: Set<string> }> = [
|
|
53
54
|
{ node: sourceId, path: [sourceId], visited: new Set([sourceId]) },
|
|
@@ -59,7 +60,7 @@ export function traceImportChain(
|
|
|
59
60
|
// the target would require one more edge which would exceed maxDepth.
|
|
60
61
|
if (path.length - 1 >= maxDepth) continue;
|
|
61
62
|
|
|
62
|
-
for (const neighbor of
|
|
63
|
+
for (const neighbor of getNeighbors(node)) {
|
|
63
64
|
if (visited.has(neighbor)) continue;
|
|
64
65
|
if (neighbor === targetId) {
|
|
65
66
|
results.push([...path, neighbor]);
|
|
@@ -76,3 +77,72 @@ export function traceImportChain(
|
|
|
76
77
|
|
|
77
78
|
return results;
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Structured text formatter for LLM consumption
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format import-chain paths as structured text.
|
|
87
|
+
*
|
|
88
|
+
* Resolves node IDs to human-readable names (name + file_path) for LLM
|
|
89
|
+
* context injection. Each path is printed as an arrow chain with file:line
|
|
90
|
+
* hints where available.
|
|
91
|
+
*
|
|
92
|
+
* @param db - The MonographDb instance (used for name resolution)
|
|
93
|
+
* @param paths - Result of traceImportChain()
|
|
94
|
+
* @param sourceId - Source node id (for summary line)
|
|
95
|
+
* @param targetId - Target node id (for summary line)
|
|
96
|
+
* @returns Structured text string
|
|
97
|
+
*/
|
|
98
|
+
export function formatImportChain(
|
|
99
|
+
db: MonographDb,
|
|
100
|
+
paths: string[][],
|
|
101
|
+
sourceId: string,
|
|
102
|
+
targetId: string,
|
|
103
|
+
): string {
|
|
104
|
+
if (paths.length === 0) {
|
|
105
|
+
return `Import chain: no path found from ${sourceId} to ${targetId}.`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Batch-resolve all node IDs in the result set
|
|
109
|
+
const allIds = [...new Set(paths.flat())];
|
|
110
|
+
const placeholders = allIds.map(() => '?').join(',');
|
|
111
|
+
const nodeRows = db
|
|
112
|
+
.prepare(
|
|
113
|
+
`SELECT id, name, file_path, start_line FROM nodes WHERE id IN (${placeholders})`,
|
|
114
|
+
)
|
|
115
|
+
.all(...allIds) as {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
file_path: string | null;
|
|
119
|
+
start_line: number | null;
|
|
120
|
+
}[];
|
|
121
|
+
|
|
122
|
+
const nodeInfo = new Map<string, { name: string; loc: string }>();
|
|
123
|
+
for (const row of nodeRows) {
|
|
124
|
+
const loc =
|
|
125
|
+
row.file_path != null
|
|
126
|
+
? row.start_line != null
|
|
127
|
+
? `${row.file_path}:${row.start_line}`
|
|
128
|
+
: row.file_path
|
|
129
|
+
: row.id;
|
|
130
|
+
nodeInfo.set(row.id, { name: row.name ?? row.id, loc });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [
|
|
134
|
+
`Import chain: ${paths.length} path${paths.length === 1 ? '' : 's'} from ${nodeInfo.get(sourceId)?.name ?? sourceId} to ${nodeInfo.get(targetId)?.name ?? targetId}`,
|
|
135
|
+
'',
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < paths.length; i++) {
|
|
139
|
+
lines.push(`Path ${i + 1} (${paths[i].length} node${paths[i].length === 1 ? '' : 's'}):`);
|
|
140
|
+
for (const id of paths[i]) {
|
|
141
|
+
const info = nodeInfo.get(id);
|
|
142
|
+
lines.push(` ${info?.name ?? id} — ${info?.loc ?? id}`);
|
|
143
|
+
}
|
|
144
|
+
if (i < paths.length - 1) lines.push('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|