@monoes/monograph 1.3.0 → 1.4.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/.monomind/registry.json +6 -0
- package/__tests__/staleness/git-staleness.test.ts +3 -3
- package/dist/src/analysis/benchmark.js.map +1 -1
- package/dist/src/analysis/changed-files.d.ts.map +1 -1
- package/dist/src/analysis/changed-files.js +11 -2
- package/dist/src/analysis/changed-files.js.map +1 -1
- package/dist/src/analysis/changed-workspaces.d.ts.map +1 -1
- package/dist/src/analysis/changed-workspaces.js +11 -3
- package/dist/src/analysis/changed-workspaces.js.map +1 -1
- package/dist/src/analysis/churn.d.ts +1 -1
- package/dist/src/analysis/churn.d.ts.map +1 -1
- package/dist/src/analysis/churn.js +56 -10
- package/dist/src/analysis/churn.js.map +1 -1
- package/dist/src/analysis/cluster-quality.d.ts +4 -4
- package/dist/src/analysis/cluster-quality.d.ts.map +1 -1
- package/dist/src/analysis/cluster-quality.js +24 -27
- package/dist/src/analysis/cluster-quality.js.map +1 -1
- package/dist/src/analysis/combined.d.ts.map +1 -1
- package/dist/src/analysis/combined.js +2 -0
- package/dist/src/analysis/combined.js.map +1 -1
- package/dist/src/analysis/feature-flags.d.ts.map +1 -1
- package/dist/src/analysis/feature-flags.js +11 -5
- package/dist/src/analysis/feature-flags.js.map +1 -1
- package/dist/src/analysis/progress.d.ts.map +1 -1
- package/dist/src/analysis/project-state.d.ts.map +1 -1
- package/dist/src/analysis/shape-extractor.js.map +1 -1
- package/dist/src/analysis/suppression-context.d.ts.map +1 -1
- package/dist/src/analysis/surprise-scorer.js.map +1 -1
- package/dist/src/analysis/targets.d.ts.map +1 -1
- package/dist/src/analysis/targets.js +4 -5
- package/dist/src/analysis/targets.js.map +1 -1
- package/dist/src/analysis/worker-pool.d.ts.map +1 -1
- package/dist/src/analysis/worker-pool.js.map +1 -1
- package/dist/src/analyze/unused-deps.js.map +1 -1
- package/dist/src/analyze/unused-exports.js.map +1 -1
- package/dist/src/analyze/unused-files.js.map +1 -1
- package/dist/src/cache/augment-cache.js.map +1 -1
- package/dist/src/cache/extraction-cache.d.ts +21 -0
- package/dist/src/cache/extraction-cache.d.ts.map +1 -1
- package/dist/src/cache/extraction-cache.js +62 -6
- package/dist/src/cache/extraction-cache.js.map +1 -1
- package/dist/src/check/index.js.map +1 -1
- package/dist/src/cli/check-update.js.map +1 -1
- package/dist/src/cli/doctor.d.ts.map +1 -1
- package/dist/src/cli/doctor.js +6 -10
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/config/boundary-config.js.map +1 -1
- package/dist/src/config/config-parsing.d.ts +1 -1
- package/dist/src/config/config-parsing.d.ts.map +1 -1
- package/dist/src/config/migration-types.d.ts +2 -2
- package/dist/src/config/migration-types.d.ts.map +1 -1
- package/dist/src/config/migration-types.js.map +1 -1
- package/dist/src/config/used-class-members.js.map +1 -1
- package/dist/src/config/workspace.js.map +1 -1
- package/dist/src/coverage/upload-source-maps.js.map +1 -1
- package/dist/src/duplicates/detect/filtering.d.ts.map +1 -1
- package/dist/src/duplicates/shingle-filter.js.map +1 -1
- package/dist/src/error/fallow-error.d.ts.map +1 -1
- package/dist/src/error/fallow-error.js.map +1 -1
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js.map +1 -1
- package/dist/src/export/analysis-json.js +1 -1
- package/dist/src/export/analysis-json.js.map +1 -1
- package/dist/src/export/badge.js.map +1 -1
- package/dist/src/export/dot.js.map +1 -1
- package/dist/src/export/sarif.d.ts.map +1 -1
- package/dist/src/export/sarif.js +13 -2
- package/dist/src/export/sarif.js.map +1 -1
- package/dist/src/fix/orchestrator.js.map +1 -1
- package/dist/src/graph/baseline.d.ts.map +1 -1
- package/dist/src/graph/baseline.js +29 -20
- package/dist/src/graph/baseline.js.map +1 -1
- package/dist/src/graph/cycle-detection.js.map +1 -1
- package/dist/src/graph/dead-code.d.ts +7 -2
- package/dist/src/graph/dead-code.d.ts.map +1 -1
- package/dist/src/graph/dead-code.js +19 -16
- package/dist/src/graph/dead-code.js.map +1 -1
- package/dist/src/graph/dfs.js.map +1 -1
- package/dist/src/graph/edge-filter.js.map +1 -1
- package/dist/src/graph/health-score.d.ts.map +1 -1
- package/dist/src/graph/health-score.js +11 -14
- package/dist/src/graph/health-score.js.map +1 -1
- package/dist/src/graph/hotspots.d.ts.map +1 -1
- package/dist/src/graph/hotspots.js +8 -4
- package/dist/src/graph/hotspots.js.map +1 -1
- package/dist/src/graph/import-chain.js.map +1 -1
- package/dist/src/graph/merge.js.map +1 -1
- package/dist/src/graph/node-search.js.map +1 -1
- package/dist/src/graph/node-types.d.ts.map +1 -1
- package/dist/src/graph/pagerank.d.ts.map +1 -1
- package/dist/src/graph/pagerank.js +10 -1
- package/dist/src/graph/pagerank.js.map +1 -1
- package/dist/src/graph/regex-search.js.map +1 -1
- package/dist/src/graph/regression.d.ts.map +1 -1
- package/dist/src/graph/resolve/discover-types.js.map +1 -1
- package/dist/src/graph/resolve/types.d.ts +3 -3
- package/dist/src/graph/resolve/types.d.ts.map +1 -1
- package/dist/src/graph/statistics.d.ts +3 -2
- package/dist/src/graph/statistics.d.ts.map +1 -1
- package/dist/src/graph/statistics.js +18 -7
- package/dist/src/graph/statistics.js.map +1 -1
- package/dist/src/graph/stats.d.ts.map +1 -1
- package/dist/src/graph/stats.js +2 -1
- package/dist/src/graph/stats.js.map +1 -1
- package/dist/src/groups/group-search.d.ts.map +1 -1
- package/dist/src/groups/group-search.js +9 -18
- package/dist/src/groups/group-search.js.map +1 -1
- package/dist/src/health/complexity-findings.js.map +1 -1
- package/dist/src/health/grouping.js.map +1 -1
- package/dist/src/health/health-report-types.js.map +1 -1
- package/dist/src/health/health-report.js.map +1 -1
- package/dist/src/health/targets.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/ingest/transcribe.js.map +1 -1
- package/dist/src/ingest/url-ingest.js.map +1 -1
- package/dist/src/license/manager.d.ts.map +1 -1
- package/dist/src/license/manager.js.map +1 -1
- package/dist/src/lsp/diagnostics-push.js.map +1 -1
- package/dist/src/mcp-tools/impact.d.ts.map +1 -1
- package/dist/src/mcp-tools/impact.js +7 -1
- package/dist/src/mcp-tools/impact.js.map +1 -1
- package/dist/src/mcp-tools/list-repos.d.ts +1 -1
- package/dist/src/mcp-tools/list-repos.d.ts.map +1 -1
- package/dist/src/mcp-tools/query.d.ts +1 -1
- package/dist/src/mcp-tools/query.d.ts.map +1 -1
- package/dist/src/parsers/c.js.map +1 -1
- package/dist/src/parsers/cpp.js.map +1 -1
- package/dist/src/parsers/csharp.js.map +1 -1
- package/dist/src/parsers/dart.js.map +1 -1
- package/dist/src/parsers/go.js.map +1 -1
- package/dist/src/parsers/java.js.map +1 -1
- package/dist/src/parsers/kotlin.js.map +1 -1
- package/dist/src/parsers/loader.js.map +1 -1
- package/dist/src/parsers/php.js.map +1 -1
- package/dist/src/parsers/python.js.map +1 -1
- package/dist/src/parsers/ruby.js.map +1 -1
- package/dist/src/parsers/rust.js.map +1 -1
- package/dist/src/parsers/swift.js.map +1 -1
- package/dist/src/parsers/typescript.js.map +1 -1
- package/dist/src/parsers/vue.js.map +1 -1
- package/dist/src/pipeline/effort.js.map +1 -1
- package/dist/src/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/src/pipeline/orchestrator.js +85 -24
- package/dist/src/pipeline/orchestrator.js.map +1 -1
- package/dist/src/pipeline/phases/bridge-adapters/electron-ipc.d.ts +11 -0
- package/dist/src/pipeline/phases/bridge-adapters/electron-ipc.d.ts.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/electron-ipc.js +96 -0
- package/dist/src/pipeline/phases/bridge-adapters/electron-ipc.js.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/registry.d.ts +3 -0
- package/dist/src/pipeline/phases/bridge-adapters/registry.d.ts.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/registry.js +13 -0
- package/dist/src/pipeline/phases/bridge-adapters/registry.js.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/tauri.d.ts +10 -0
- package/dist/src/pipeline/phases/bridge-adapters/tauri.d.ts.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/tauri.js +104 -0
- package/dist/src/pipeline/phases/bridge-adapters/tauri.js.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/types.d.ts +30 -0
- package/dist/src/pipeline/phases/bridge-adapters/types.d.ts.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/types.js +2 -0
- package/dist/src/pipeline/phases/bridge-adapters/types.js.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/wails.d.ts +10 -0
- package/dist/src/pipeline/phases/bridge-adapters/wails.d.ts.map +1 -0
- package/dist/src/pipeline/phases/bridge-adapters/wails.js +27 -0
- package/dist/src/pipeline/phases/bridge-adapters/wails.js.map +1 -0
- package/dist/src/pipeline/phases/bridge-resolver.d.ts +23 -0
- package/dist/src/pipeline/phases/bridge-resolver.d.ts.map +1 -0
- package/dist/src/pipeline/phases/bridge-resolver.js +70 -0
- package/dist/src/pipeline/phases/bridge-resolver.js.map +1 -0
- package/dist/src/pipeline/phases/cross-file.d.ts.map +1 -1
- package/dist/src/pipeline/phases/cross-file.js +70 -27
- package/dist/src/pipeline/phases/cross-file.js.map +1 -1
- package/dist/src/pipeline/phases/leiden.js.map +1 -1
- package/dist/src/pipeline/phases/parse.d.ts.map +1 -1
- package/dist/src/pipeline/phases/parse.js +58 -17
- package/dist/src/pipeline/phases/parse.js.map +1 -1
- package/dist/src/pipeline/phases/scan.d.ts.map +1 -1
- package/dist/src/pipeline/phases/scan.js +12 -5
- package/dist/src/pipeline/phases/scan.js.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.d.ts +13 -0
- package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.js +18 -4
- package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
- package/dist/src/pipeline/progress.d.ts.map +1 -1
- package/dist/src/pipeline/runner.d.ts.map +1 -1
- package/dist/src/pipeline/runner.js.map +1 -1
- package/dist/src/pipeline/types.d.ts +2 -0
- package/dist/src/pipeline/types.d.ts.map +1 -1
- package/dist/src/pipeline/types.js +1 -0
- package/dist/src/pipeline/types.js.map +1 -1
- package/dist/src/regression/baseline.js.map +1 -1
- package/dist/src/regression/outcome.js.map +1 -1
- package/dist/src/report/badge-svg.js.map +1 -1
- package/dist/src/report/duplication-grouping.js.map +1 -1
- package/dist/src/report/human-check.js.map +1 -1
- package/dist/src/report/human-dupes.js.map +1 -1
- package/dist/src/report/human-health.js.map +1 -1
- package/dist/src/report/json-builders.js.map +1 -1
- package/dist/src/report/number-format.js.map +1 -1
- package/dist/src/reporting/graph-report.d.ts.map +1 -1
- package/dist/src/reporting/graph-report.js +8 -9
- package/dist/src/reporting/graph-report.js.map +1 -1
- package/dist/src/scope-resolution/evidence-weights.d.ts.map +1 -1
- package/dist/src/search/hybrid-query.js.map +1 -1
- package/dist/src/security/safe-fetch.js.map +1 -1
- package/dist/src/staleness/git-staleness.d.ts.map +1 -1
- package/dist/src/staleness/git-staleness.js +5 -2
- package/dist/src/staleness/git-staleness.js.map +1 -1
- package/dist/src/storage/db.d.ts +13 -1
- package/dist/src/storage/db.d.ts.map +1 -1
- package/dist/src/storage/db.js +23 -2
- package/dist/src/storage/db.js.map +1 -1
- package/dist/src/storage/edge-store.d.ts +6 -0
- package/dist/src/storage/edge-store.d.ts.map +1 -1
- package/dist/src/storage/edge-store.js +8 -1
- package/dist/src/storage/edge-store.js.map +1 -1
- package/dist/src/storage/fts-store.d.ts.map +1 -1
- package/dist/src/storage/fts-store.js +31 -9
- package/dist/src/storage/fts-store.js.map +1 -1
- package/dist/src/storage/node-store.js.map +1 -1
- package/dist/src/storage/schema.d.ts +5 -0
- package/dist/src/storage/schema.d.ts.map +1 -1
- package/dist/src/storage/schema.js +25 -0
- package/dist/src/storage/schema.js.map +1 -1
- package/dist/src/storage/unlinked-refs.js.map +1 -1
- package/dist/src/types.d.ts +2 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/watch/watcher.d.ts.map +1 -1
- package/dist/src/watch/watcher.js +12 -2
- package/dist/src/watch/watcher.js.map +1 -1
- package/dist/src/web/analyze-api.d.ts.map +1 -1
- package/dist/src/web/analyze-api.js +3 -1
- package/dist/src/web/analyze-api.js.map +1 -1
- package/dist/src/web/api.d.ts +3 -0
- package/dist/src/web/api.d.ts.map +1 -1
- package/dist/src/web/api.js +59 -20
- package/dist/src/web/api.js.map +1 -1
- package/dist/src/web/async-jobs.d.ts.map +1 -1
- package/dist/src/web/async-jobs.js +15 -0
- package/dist/src/web/async-jobs.js.map +1 -1
- package/dist/src/web/react-ui.js +2 -2
- package/dist/src/wiki/providers.js.map +1 -1
- package/dist/src/wiki/wiki-generator.d.ts.map +1 -1
- package/dist/src/wiki/wiki-generator.js +9 -6
- package/dist/src/wiki/wiki-generator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -2
- package/src/__tests__/graph/dead-code.test.ts +27 -7
- package/src/__tests__/pipeline/phases/bridge-adapters/electron-ipc.test.ts +87 -0
- package/src/__tests__/pipeline/phases/bridge-adapters/tauri.test.ts +116 -0
- package/src/__tests__/pipeline/phases/bridge-adapters/wails.test.ts +81 -0
- package/src/__tests__/pipeline/phases/bridge-resolver.test.ts +110 -0
- package/src/analysis/changed-files.ts +14 -2
- package/src/analysis/changed-workspaces.ts +13 -4
- package/src/analysis/churn.ts +57 -10
- package/src/analysis/cluster-quality.ts +25 -26
- package/src/analysis/combined.ts +2 -0
- package/src/analysis/feature-flags.ts +10 -5
- package/src/analysis/targets.ts +4 -5
- package/src/cache/extraction-cache.ts +59 -6
- package/src/cli/doctor.ts +6 -10
- package/src/export/analysis-json.ts +1 -1
- package/src/export/sarif.ts +11 -2
- package/src/graph/baseline.ts +29 -20
- package/src/graph/dead-code.ts +19 -16
- package/src/graph/health-score.ts +13 -16
- package/src/graph/hotspots.ts +8 -4
- package/src/graph/pagerank.ts +10 -1
- package/src/graph/statistics.ts +19 -7
- package/src/graph/stats.ts +2 -1
- package/src/groups/group-search.ts +9 -27
- package/src/index.ts +3 -0
- package/src/mcp-tools/impact.ts +7 -1
- package/src/pipeline/orchestrator.ts +87 -26
- package/src/pipeline/phases/bridge-adapters/electron-ipc.ts +91 -0
- package/src/pipeline/phases/bridge-adapters/registry.ts +14 -0
- package/src/pipeline/phases/bridge-adapters/tauri.ts +103 -0
- package/src/pipeline/phases/bridge-adapters/types.ts +31 -0
- package/src/pipeline/phases/bridge-adapters/wails.ts +33 -0
- package/src/pipeline/phases/bridge-resolver.ts +77 -0
- package/src/pipeline/phases/cross-file.ts +75 -27
- package/src/pipeline/phases/parse.ts +60 -17
- package/src/pipeline/phases/scan.ts +11 -5
- package/src/pipeline/phases/scope-resolution.ts +19 -4
- package/src/pipeline/types.ts +3 -0
- package/src/reporting/graph-report.ts +8 -9
- package/src/staleness/git-staleness.ts +5 -2
- package/src/storage/db.ts +35 -1
- package/src/storage/edge-store.ts +8 -1
- package/src/storage/fts-store.ts +31 -9
- package/src/storage/schema.ts +30 -0
- package/src/watch/watcher.ts +11 -2
- package/src/web/analyze-api.ts +3 -1
- package/src/web/api.ts +62 -22
- package/src/web/async-jobs.ts +15 -0
- package/src/web/react-ui.ts +2 -2
- package/src/wiki/wiki-generator.ts +9 -6
- package/tsconfig.json +2 -1
|
@@ -98,46 +98,45 @@ export function silhouetteScore(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Compute Newman–Girvan modularity Q
|
|
101
|
+
* Compute Newman–Girvan modularity Q using the community-level formula:
|
|
102
|
+
* Q = Σ_c [e_c / m - (a_c / 2m)²]
|
|
103
|
+
* where e_c = intra-community edges, a_c = sum of degrees in community c, m = total edges.
|
|
102
104
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* where m = total edge count, k_i = degree of node i, A_ij = adjacency.
|
|
105
|
+
* O(E + N) instead of O(N²).
|
|
106
106
|
* Returns a value in (-0.5, 1].
|
|
107
107
|
*/
|
|
108
108
|
export function modularityScore(
|
|
109
109
|
memberships: Map<string, number>,
|
|
110
110
|
edges: Edge[],
|
|
111
111
|
): number {
|
|
112
|
+
if (edges.length === 0 || memberships.size === 0) return 0;
|
|
113
|
+
|
|
112
114
|
const adj = buildAdjacency(edges);
|
|
115
|
+
const m = edges.length; // directed edges count (each undirected pair counted once in input)
|
|
113
116
|
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
// Sum of degrees per community and intra-community edge count
|
|
118
|
+
const communityDegreeSum = new Map<number, number>();
|
|
119
|
+
const intraCommunityEdges = new Map<number, number>();
|
|
120
|
+
|
|
121
|
+
for (const [nodeId, cid] of memberships) {
|
|
122
|
+
const deg = adj.get(nodeId)?.size ?? 0;
|
|
123
|
+
communityDegreeSum.set(cid, (communityDegreeSum.get(cid) ?? 0) + deg);
|
|
118
124
|
}
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
for (const { sourceId, targetId } of edges) {
|
|
127
|
+
const cs = memberships.get(sourceId);
|
|
128
|
+
const ct = memberships.get(targetId);
|
|
129
|
+
if (cs !== undefined && ct !== undefined && cs === ct) {
|
|
130
|
+
intraCommunityEdges.set(cs, (intraCommunityEdges.get(cs) ?? 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
125
133
|
|
|
126
134
|
const twoM = 2 * m;
|
|
127
135
|
let Q = 0;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const u = nodes[i]!;
|
|
132
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
133
|
-
const v = nodes[j]!;
|
|
134
|
-
if (memberships.get(u) !== memberships.get(v)) continue;
|
|
135
|
-
const aij = adj.get(u)?.has(v) ? 1 : 0;
|
|
136
|
-
const ki = degree.get(u) ?? 0;
|
|
137
|
-
const kj = degree.get(v) ?? 0;
|
|
138
|
-
Q += aij - (ki * kj) / twoM;
|
|
139
|
-
}
|
|
136
|
+
for (const [cid, degSum] of communityDegreeSum) {
|
|
137
|
+
const ec = intraCommunityEdges.get(cid) ?? 0;
|
|
138
|
+
Q += ec / m - (degSum / twoM) ** 2;
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
return Q
|
|
141
|
+
return Q;
|
|
143
142
|
}
|
package/src/analysis/combined.ts
CHANGED
|
@@ -29,6 +29,8 @@ export function resolveAnalyses(opts: CombinedOptions): Set<AnalysisKind> {
|
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// monolean: stub — resolves which analyses to run but doesn't execute them.
|
|
33
|
+
// Wire up when dead-code/duplication detectors are called from a unified entry point.
|
|
32
34
|
export async function runCombined(
|
|
33
35
|
db: MonographDb,
|
|
34
36
|
opts: CombinedOptions
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, readdirSync,
|
|
1
|
+
import { readFileSync, readdirSync, lstatSync } from 'fs';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
3
|
|
|
4
4
|
export type FlagKind = 'EnvironmentVariable' | 'SdkCall' | 'ConfigObject';
|
|
@@ -170,13 +170,18 @@ export function crossReferenceWithDeadCode(
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
function walkDir(dir: string, fn: (filePath: string) => void): void {
|
|
173
|
-
let
|
|
174
|
-
try {
|
|
175
|
-
for (const
|
|
173
|
+
let dirents: import('fs').Dirent[];
|
|
174
|
+
try { dirents = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
175
|
+
for (const dirent of dirents) {
|
|
176
|
+
const entry = dirent.name;
|
|
176
177
|
if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'build') continue;
|
|
178
|
+
// Never follow symlinks — avoids infinite recursion on a symlink pointing to
|
|
179
|
+
// an ancestor directory, and avoids indexing a symlinked external tree as if
|
|
180
|
+
// it were part of the repo. Matches scan.ts's walk().
|
|
181
|
+
if (dirent.isSymbolicLink()) continue;
|
|
177
182
|
const full = join(dir, entry);
|
|
178
183
|
let st;
|
|
179
|
-
try { st =
|
|
184
|
+
try { st = lstatSync(full); } catch { continue; }
|
|
180
185
|
if (st.isDirectory()) walkDir(full, fn);
|
|
181
186
|
else if (st.isFile()) fn(full);
|
|
182
187
|
}
|
package/src/analysis/targets.ts
CHANGED
|
@@ -44,13 +44,12 @@ export function computeRefactoringTargets(db: MonographDb): RefactoringTargetsRe
|
|
|
44
44
|
n.id,
|
|
45
45
|
n.file_path,
|
|
46
46
|
n.properties,
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
COALESCE(d_in.cnt, 0) as in_degree,
|
|
48
|
+
COALESCE(d_out.cnt, 0) as out_degree
|
|
49
49
|
FROM nodes n
|
|
50
|
-
LEFT JOIN edges
|
|
51
|
-
LEFT JOIN edges
|
|
50
|
+
LEFT JOIN (SELECT target_id, COUNT(*) AS cnt FROM edges GROUP BY target_id) d_in ON d_in.target_id = n.id
|
|
51
|
+
LEFT JOIN (SELECT source_id, COUNT(*) AS cnt FROM edges GROUP BY source_id) d_out ON d_out.source_id = n.id
|
|
52
52
|
WHERE n.label = 'File' AND n.file_path IS NOT NULL
|
|
53
|
-
GROUP BY n.id
|
|
54
53
|
`).all() as FileNodeRow[];
|
|
55
54
|
|
|
56
55
|
if (rows.length === 0) {
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
2
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import type { MonographNode, MonographEdge } from '../types.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Bump this whenever the parser/extractor output format changes in a way that
|
|
8
|
+
* would make previously-cached nodes/edges stale or wrong (e.g. a parser bugfix,
|
|
9
|
+
* a change to what symbols get extracted, a change to node/edge shape). Any
|
|
10
|
+
* cached entry written under an older version is treated as a miss on read, so
|
|
11
|
+
* bumping this effectively invalidates the entire on-disk extraction cache.
|
|
12
|
+
*/
|
|
13
|
+
export const EXTRACTION_CACHE_VERSION = 1;
|
|
14
|
+
|
|
6
15
|
export interface CacheEntry {
|
|
7
16
|
fileHash: string;
|
|
8
17
|
mtimeMs?: number;
|
|
9
18
|
size?: number;
|
|
10
19
|
nodes: MonographNode[];
|
|
11
20
|
edges: MonographEdge[];
|
|
21
|
+
/** Extraction format version this entry was written under — see EXTRACTION_CACHE_VERSION. */
|
|
22
|
+
cacheVersion?: number;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
export class ExtractionCache {
|
|
@@ -32,6 +43,46 @@ export class ExtractionCache {
|
|
|
32
43
|
return join(this.dir, `${key}.json`);
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
/** tmp+rename write — cache corruption is low-stakes but a torn write from a
|
|
47
|
+
* killed process still shouldn't poison the next read. */
|
|
48
|
+
private writeAtomic(p: string, data: string): void {
|
|
49
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(tmp, data);
|
|
52
|
+
renameSync(tmp, p);
|
|
53
|
+
} catch {
|
|
54
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Delete cache entries whose mtime exceeds maxAgeMs (default 30 days).
|
|
60
|
+
* Covers files that were deleted/renamed (their cache entry is now orphaned
|
|
61
|
+
* and would otherwise sit on disk forever) as well as entries simply not
|
|
62
|
+
* touched in a long time. Single readdir + stat pass over the cache dir.
|
|
63
|
+
* Returns the number of entries removed.
|
|
64
|
+
*/
|
|
65
|
+
prune(maxAgeMs = 30 * 24 * 60 * 60 * 1000): number {
|
|
66
|
+
let removed = 0;
|
|
67
|
+
let entries: string[];
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(this.dir).filter((f) => f.endsWith('.json') && !f.startsWith('._'));
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
74
|
+
for (const f of entries) {
|
|
75
|
+
const p = join(this.dir, f);
|
|
76
|
+
try {
|
|
77
|
+
if (statSync(p).mtimeMs < cutoff) {
|
|
78
|
+
unlinkSync(p);
|
|
79
|
+
removed++;
|
|
80
|
+
}
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
|
|
35
86
|
/**
|
|
36
87
|
* Fast-path: check mtime+size before falling back to content hash.
|
|
37
88
|
* Returns cached entry if file hasn't changed, null on miss.
|
|
@@ -41,6 +92,7 @@ export class ExtractionCache {
|
|
|
41
92
|
if (!existsSync(p)) return null;
|
|
42
93
|
try {
|
|
43
94
|
const entry: CacheEntry = JSON.parse(readFileSync(p, 'utf-8'));
|
|
95
|
+
if (entry.cacheVersion !== EXTRACTION_CACHE_VERSION) return null;
|
|
44
96
|
const st = statSync(filePath);
|
|
45
97
|
if (entry.mtimeMs === st.mtimeMs && entry.size === st.size) return entry;
|
|
46
98
|
// mtime/size differ or missing — recheck content hash
|
|
@@ -49,7 +101,7 @@ export class ExtractionCache {
|
|
|
49
101
|
// Hash matches — update entry with current mtime+size for next run
|
|
50
102
|
entry.mtimeMs = st.mtimeMs;
|
|
51
103
|
entry.size = st.size;
|
|
52
|
-
try {
|
|
104
|
+
try { this.writeAtomic(p, JSON.stringify(entry)); } catch { /* non-fatal */ }
|
|
53
105
|
return entry;
|
|
54
106
|
} catch { return null; }
|
|
55
107
|
}
|
|
@@ -59,6 +111,7 @@ export class ExtractionCache {
|
|
|
59
111
|
if (!existsSync(p)) return null;
|
|
60
112
|
try {
|
|
61
113
|
const entry: CacheEntry = JSON.parse(readFileSync(p, 'utf-8'));
|
|
114
|
+
if (entry.cacheVersion !== EXTRACTION_CACHE_VERSION) return null;
|
|
62
115
|
return entry.fileHash === fileHash ? entry : null;
|
|
63
116
|
} catch { return null; }
|
|
64
117
|
}
|
|
@@ -67,21 +120,21 @@ export class ExtractionCache {
|
|
|
67
120
|
let mtimeMs: number | undefined;
|
|
68
121
|
let size: number | undefined;
|
|
69
122
|
try { const st = statSync(filePath); mtimeMs = st.mtimeMs; size = st.size; } catch { /* ignore */ }
|
|
70
|
-
const entry: CacheEntry = { fileHash, mtimeMs, size, nodes, edges };
|
|
71
|
-
|
|
123
|
+
const entry: CacheEntry = { fileHash, mtimeMs, size, nodes, edges, cacheVersion: EXTRACTION_CACHE_VERSION };
|
|
124
|
+
this.writeAtomic(this.entryPath(filePath), JSON.stringify(entry));
|
|
72
125
|
}
|
|
73
126
|
|
|
74
127
|
setDeferred(filePath: string, fileHash: string, nodes: MonographNode[], edges: MonographEdge[]): void {
|
|
75
128
|
let mtimeMs: number | undefined;
|
|
76
129
|
let size: number | undefined;
|
|
77
130
|
try { const st = statSync(filePath); mtimeMs = st.mtimeMs; size = st.size; } catch { /* ignore */ }
|
|
78
|
-
const entry: CacheEntry = { fileHash, mtimeMs, size, nodes, edges };
|
|
131
|
+
const entry: CacheEntry = { fileHash, mtimeMs, size, nodes, edges, cacheVersion: EXTRACTION_CACHE_VERSION };
|
|
79
132
|
this.pending.push({ path: this.entryPath(filePath), data: JSON.stringify(entry) });
|
|
80
133
|
}
|
|
81
134
|
|
|
82
135
|
flush(): void {
|
|
83
136
|
for (const { path, data } of this.pending) {
|
|
84
|
-
try {
|
|
137
|
+
try { this.writeAtomic(path, data); } catch { /* non-fatal */ }
|
|
85
138
|
}
|
|
86
139
|
this.pending = [];
|
|
87
140
|
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -51,12 +51,9 @@ function checkDbExists(dbPath: string): DoctorCheck {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function checkDbReadable(dbPath: string): DoctorCheck {
|
|
54
|
+
async function checkDbReadable(dbPath: string): Promise<DoctorCheck> {
|
|
55
55
|
try {
|
|
56
|
-
|
|
57
|
-
// crash the entire doctor invocation.
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
59
|
-
const Database = require('better-sqlite3') as typeof import('better-sqlite3');
|
|
56
|
+
const Database = (await import('better-sqlite3')).default;
|
|
60
57
|
const db = new Database(dbPath, { readonly: true });
|
|
61
58
|
db.prepare('SELECT 1').get();
|
|
62
59
|
db.close();
|
|
@@ -67,10 +64,9 @@ function checkDbReadable(dbPath: string): DoctorCheck {
|
|
|
67
64
|
}
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
function checkNodeCount(dbPath: string): DoctorCheck {
|
|
67
|
+
async function checkNodeCount(dbPath: string): Promise<DoctorCheck> {
|
|
71
68
|
try {
|
|
72
|
-
|
|
73
|
-
const Database = require('better-sqlite3') as typeof import('better-sqlite3');
|
|
69
|
+
const Database = (await import('better-sqlite3')).default;
|
|
74
70
|
const db = new Database(dbPath, { readonly: true });
|
|
75
71
|
const row = db.prepare('SELECT COUNT(*) AS n FROM nodes').get() as { n: number } | undefined;
|
|
76
72
|
db.close();
|
|
@@ -159,8 +155,8 @@ export async function runDoctor(repoPath: string): Promise<DoctorResult> {
|
|
|
159
155
|
|
|
160
156
|
// 3. DB readable (only if DB exists)
|
|
161
157
|
if (dbExistsCheck.status !== 'error') {
|
|
162
|
-
checks.push(checkDbReadable(dbPath));
|
|
163
|
-
checks.push(checkNodeCount(dbPath));
|
|
158
|
+
checks.push(await checkDbReadable(dbPath));
|
|
159
|
+
checks.push(await checkNodeCount(dbPath));
|
|
164
160
|
} else {
|
|
165
161
|
checks.push({
|
|
166
162
|
name: 'SQLite DB readable',
|
|
@@ -82,6 +82,6 @@ export function buildDuplicationResultsEnvelope(
|
|
|
82
82
|
|
|
83
83
|
export function stripRootPrefix(obj: unknown, rootPrefix: string): unknown {
|
|
84
84
|
const json = JSON.stringify(obj);
|
|
85
|
-
const escaped = rootPrefix.replace(/[
|
|
85
|
+
const escaped = rootPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
86
|
return JSON.parse(json.replace(new RegExp(escaped, 'g'), ''));
|
|
87
87
|
}
|
package/src/export/sarif.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { relative, dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PKG_VERSION = (() => {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')).version ?? '0.0.0';
|
|
11
|
+
} catch { return '0.0.0'; }
|
|
12
|
+
})();
|
|
4
13
|
|
|
5
14
|
export interface SarifRule {
|
|
6
15
|
id: string;
|
|
@@ -231,6 +240,6 @@ export function exportHealthSarif(findings: SarifHealthFinding[], root?: string)
|
|
|
231
240
|
return {
|
|
232
241
|
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
233
242
|
version: '2.1.0',
|
|
234
|
-
runs: [{ tool: { driver: { name: 'monograph-health', version:
|
|
243
|
+
runs: [{ tool: { driver: { name: 'monograph-health', version: PKG_VERSION, rules } }, results }],
|
|
235
244
|
};
|
|
236
245
|
}
|
package/src/graph/baseline.ts
CHANGED
|
@@ -126,15 +126,19 @@ export function extractFindingsFromDb(
|
|
|
126
126
|
|
|
127
127
|
// God nodes (degree > 50)
|
|
128
128
|
const gods = db.prepare(`
|
|
129
|
-
SELECT n.id, n.name, n.file_path, n.start_line,
|
|
130
|
-
COUNT(DISTINCT e1.id) + COUNT(DISTINCT e2.id) as degree
|
|
129
|
+
SELECT n.id, n.name, n.file_path, n.start_line, d.deg as degree
|
|
131
130
|
FROM nodes n
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
JOIN (
|
|
132
|
+
SELECT node_id, SUM(cnt) AS deg FROM (
|
|
133
|
+
SELECT source_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY source_id
|
|
134
|
+
UNION ALL
|
|
135
|
+
SELECT target_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY target_id
|
|
136
|
+
) GROUP BY node_id
|
|
137
|
+
HAVING deg > 50
|
|
138
|
+
ORDER BY deg DESC
|
|
139
|
+
LIMIT 100
|
|
140
|
+
) d ON d.node_id = n.id
|
|
141
|
+
ORDER BY d.deg DESC
|
|
138
142
|
`).all() as { id: string; name: string; file_path: string | null; start_line: number | null; degree: number }[];
|
|
139
143
|
|
|
140
144
|
for (const n of gods) {
|
|
@@ -173,20 +177,25 @@ export function extractFindingsFromDb(
|
|
|
173
177
|
});
|
|
174
178
|
}
|
|
175
179
|
|
|
176
|
-
// Bridge nodes: nodes whose edges span
|
|
177
|
-
//
|
|
180
|
+
// Bridge nodes: nodes whose edges span 2+ distinct foreign communities.
|
|
181
|
+
// Pre-aggregate cross-community counts per direction to avoid 4-way cross product.
|
|
178
182
|
const bridges = db.prepare(`
|
|
179
|
-
SELECT n.id, n.name, n.file_path, n.start_line
|
|
180
|
-
COUNT(DISTINCT CASE WHEN e_out.source_id = n.id AND n_tgt.community_id != n.community_id THEN n_tgt.community_id END) +
|
|
181
|
-
COUNT(DISTINCT CASE WHEN e_in.target_id = n.id AND n_src.community_id != n.community_id THEN n_src.community_id END) AS cross_communities
|
|
183
|
+
SELECT n.id, n.name, n.file_path, n.start_line
|
|
182
184
|
FROM nodes n
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
JOIN (
|
|
186
|
+
SELECT node_id, COUNT(DISTINCT foreign_community) AS cross_cnt FROM (
|
|
187
|
+
SELECT e.source_id AS node_id, n2.community_id AS foreign_community
|
|
188
|
+
FROM edges e
|
|
189
|
+
JOIN nodes n1 ON n1.id = e.source_id AND n1.community_id IS NOT NULL
|
|
190
|
+
JOIN nodes n2 ON n2.id = e.target_id AND n2.community_id IS NOT NULL AND n2.community_id != n1.community_id
|
|
191
|
+
UNION ALL
|
|
192
|
+
SELECT e.target_id AS node_id, n1.community_id AS foreign_community
|
|
193
|
+
FROM edges e
|
|
194
|
+
JOIN nodes n1 ON n1.id = e.source_id AND n1.community_id IS NOT NULL
|
|
195
|
+
JOIN nodes n2 ON n2.id = e.target_id AND n2.community_id IS NOT NULL AND n1.community_id != n2.community_id
|
|
196
|
+
) GROUP BY node_id
|
|
197
|
+
HAVING cross_cnt >= 2
|
|
198
|
+
) b ON b.node_id = n.id
|
|
190
199
|
LIMIT 100
|
|
191
200
|
`).all() as { id: string; name: string; file_path: string | null; start_line: number | null }[];
|
|
192
201
|
|
package/src/graph/dead-code.ts
CHANGED
|
@@ -11,8 +11,13 @@ import type { MonographDb } from '../storage/db.js';
|
|
|
11
11
|
* - Only exported — private locals are internal by design
|
|
12
12
|
* - Skips entry-point, test, dist, and node_modules paths
|
|
13
13
|
* - Requires zero inbound CALLS, IMPORTS, REFERENCES, and RE_EXPORTS edges
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
14
|
+
* - Not re-exported through any index.ts barrel file, via an actual RE_EXPORTS
|
|
15
|
+
* edge from that barrel's File node (not merely sharing a name with something
|
|
16
|
+
* an index.ts happens to also define)
|
|
17
|
+
*
|
|
18
|
+
* Known gap: files consumed only via a dynamic `import()` call won't have a static
|
|
19
|
+
* edge in the graph, so they can still surface as false positives here — this
|
|
20
|
+
* detector only tracks statically-resolved edges.
|
|
16
21
|
*/
|
|
17
22
|
export function detectDeadCode(db: MonographDb): string[] {
|
|
18
23
|
return detectDeadCodeNodes(db).map(n => n.id);
|
|
@@ -58,21 +63,19 @@ export function detectDeadCodeNodes(db: MonographDb): DeadCodeNode[] {
|
|
|
58
63
|
WHERE e.target_id = n.id
|
|
59
64
|
AND e.relation IN ('CALLS', 'IMPORTS', 'REFERENCES', 'RE_EXPORTS')
|
|
60
65
|
)
|
|
61
|
-
--
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
AND o.file_path IS NOT NULL
|
|
66
|
-
AND o.file_path NOT LIKE '%/dist/%'
|
|
67
|
-
AND o.file_path NOT LIKE '%node_modules%'
|
|
68
|
-
)
|
|
69
|
-
-- not re-exported through a barrel (index.ts/index.js)
|
|
66
|
+
-- not re-exported through a barrel (index.ts/index.js) — checked via an
|
|
67
|
+
-- ACTUAL RE_EXPORTS edge from the barrel's File node to this candidate's
|
|
68
|
+
-- File node, not a same-NAME coincidence with an unrelated symbol that
|
|
69
|
+
-- happens to also live in some index.ts elsewhere in the repo.
|
|
70
70
|
AND NOT EXISTS (
|
|
71
|
-
SELECT 1 FROM
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
AND
|
|
71
|
+
SELECT 1 FROM edges e
|
|
72
|
+
JOIN nodes fn ON fn.id = e.target_id AND fn.label = 'File'
|
|
73
|
+
JOIN nodes bn ON bn.id = e.source_id AND bn.label = 'File'
|
|
74
|
+
WHERE e.relation = 'RE_EXPORTS'
|
|
75
|
+
AND fn.file_path = n.file_path
|
|
76
|
+
AND bn.file_path LIKE '%/index.%'
|
|
77
|
+
AND bn.file_path NOT LIKE '%/dist/%'
|
|
78
|
+
AND bn.file_path NOT LIKE '%node_modules%'
|
|
76
79
|
)
|
|
77
80
|
ORDER BY n.file_path, n.start_line`,
|
|
78
81
|
)
|
|
@@ -33,7 +33,7 @@ export function computeHealthScore(db: Database.Database): HealthScoreResult {
|
|
|
33
33
|
|
|
34
34
|
// ── Unreachable file nodes ────────────────────────────────────────────────
|
|
35
35
|
const unreachableRow = db.prepare(
|
|
36
|
-
"SELECT COUNT(*) as c FROM nodes WHERE label = 'File' AND properties
|
|
36
|
+
"SELECT COUNT(*) as c FROM nodes WHERE label = 'File' AND json_extract(properties, '$.unreachable') = 1"
|
|
37
37
|
).get() as { c: number };
|
|
38
38
|
const unreachableFiles = unreachableRow.c;
|
|
39
39
|
|
|
@@ -42,23 +42,20 @@ export function computeHealthScore(db: Database.Database): HealthScoreResult {
|
|
|
42
42
|
const totalNodes = totalNodesRow.c;
|
|
43
43
|
|
|
44
44
|
// ── p95 degree (god nodes) ────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const degrees = [...degMap.values()].sort((a, b) => a - b);
|
|
57
|
-
const p95Index = Math.floor(degrees.length * 0.95);
|
|
58
|
-
const p95Degree = degrees[p95Index] ?? 0;
|
|
45
|
+
// Compute per-node total degree (in + out) entirely in SQL
|
|
46
|
+
const degreeRows = db.prepare(`
|
|
47
|
+
SELECT node_id, SUM(cnt) AS degree FROM (
|
|
48
|
+
SELECT source_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY source_id
|
|
49
|
+
UNION ALL
|
|
50
|
+
SELECT target_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY target_id
|
|
51
|
+
) GROUP BY node_id ORDER BY degree ASC
|
|
52
|
+
`).all() as { node_id: string; degree: number }[];
|
|
53
|
+
|
|
54
|
+
const p95Index = Math.floor(degreeRows.length * 0.95);
|
|
55
|
+
const p95Degree = degreeRows[p95Index]?.degree ?? 0;
|
|
59
56
|
|
|
60
57
|
const godNodeCount = p95Degree > 0
|
|
61
|
-
?
|
|
58
|
+
? degreeRows.filter(r => r.degree > p95Degree).length
|
|
62
59
|
: 0;
|
|
63
60
|
|
|
64
61
|
// ── Circular edges (bidirectional import pairs) ───────────────────────────
|
package/src/graph/hotspots.ts
CHANGED
|
@@ -123,12 +123,16 @@ export function computeHotspots(
|
|
|
123
123
|
// ── Step 3: Get file nodes + centrality from DB ───────────────────────────
|
|
124
124
|
const fileNodes = db.prepare(`
|
|
125
125
|
SELECT n.id, n.name, n.file_path, n.label, n.community_id,
|
|
126
|
-
|
|
126
|
+
COALESCE(d.deg, 0) as degree
|
|
127
127
|
FROM nodes n
|
|
128
|
-
LEFT JOIN
|
|
129
|
-
|
|
128
|
+
LEFT JOIN (
|
|
129
|
+
SELECT node_id, SUM(cnt) AS deg FROM (
|
|
130
|
+
SELECT source_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY source_id
|
|
131
|
+
UNION ALL
|
|
132
|
+
SELECT target_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY target_id
|
|
133
|
+
) GROUP BY node_id
|
|
134
|
+
) d ON d.node_id = n.id
|
|
130
135
|
WHERE n.label = 'File' AND n.file_path IS NOT NULL
|
|
131
|
-
GROUP BY n.id
|
|
132
136
|
`).all() as { id: string; name: string; file_path: string; label: string; community_id: number | null; degree: number }[];
|
|
133
137
|
|
|
134
138
|
// ── Step 4: Join churn + centrality ──────────────────────────────────────
|
package/src/graph/pagerank.ts
CHANGED
|
@@ -24,6 +24,10 @@ function getStmts(db: MonographDb): StmtCache {
|
|
|
24
24
|
const key = (db as unknown as { name: string }).name ?? '__default__';
|
|
25
25
|
let cache = _stmtCache.get(key);
|
|
26
26
|
if (!cache) {
|
|
27
|
+
if (_stmtCache.size >= MAX_STMT_CACHE) {
|
|
28
|
+
const oldest = _stmtCache.keys().next().value!;
|
|
29
|
+
_stmtCache.delete(oldest);
|
|
30
|
+
}
|
|
27
31
|
cache = {
|
|
28
32
|
selectNodes: db.prepare('SELECT id FROM nodes'),
|
|
29
33
|
selectEdges: db.prepare('SELECT source_id, target_id FROM edges'),
|
|
@@ -38,6 +42,8 @@ function getStmts(db: MonographDb): StmtCache {
|
|
|
38
42
|
// changed. Keyed by (dbName, nodeCount, edgeCount) with a 5-second TTL.
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
40
44
|
const PAGERANK_CACHE_TTL_MS = 5_000;
|
|
45
|
+
const MAX_RESULT_CACHE = 50;
|
|
46
|
+
const MAX_STMT_CACHE = 20;
|
|
41
47
|
|
|
42
48
|
interface PageRankCacheEntry {
|
|
43
49
|
result: Map<string, number>;
|
|
@@ -149,7 +155,10 @@ export function pageRank(db: MonographDb, options: PageRankOptions = {}): Map<st
|
|
|
149
155
|
result.set(nodes[i], scores[i]);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
|
|
158
|
+
if (_resultCache.size >= MAX_RESULT_CACHE) {
|
|
159
|
+
const oldest = _resultCache.keys().next().value!;
|
|
160
|
+
_resultCache.delete(oldest);
|
|
161
|
+
}
|
|
153
162
|
_resultCache.set(cacheKey, { result, expiresAt: now + PAGERANK_CACHE_TTL_MS });
|
|
154
163
|
|
|
155
164
|
return result;
|
package/src/graph/statistics.ts
CHANGED
|
@@ -77,21 +77,24 @@ export function clusteringCoefficient(db: MonographDb): number {
|
|
|
77
77
|
let totalCoeff = 0;
|
|
78
78
|
let countWithNeighbors = 0;
|
|
79
79
|
|
|
80
|
+
// Cap per-node neighbor scan to avoid O(k²) explosion on hub nodes
|
|
81
|
+
const MAX_K = 200;
|
|
80
82
|
for (const [node, nbrs] of neighbors) {
|
|
81
83
|
const k = nbrs.size;
|
|
82
84
|
if (k < 2) continue;
|
|
83
85
|
|
|
84
86
|
let triangles = 0;
|
|
85
|
-
const nbList = [...nbrs];
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const nbList = k <= MAX_K ? [...nbrs] : [...nbrs].slice(0, MAX_K);
|
|
88
|
+
const effectiveK = nbList.length;
|
|
89
|
+
for (let i = 0; i < effectiveK; i++) {
|
|
90
|
+
for (let j = i + 1; j < effectiveK; j++) {
|
|
88
91
|
if (neighbors.get(nbList[i])?.has(nbList[j])) {
|
|
89
92
|
triangles++;
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
const possible = (
|
|
97
|
+
const possible = (effectiveK * (effectiveK - 1)) / 2;
|
|
95
98
|
totalCoeff += triangles / possible;
|
|
96
99
|
countWithNeighbors++;
|
|
97
100
|
}
|
|
@@ -112,19 +115,28 @@ export interface PathStats {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/**
|
|
115
|
-
* Compute both average path length and graph diameter
|
|
116
|
-
*
|
|
118
|
+
* Compute both average path length and graph diameter via BFS.
|
|
119
|
+
* For graphs > 500 nodes, uses deterministic sampling (every Nth node)
|
|
120
|
+
* to keep runtime practical — O(sample * E) instead of O(N * E).
|
|
117
121
|
*/
|
|
118
122
|
export function pathStats(db: MonographDb): PathStats {
|
|
119
123
|
const graph = getGraph(db);
|
|
120
124
|
const nodes = graph.nodes();
|
|
121
125
|
if (nodes.length < 2) return { averagePathLength: 0, diameter: 0 };
|
|
122
126
|
|
|
127
|
+
// Sample sources for large graphs — stride ensures even coverage
|
|
128
|
+
const MAX_SOURCES = 500;
|
|
129
|
+
const stride = nodes.length <= MAX_SOURCES ? 1 : Math.ceil(nodes.length / MAX_SOURCES);
|
|
130
|
+
const sources: string[] = [];
|
|
131
|
+
for (let i = 0; i < nodes.length && sources.length < MAX_SOURCES; i += stride) {
|
|
132
|
+
sources.push(nodes[i]);
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
let totalLength = 0;
|
|
124
136
|
let reachablePairs = 0;
|
|
125
137
|
let maxDist = 0;
|
|
126
138
|
|
|
127
|
-
for (const source of
|
|
139
|
+
for (const source of sources) {
|
|
128
140
|
bfsFromNode(graph, source, (node, _attr, depth) => {
|
|
129
141
|
if (node !== source && depth > 0) {
|
|
130
142
|
totalLength += depth;
|
package/src/graph/stats.ts
CHANGED
|
@@ -143,10 +143,11 @@ export function computeGraphStats(db: Database.Database): GraphStatsSummary {
|
|
|
143
143
|
/** Format a GraphStatsSummary as structured text for LLM consumption. */
|
|
144
144
|
export function formatGraphStats(s: GraphStatsSummary): string {
|
|
145
145
|
const cp = s.couplingProfile;
|
|
146
|
+
const totalEntities = cp.fanInProfile.low + cp.fanInProfile.medium + cp.fanInProfile.high + cp.fanInProfile.critical;
|
|
146
147
|
const lines: string[] = [
|
|
147
148
|
`Graph Stats`,
|
|
148
149
|
` Nodes: ${s.nodeCount} Edges: ${s.edgeCount} Communities: ${s.communityCount} Files: ${s.fileCount}`,
|
|
149
|
-
`Coupling Profile (${cp.totalFiles} files)`,
|
|
150
|
+
`Coupling Profile (${totalEntities} entities, ${cp.totalFiles} files)`,
|
|
150
151
|
` Fan-in p50/p75/p90/p95: ${cp.p50FanIn}/${cp.p75FanIn}/${cp.p90FanIn}/${cp.p95FanIn}`,
|
|
151
152
|
` High-coupling (>p95): ${cp.couplingHighPct}%`,
|
|
152
153
|
`Fan-in Risk: low=${cp.fanInProfile.low}(${cp.fanInProfile.lowPct}%) med=${cp.fanInProfile.medium}(${cp.fanInProfile.mediumPct}%) high=${cp.fanInProfile.high}(${cp.fanInProfile.highPct}%) crit=${cp.fanInProfile.critical}(${cp.fanInProfile.criticalPct}%)`,
|