@monoes/monograph 1.2.0 → 1.2.1
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/__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/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.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/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/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 +49 -4
- package/src/graph/normalize-id.ts +26 -2
- package/src/graph/pagerank.ts +69 -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 +96 -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/coverage/runtime.ts +0 -249
package/src/graph/complexity.ts
CHANGED
|
@@ -32,6 +32,12 @@ export function computeCrapScore(cc: number, coverage: number): number {
|
|
|
32
32
|
return cc * cc * Math.pow(1 - cov, 3) + cc;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function percentile(arr: number[], p: number): number {
|
|
36
|
+
if (arr.length === 0) return 0;
|
|
37
|
+
const idx = Math.floor((p / 100) * (arr.length - 1));
|
|
38
|
+
return arr[idx];
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* Compute cyclomatic and cognitive complexity for all Function/Method nodes
|
|
37
43
|
* in the knowledge graph. Uses graph degree as a proxy for decision points
|
|
@@ -41,9 +47,11 @@ export function computeCrapScore(cc: number, coverage: number): number {
|
|
|
41
47
|
* - Cognitive complexity proxy: (endLine - startLine) / 10, capped at 20
|
|
42
48
|
* - LOC: endLine - startLine + 1 (or 1 if missing)
|
|
43
49
|
* - CRAP: computed with coverage = 0 (worst-case, no test data available)
|
|
50
|
+
*
|
|
51
|
+
* Batches all SQL work to avoid N+1 queries (2 GROUP BY queries replace 2×N individual queries).
|
|
44
52
|
*/
|
|
45
53
|
export function computeComplexity(db: MonographDb): ComplexityReport {
|
|
46
|
-
//
|
|
54
|
+
// Fetch all function/method nodes in one query
|
|
47
55
|
const rows = db.prepare(`
|
|
48
56
|
SELECT id, name, file_path, start_line, end_line, properties
|
|
49
57
|
FROM nodes
|
|
@@ -57,52 +65,53 @@ export function computeComplexity(db: MonographDb): ComplexityReport {
|
|
|
57
65
|
properties: string | null;
|
|
58
66
|
}>;
|
|
59
67
|
|
|
68
|
+
if (rows.length === 0) {
|
|
69
|
+
return { functions: [], p50cc: 0, p90cc: 0, p95cc: 0, highComplexityCount: 0, criticalCount: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Batch edge counts: 2 GROUP BY queries replace 2×N individual queries ─
|
|
73
|
+
const callsMap = new Map<string, number>();
|
|
74
|
+
for (const { source_id, c } of db.prepare(`
|
|
75
|
+
SELECT source_id, COUNT(*) as c FROM edges WHERE relation = 'CALLS' GROUP BY source_id
|
|
76
|
+
`).all() as { source_id: string; c: number }[]) {
|
|
77
|
+
callsMap.set(source_id, c);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const accessesMap = new Map<string, number>();
|
|
81
|
+
for (const { source_id, c } of db.prepare(`
|
|
82
|
+
SELECT source_id, COUNT(*) as c FROM edges WHERE relation = 'ACCESSES' GROUP BY source_id
|
|
83
|
+
`).all() as { source_id: string; c: number }[]) {
|
|
84
|
+
accessesMap.set(source_id, c);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Compute metrics for each function ────────────────────────────────────
|
|
60
88
|
const functions: FunctionComplexity[] = [];
|
|
89
|
+
const propUpdates: Array<{ id: string; props: string }> = [];
|
|
61
90
|
|
|
62
91
|
for (const row of rows) {
|
|
63
92
|
const props = row.properties ? JSON.parse(row.properties) as Record<string, unknown> : {};
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
SELECT COUNT(*) as c FROM edges
|
|
68
|
-
WHERE source_id = ? AND relation = 'CALLS'
|
|
69
|
-
`).get(row.id) as { c: number }).c;
|
|
70
|
-
|
|
71
|
-
// Outgoing ACCESSES edges
|
|
72
|
-
const accessesCount = (db.prepare(`
|
|
73
|
-
SELECT COUNT(*) as c FROM edges
|
|
74
|
-
WHERE source_id = ? AND relation = 'ACCESSES'
|
|
75
|
-
`).get(row.id) as { c: number }).c;
|
|
94
|
+
const callsCount = callsMap.get(row.id) ?? 0;
|
|
95
|
+
const accessesCount = accessesMap.get(row.id) ?? 0;
|
|
76
96
|
|
|
77
|
-
// Cyclomatic complexity proxy: decision points + 1
|
|
78
97
|
const cyclomaticComplexity = Math.round(callsCount * 0.5 + accessesCount * 0.2 + 1);
|
|
79
98
|
|
|
80
|
-
// Cognitive complexity proxy: nesting depth approximation capped at 20
|
|
81
99
|
const lineSpan = (row.start_line != null && row.end_line != null)
|
|
82
100
|
? row.end_line - row.start_line
|
|
83
101
|
: 0;
|
|
84
102
|
const cognitiveComplexity = Math.min(Math.round(lineSpan / 10), 20);
|
|
85
|
-
|
|
86
|
-
// LOC
|
|
87
103
|
const linesOfCode = (row.start_line != null && row.end_line != null)
|
|
88
104
|
? row.end_line - row.start_line + 1
|
|
89
105
|
: 1;
|
|
90
106
|
|
|
91
|
-
// Param count from stored properties
|
|
92
107
|
const paramCount = typeof props.paramCount === 'number' ? props.paramCount : 0;
|
|
93
|
-
|
|
94
|
-
// CRAP score with no coverage data (worst-case: coverage = 0)
|
|
95
108
|
const crapScore = computeCrapScore(cyclomaticComplexity, 0);
|
|
96
109
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
cyclomaticComplexity,
|
|
101
|
-
|
|
102
|
-
crapScore,
|
|
103
|
-
};
|
|
104
|
-
db.prepare('UPDATE nodes SET properties = ? WHERE id = ?')
|
|
105
|
-
.run(JSON.stringify(updatedProps), row.id);
|
|
110
|
+
// Stage property update — batched in a single transaction below
|
|
111
|
+
propUpdates.push({
|
|
112
|
+
id: row.id,
|
|
113
|
+
props: JSON.stringify({ ...props, cyclomaticComplexity, cognitiveComplexity, crapScore }),
|
|
114
|
+
});
|
|
106
115
|
|
|
107
116
|
functions.push({
|
|
108
117
|
nodeId: row.id,
|
|
@@ -118,28 +127,74 @@ export function computeComplexity(db: MonographDb): ComplexityReport {
|
|
|
118
127
|
});
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (arr.length === 0) return 0;
|
|
127
|
-
const idx = Math.floor((p / 100) * (arr.length - 1));
|
|
128
|
-
return arr[idx];
|
|
129
|
-
}
|
|
130
|
+
// ── Batch UPDATE all property writes in one transaction ───────────────────
|
|
131
|
+
const updateStmt = db.prepare('UPDATE nodes SET properties = ? WHERE id = ?');
|
|
132
|
+
db.transaction((updates: typeof propUpdates) => {
|
|
133
|
+
for (const { id, props } of updates) updateStmt.run(props, id);
|
|
134
|
+
})(propUpdates);
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const p95cc = percentile(ccValues, 95);
|
|
134
|
-
const highComplexityCount = functions.filter(f => f.cyclomaticComplexity > 10).length;
|
|
135
|
-
const criticalCount = functions.filter(f => f.cyclomaticComplexity > 20).length;
|
|
136
|
+
// ── Compute percentiles ───────────────────────────────────────────────────
|
|
137
|
+
const ccValues = functions.map(f => f.cyclomaticComplexity).sort((a, b) => a - b);
|
|
136
138
|
|
|
137
139
|
return {
|
|
138
140
|
functions,
|
|
139
|
-
p50cc,
|
|
140
|
-
p90cc,
|
|
141
|
-
p95cc,
|
|
142
|
-
highComplexityCount,
|
|
143
|
-
criticalCount,
|
|
141
|
+
p50cc: percentile(ccValues, 50),
|
|
142
|
+
p90cc: percentile(ccValues, 90),
|
|
143
|
+
p95cc: percentile(ccValues, 95),
|
|
144
|
+
highComplexityCount: functions.filter(f => f.cyclomaticComplexity > 10).length,
|
|
145
|
+
criticalCount: functions.filter(f => f.cyclomaticComplexity > 20).length,
|
|
144
146
|
};
|
|
145
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Format a ComplexityReport as structured text with file:line hints for LLM navigation.
|
|
151
|
+
*
|
|
152
|
+
* @param report - ComplexityReport from computeComplexity()
|
|
153
|
+
* @param topN - number of worst offenders to list (default 10)
|
|
154
|
+
* @returns structured text suitable for LLM consumption
|
|
155
|
+
*/
|
|
156
|
+
export function formatComplexity(report: ComplexityReport, topN = 10): string {
|
|
157
|
+
const { functions, p50cc, p90cc, p95cc, highComplexityCount, criticalCount } = report;
|
|
158
|
+
|
|
159
|
+
if (functions.length === 0) {
|
|
160
|
+
return 'complexity: no Function/Method nodes found in graph\n';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const lines: string[] = [
|
|
164
|
+
`complexity: ${functions.length} functions analysed`,
|
|
165
|
+
` p50_cc: ${p50cc} p90_cc: ${p90cc} p95_cc: ${p95cc}`,
|
|
166
|
+
` high(cc>10): ${highComplexityCount} critical(cc>20): ${criticalCount}`,
|
|
167
|
+
'',
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
// Sort by crapScore descending (worst first)
|
|
171
|
+
const worst = [...functions]
|
|
172
|
+
.sort((a, b) => b.crapScore - a.crapScore)
|
|
173
|
+
.slice(0, topN);
|
|
174
|
+
|
|
175
|
+
if (worst.length > 0) {
|
|
176
|
+
lines.push(`top_${topN}_worst_crap:`);
|
|
177
|
+
for (const fn of worst) {
|
|
178
|
+
const loc = fn.filePath
|
|
179
|
+
? `${fn.filePath}:${fn.startLine ?? 1}`
|
|
180
|
+
: `<unknown>:1`;
|
|
181
|
+
lines.push(` - ${fn.name}`);
|
|
182
|
+
lines.push(` file: ${loc}`);
|
|
183
|
+
lines.push(` cc: ${fn.cyclomaticComplexity} crap: ${fn.crapScore.toFixed(1)} loc: ${fn.linesOfCode}`);
|
|
184
|
+
}
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const critical = functions.filter(f => f.cyclomaticComplexity > 20);
|
|
189
|
+
if (critical.length > 0) {
|
|
190
|
+
lines.push(`critical_functions(cc>20): ${critical.length}`);
|
|
191
|
+
for (const fn of critical.slice(0, 5)) {
|
|
192
|
+
const loc = fn.filePath ? `${fn.filePath}:${fn.startLine ?? 1}` : `<unknown>:1`;
|
|
193
|
+
lines.push(` - ${fn.name} file: ${loc} cc: ${fn.cyclomaticComplexity}`);
|
|
194
|
+
}
|
|
195
|
+
if (critical.length > 5) lines.push(` ... and ${critical.length - 5} more`);
|
|
196
|
+
lines.push('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return lines.join('\n');
|
|
200
|
+
}
|
|
@@ -36,6 +36,23 @@ export function crossReference(duplication: CrossRefDuplicationReport, deadCode:
|
|
|
36
36
|
const findings: CombinedFinding[] = [];
|
|
37
37
|
let clonesInUnusedFiles = 0, clonesWithUnusedExports = 0;
|
|
38
38
|
|
|
39
|
+
// Preindex unusedExports and unusedTypes by file path to avoid O(N) Array.find
|
|
40
|
+
// per clone instance. Each lookup becomes an O(1) Map get + a short in-range scan
|
|
41
|
+
// over the candidates for that specific file.
|
|
42
|
+
const exportsByPath = new Map<string, Array<{ exportName: string; line: number }>>();
|
|
43
|
+
for (const e of deadCode.unusedExports) {
|
|
44
|
+
const bucket = exportsByPath.get(e.path) ?? [];
|
|
45
|
+
bucket.push({ exportName: e.exportName, line: e.line });
|
|
46
|
+
exportsByPath.set(e.path, bucket);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const typesByPath = new Map<string, Array<{ typeName: string; line: number }>>();
|
|
50
|
+
for (const t of deadCode.unusedTypes) {
|
|
51
|
+
const bucket = typesByPath.get(t.path) ?? [];
|
|
52
|
+
bucket.push({ typeName: t.typeName, line: t.line });
|
|
53
|
+
typesByPath.set(t.path, bucket);
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
for (let gi = 0; gi < duplication.cloneGroups.length; gi++) {
|
|
40
57
|
for (const inst of duplication.cloneGroups[gi].instances) {
|
|
41
58
|
if (deadCode.unusedFiles.has(inst.file)) {
|
|
@@ -43,13 +60,17 @@ export function crossReference(duplication: CrossRefDuplicationReport, deadCode:
|
|
|
43
60
|
clonesInUnusedFiles++;
|
|
44
61
|
continue;
|
|
45
62
|
}
|
|
46
|
-
|
|
63
|
+
|
|
64
|
+
const exportsForFile = exportsByPath.get(inst.file);
|
|
65
|
+
const overlap = exportsForFile?.find(e => e.line >= inst.startLine && e.line <= inst.endLine);
|
|
47
66
|
if (overlap) {
|
|
48
67
|
findings.push({ cloneInstance: inst, deadCodeKind: { type: 'unused-export', exportName: overlap.exportName }, groupIndex: gi });
|
|
49
68
|
clonesWithUnusedExports++;
|
|
50
69
|
continue;
|
|
51
70
|
}
|
|
52
|
-
|
|
71
|
+
|
|
72
|
+
const typesForFile = typesByPath.get(inst.file);
|
|
73
|
+
const typeOverlap = typesForFile?.find(t => t.line >= inst.startLine && t.line <= inst.endLine);
|
|
53
74
|
if (typeOverlap) {
|
|
54
75
|
findings.push({ cloneInstance: inst, deadCodeKind: { type: 'unused-type', typeName: typeOverlap.typeName }, groupIndex: gi });
|
|
55
76
|
clonesWithUnusedExports++;
|
|
@@ -111,3 +111,29 @@ export function crossReferenceDuplicatesAndDeadCode(db: MonographDb): CrossRefer
|
|
|
111
111
|
crossCount: crossIds.length,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Format a CrossReferenceReport as structured text for LLM consumption.
|
|
117
|
+
*/
|
|
118
|
+
export function formatCrossReferenceReport(report: CrossReferenceReport): string {
|
|
119
|
+
const lines: string[] = [
|
|
120
|
+
`Cross-reference analysis: dead code × structural duplicates`,
|
|
121
|
+
` Dead (unreachable) file nodes : ${report.deadCount}`,
|
|
122
|
+
` Duplicated file nodes : ${report.duplicateCount}`,
|
|
123
|
+
` Both dead AND duplicated : ${report.crossCount} (highest-confidence safe-delete candidates)`,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
if (report.crossCount === 0) {
|
|
127
|
+
lines.push(' No cross-referenced candidates found.');
|
|
128
|
+
return lines.join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push('Safe-delete candidates:');
|
|
133
|
+
for (const f of report.findings) {
|
|
134
|
+
const loc = f.filePath ? ` ${f.filePath}` : ` (no path) ${f.nodeName}`;
|
|
135
|
+
lines.push(loc);
|
|
136
|
+
lines.push(` ${f.description}`);
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
@@ -25,7 +25,12 @@ function canonicalCycle(cycle: number[], idToPath: Map<number, string>): number[
|
|
|
25
25
|
minPos = i;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
if (minPos === 0) return cycle.slice(); // already canonical — single allocation
|
|
29
|
+
// Single-pass rotation without 3 intermediate arrays (2x slice + spread)
|
|
30
|
+
const n = cycle.length;
|
|
31
|
+
const result = new Array<number>(n);
|
|
32
|
+
for (let i = 0; i < n; i++) result[i] = cycle[(minPos + i) % n];
|
|
33
|
+
return result;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
function tryRecordCycle(
|
|
@@ -103,7 +108,8 @@ function enumerateElementaryCycles(
|
|
|
103
108
|
const cycles: number[][] = [];
|
|
104
109
|
const seen = new Set<string>();
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
// Use slice() rather than spread to avoid the iterator allocation overhead
|
|
112
|
+
const sortedNodes = sccNodes.slice().sort((a, b) => {
|
|
107
113
|
const pa = idToPath.get(a) ?? '';
|
|
108
114
|
const pb = idToPath.get(b) ?? '';
|
|
109
115
|
return pa < pb ? -1 : pa > pb ? 1 : 0;
|
|
@@ -284,9 +290,22 @@ export function findCycles(
|
|
|
284
290
|
});
|
|
285
291
|
|
|
286
292
|
return result.map(cycle => ({
|
|
287
|
-
|
|
293
|
+
// slice() instead of spread to avoid iterator overhead
|
|
294
|
+
files: cycle.slice()
|
|
288
295
|
.sort((a, b) => (idToPath.get(a) ?? '').localeCompare(idToPath.get(b) ?? ''))
|
|
289
296
|
.map(id => idToPath.get(id) ?? String(id)),
|
|
290
297
|
length: cycle.length,
|
|
291
298
|
}));
|
|
292
299
|
}
|
|
300
|
+
|
|
301
|
+
/** Format cycle groups as structured text for LLM consumption. */
|
|
302
|
+
export function formatCycleGroups(groups: CycleGroup[]): string {
|
|
303
|
+
if (groups.length === 0) return 'No import cycles detected.';
|
|
304
|
+
const lines: string[] = [`Import Cycles (${groups.length} total):`];
|
|
305
|
+
for (const g of groups.slice(0, 30)) {
|
|
306
|
+
lines.push(`\n Cycle (length ${g.length}):`);
|
|
307
|
+
for (const f of g.files) lines.push(` ${f}`);
|
|
308
|
+
}
|
|
309
|
+
if (groups.length > 30) lines.push(`\n ... and ${groups.length - 30} more cycles`);
|
|
310
|
+
return lines.join('\n');
|
|
311
|
+
}
|
package/src/graph/cycles.ts
CHANGED
|
@@ -1,71 +1,127 @@
|
|
|
1
1
|
import type { MonographDb } from '../storage/db.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function findStronglyConnectedComponents(db: MonographDb): string[][] {
|
|
13
|
-
const nodeRows = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
|
|
14
|
-
const edgeRows = db.prepare('SELECT source_id, target_id FROM edges').all() as {
|
|
15
|
-
source_id: string;
|
|
16
|
-
target_id: string;
|
|
17
|
-
}[];
|
|
18
|
-
|
|
19
|
-
if (nodeRows.length === 0) return [];
|
|
20
|
-
|
|
21
|
-
const nodes = nodeRows.map(r => r.id);
|
|
3
|
+
/** Build adjacency maps from DB rows (shared by both exports). */
|
|
4
|
+
function buildAdjacency(
|
|
5
|
+
nodes: string[],
|
|
6
|
+
edgeRows: { source_id: string; target_id: string }[],
|
|
7
|
+
): {
|
|
8
|
+
adj: Map<string, string[]>;
|
|
9
|
+
radj: Map<string, string[]>;
|
|
10
|
+
selfLoops: Set<string>;
|
|
11
|
+
} {
|
|
22
12
|
const nodeSet = new Set(nodes);
|
|
23
|
-
|
|
24
13
|
const adj = new Map<string, string[]>();
|
|
25
14
|
const radj = new Map<string, string[]>();
|
|
26
15
|
for (const n of nodes) {
|
|
27
16
|
adj.set(n, []);
|
|
28
17
|
radj.set(n, []);
|
|
29
18
|
}
|
|
30
|
-
|
|
19
|
+
const selfLoops = new Set<string>();
|
|
31
20
|
for (const { source_id: src, target_id: tgt } of edgeRows) {
|
|
32
21
|
if (!nodeSet.has(src) || !nodeSet.has(tgt)) continue;
|
|
33
|
-
if (src === tgt)
|
|
22
|
+
if (src === tgt) {
|
|
23
|
+
selfLoops.add(src);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
34
26
|
adj.get(src)!.push(tgt);
|
|
35
27
|
radj.get(tgt)!.push(src);
|
|
36
28
|
}
|
|
29
|
+
return { adj, radj, selfLoops };
|
|
30
|
+
}
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
/** Iterative DFS pass 1: populate finish order (replaces recursive dfs1). */
|
|
33
|
+
function iterativeDfs1(
|
|
34
|
+
roots: string[],
|
|
35
|
+
adj: Map<string, string[]>,
|
|
36
|
+
visited: Set<string>,
|
|
37
|
+
finishOrder: string[],
|
|
38
|
+
): void {
|
|
39
|
+
for (const root of roots) {
|
|
40
|
+
if (visited.has(root)) continue;
|
|
41
|
+
// Stack entries: [node, neighborIndex]
|
|
42
|
+
const stack: [string, number][] = [[root, 0]];
|
|
43
|
+
visited.add(root);
|
|
44
|
+
while (stack.length > 0) {
|
|
45
|
+
const frame = stack[stack.length - 1];
|
|
46
|
+
const [node, idx] = frame;
|
|
47
|
+
const neighbors = adj.get(node) ?? [];
|
|
48
|
+
if (idx < neighbors.length) {
|
|
49
|
+
frame[1]++;
|
|
50
|
+
const neighbor = neighbors[idx];
|
|
51
|
+
if (!visited.has(neighbor)) {
|
|
52
|
+
visited.add(neighbor);
|
|
53
|
+
stack.push([neighbor, 0]);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
stack.pop();
|
|
57
|
+
finishOrder.push(node);
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
|
-
finishOrder.push(node);
|
|
47
60
|
}
|
|
61
|
+
}
|
|
48
62
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
/** Iterative DFS pass 2: collect SCC component (replaces recursive dfs2). */
|
|
64
|
+
function iterativeDfs2(
|
|
65
|
+
root: string,
|
|
66
|
+
radj: Map<string, string[]>,
|
|
67
|
+
assigned: Set<string>,
|
|
68
|
+
component: string[],
|
|
69
|
+
): void {
|
|
70
|
+
const stack: string[] = [root];
|
|
71
|
+
assigned.add(root);
|
|
72
|
+
while (stack.length > 0) {
|
|
73
|
+
const node = stack.pop()!;
|
|
58
74
|
component.push(node);
|
|
59
75
|
for (const neighbor of radj.get(node) ?? []) {
|
|
60
|
-
if (!assigned.has(neighbor))
|
|
76
|
+
if (!assigned.has(neighbor)) {
|
|
77
|
+
assigned.add(neighbor);
|
|
78
|
+
stack.push(neighbor);
|
|
79
|
+
}
|
|
61
80
|
}
|
|
62
81
|
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch nodes and edges once and return raw rows (shared helper).
|
|
86
|
+
*/
|
|
87
|
+
function fetchGraphRows(db: MonographDb): {
|
|
88
|
+
nodes: string[];
|
|
89
|
+
edgeRows: { source_id: string; target_id: string }[];
|
|
90
|
+
} {
|
|
91
|
+
const nodeRows = db.prepare('SELECT id FROM nodes').all() as { id: string }[];
|
|
92
|
+
const edgeRows = db.prepare('SELECT source_id, target_id FROM edges').all() as {
|
|
93
|
+
source_id: string;
|
|
94
|
+
target_id: string;
|
|
95
|
+
}[];
|
|
96
|
+
return { nodes: nodeRows.map(r => r.id), edgeRows };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find ALL strongly connected components (SCCs) using iterative Kosaraju's algorithm.
|
|
101
|
+
*
|
|
102
|
+
* Returns every SCC, including singleton components (single nodes with no self-loop).
|
|
103
|
+
* This is the full SCC decomposition of the graph.
|
|
104
|
+
*
|
|
105
|
+
* @param db - The MonographDb instance
|
|
106
|
+
* @returns Array of SCCs; each SCC is an array of node ids in that component.
|
|
107
|
+
*/
|
|
108
|
+
export function findStronglyConnectedComponents(db: MonographDb): string[][] {
|
|
109
|
+
const { nodes, edgeRows } = fetchGraphRows(db);
|
|
110
|
+
if (nodes.length === 0) return [];
|
|
63
111
|
|
|
112
|
+
const { adj, radj } = buildAdjacency(nodes, edgeRows);
|
|
113
|
+
|
|
114
|
+
const visited = new Set<string>();
|
|
115
|
+
const finishOrder: string[] = [];
|
|
116
|
+
iterativeDfs1(nodes, adj, visited, finishOrder);
|
|
117
|
+
|
|
118
|
+
const assigned = new Set<string>();
|
|
119
|
+
const sccs: string[][] = [];
|
|
64
120
|
for (let i = finishOrder.length - 1; i >= 0; i--) {
|
|
65
121
|
const node = finishOrder[i];
|
|
66
122
|
if (!assigned.has(node)) {
|
|
67
123
|
const component: string[] = [];
|
|
68
|
-
|
|
124
|
+
iterativeDfs2(node, radj, assigned, component);
|
|
69
125
|
sccs.push(component);
|
|
70
126
|
}
|
|
71
127
|
}
|
|
@@ -77,77 +133,31 @@ export function findStronglyConnectedComponents(db: MonographDb): string[][] {
|
|
|
77
133
|
* Find all strongly connected components (SCCs) with more than 1 node,
|
|
78
134
|
* or self-loops, and return them as cycle node lists.
|
|
79
135
|
*
|
|
80
|
-
* Uses Kosaraju's algorithm to
|
|
81
|
-
* a cycle. Self-loops
|
|
136
|
+
* Uses iterative Kosaraju's algorithm to avoid stack overflow on large graphs.
|
|
137
|
+
* Each SCC with size > 1 represents a cycle. Self-loops are also detected.
|
|
82
138
|
*
|
|
83
139
|
* @param db - The MonographDb instance
|
|
84
140
|
* @returns Array of cycles; each cycle is an array of node ids participating in it.
|
|
85
141
|
*/
|
|
86
142
|
export function findCycles(db: MonographDb): string[][] {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
source_id: string;
|
|
90
|
-
target_id: string;
|
|
91
|
-
}[];
|
|
92
|
-
|
|
93
|
-
if (nodeRows.length === 0) return [];
|
|
94
|
-
|
|
95
|
-
const nodes = nodeRows.map(r => r.id);
|
|
96
|
-
const nodeSet = new Set(nodes);
|
|
143
|
+
const { nodes, edgeRows } = fetchGraphRows(db);
|
|
144
|
+
if (nodes.length === 0) return [];
|
|
97
145
|
|
|
98
|
-
|
|
99
|
-
const adj = new Map<string, string[]>();
|
|
100
|
-
const radj = new Map<string, string[]>();
|
|
101
|
-
for (const n of nodes) {
|
|
102
|
-
adj.set(n, []);
|
|
103
|
-
radj.set(n, []);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const selfLoops = new Set<string>();
|
|
107
|
-
|
|
108
|
-
for (const { source_id: src, target_id: tgt } of edgeRows) {
|
|
109
|
-
if (!nodeSet.has(src) || !nodeSet.has(tgt)) continue;
|
|
110
|
-
if (src === tgt) {
|
|
111
|
-
selfLoops.add(src);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
adj.get(src)!.push(tgt);
|
|
115
|
-
radj.get(tgt)!.push(src);
|
|
116
|
-
}
|
|
146
|
+
const { adj, radj, selfLoops } = buildAdjacency(nodes, edgeRows);
|
|
117
147
|
|
|
118
|
-
//
|
|
148
|
+
// Pass 1: iterative DFS on original graph, build finish order
|
|
119
149
|
const visited = new Set<string>();
|
|
120
150
|
const finishOrder: string[] = [];
|
|
151
|
+
iterativeDfs1(nodes, adj, visited, finishOrder);
|
|
121
152
|
|
|
122
|
-
|
|
123
|
-
visited.add(node);
|
|
124
|
-
for (const neighbor of adj.get(node) ?? []) {
|
|
125
|
-
if (!visited.has(neighbor)) dfs1(neighbor);
|
|
126
|
-
}
|
|
127
|
-
finishOrder.push(node);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (const node of nodes) {
|
|
131
|
-
if (!visited.has(node)) dfs1(node);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Kosaraju's algorithm — pass 2: DFS on reverse graph in reverse finish order
|
|
153
|
+
// Pass 2: iterative DFS on reverse graph in reverse finish order
|
|
135
154
|
const assigned = new Set<string>();
|
|
136
155
|
const sccs: string[][] = [];
|
|
137
|
-
|
|
138
|
-
function dfs2(node: string, component: string[]): void {
|
|
139
|
-
assigned.add(node);
|
|
140
|
-
component.push(node);
|
|
141
|
-
for (const neighbor of radj.get(node) ?? []) {
|
|
142
|
-
if (!assigned.has(neighbor)) dfs2(neighbor, component);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
156
|
for (let i = finishOrder.length - 1; i >= 0; i--) {
|
|
147
157
|
const node = finishOrder[i];
|
|
148
158
|
if (!assigned.has(node)) {
|
|
149
159
|
const component: string[] = [];
|
|
150
|
-
|
|
160
|
+
iterativeDfs2(node, radj, assigned, component);
|
|
151
161
|
if (component.length > 1) {
|
|
152
162
|
sccs.push(component);
|
|
153
163
|
}
|
|
@@ -161,3 +171,52 @@ export function findCycles(db: MonographDb): string[][] {
|
|
|
161
171
|
|
|
162
172
|
return sccs;
|
|
163
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Format cycle results as structured text with file:line hints for LLM navigation.
|
|
177
|
+
*
|
|
178
|
+
* @param cycles - Array of cycle arrays (node ids / file paths)
|
|
179
|
+
* @param nodeToFile - Optional map from node id to file path for resolving paths
|
|
180
|
+
* @returns Structured text output suitable for LLM consumption
|
|
181
|
+
*/
|
|
182
|
+
export function formatCycles(
|
|
183
|
+
cycles: string[][],
|
|
184
|
+
nodeToFile?: Map<string, string>,
|
|
185
|
+
): string {
|
|
186
|
+
if (cycles.length === 0) {
|
|
187
|
+
return 'cycles: none\nstatus: no circular dependencies detected\n';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const resolve = (id: string): string => nodeToFile?.get(id) ?? id;
|
|
191
|
+
|
|
192
|
+
const lines: string[] = [
|
|
193
|
+
`cycles: ${cycles.length} circular dependency group(s) found`,
|
|
194
|
+
'',
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
// Sort by cycle length ascending, then alphabetically
|
|
198
|
+
const sorted = [...cycles].sort((a, b) => {
|
|
199
|
+
if (a.length !== b.length) return a.length - b.length;
|
|
200
|
+
return resolve(a[0]).localeCompare(resolve(b[0]));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
sorted.forEach((cycle, i) => {
|
|
204
|
+
const isSelfLoop = cycle.length === 1;
|
|
205
|
+
const label = isSelfLoop ? 'self-loop' : `cycle(${cycle.length})`;
|
|
206
|
+
lines.push(`[${i + 1}] ${label}`);
|
|
207
|
+
for (const node of cycle) {
|
|
208
|
+
lines.push(` file: ${resolve(node)}:1`);
|
|
209
|
+
}
|
|
210
|
+
if (!isSelfLoop) {
|
|
211
|
+
// Show the closing edge
|
|
212
|
+
lines.push(` closes: ${resolve(cycle[cycle.length - 1])} → ${resolve(cycle[0])}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push('');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const selfLoopCount = sorted.filter(c => c.length === 1).length;
|
|
218
|
+
const multiNodeCount = sorted.length - selfLoopCount;
|
|
219
|
+
lines.push(`summary: ${multiNodeCount} multi-node cycle(s), ${selfLoopCount} self-loop(s)`);
|
|
220
|
+
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|