@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monograph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Native TypeScript code intelligence engine for monomind",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"import": "./dist/src/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
},
|
|
14
17
|
"publishConfig": {
|
|
15
18
|
"access": "public",
|
|
16
19
|
"tag": "latest"
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
"@types/express": "^5.0.0",
|
|
51
54
|
"@types/micromatch": "^4.0.10",
|
|
52
55
|
"@types/node": "^20.0.0",
|
|
53
|
-
"typescript": "^
|
|
56
|
+
"typescript": "^7.0.2",
|
|
54
57
|
"vitest": "^4.1.4"
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -34,6 +34,11 @@ function insertCallEdge(db: Database.Database, src: string, tgt: string) {
|
|
|
34
34
|
.run(`${src}_${tgt}_calls`, src, tgt);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function insertReExportEdge(db: Database.Database, srcFileId: string, tgtFileId: string) {
|
|
38
|
+
db.prepare(`INSERT INTO edges (id, source_id, target_id, relation, confidence, confidence_score) VALUES (?, ?, ?, 'RE_EXPORTS', 'INFERRED', 0.8)`)
|
|
39
|
+
.run(`${srcFileId}_${tgtFileId}_reexports`, srcFileId, tgtFileId);
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
describe('detectDeadCode', () => {
|
|
38
43
|
it('returns empty array for empty graph', () => {
|
|
39
44
|
const db = makeTempDb();
|
|
@@ -93,24 +98,39 @@ describe('detectDeadCode', () => {
|
|
|
93
98
|
db.close();
|
|
94
99
|
});
|
|
95
100
|
|
|
96
|
-
it('
|
|
101
|
+
it('does NOT suppress on a same-name coincidence alone (no real edge)', () => {
|
|
102
|
+
// Regression test for P2-18(a): the old suppression matched on NAME EQUALITY
|
|
103
|
+
// anywhere in the codebase, so a genuinely-dead `shared()` in a.ts was hidden
|
|
104
|
+
// just because an unrelated `shared` also existed in b.ts. With no actual
|
|
105
|
+
// RE_EXPORTS/IMPORTS edge between them, the candidate must still be flagged.
|
|
97
106
|
const db = makeTempDb();
|
|
98
107
|
insertFileNode(db, 'src/a.ts');
|
|
99
108
|
insertFileNode(db, 'src/b.ts');
|
|
100
|
-
insertFunction(db, 'shared', 'src/a.ts', { exported: true });
|
|
101
|
-
// Same name in another file (import binding)
|
|
109
|
+
const id = insertFunction(db, 'shared', 'src/a.ts', { exported: true });
|
|
102
110
|
insertFunction(db, 'shared', 'src/b.ts', { exported: false });
|
|
103
|
-
expect(detectDeadCode(db)).
|
|
111
|
+
expect(detectDeadCode(db)).toContain(id);
|
|
104
112
|
db.close();
|
|
105
113
|
});
|
|
106
114
|
|
|
107
|
-
it('
|
|
115
|
+
it('does NOT suppress on same-name-in-index.ts alone (no real RE_EXPORTS edge)', () => {
|
|
116
|
+
// Regression test for P2-18(a): a function sharing a name with something
|
|
117
|
+
// index.ts happens to also define is not the same as being re-exported by it.
|
|
108
118
|
const db = makeTempDb();
|
|
109
119
|
insertFileNode(db, 'src/utils.ts');
|
|
110
120
|
insertFileNode(db, 'src/index.ts');
|
|
111
|
-
insertFunction(db, 'helper', 'src/utils.ts', { exported: true });
|
|
112
|
-
// Same name in index.ts (barrel re-export)
|
|
121
|
+
const id = insertFunction(db, 'helper', 'src/utils.ts', { exported: true });
|
|
113
122
|
insertFunction(db, 'helper', 'src/index.ts', { exported: true });
|
|
123
|
+
expect(detectDeadCode(db)).toContain(id);
|
|
124
|
+
db.close();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('skips functions re-exported through a REAL RE_EXPORTS edge from a barrel', () => {
|
|
128
|
+
const db = makeTempDb();
|
|
129
|
+
const utilsFileId = insertFileNode(db, 'src/utils.ts');
|
|
130
|
+
const indexFileId = insertFileNode(db, 'src/index.ts');
|
|
131
|
+
insertFunction(db, 'helper', 'src/utils.ts', { exported: true });
|
|
132
|
+
// Actual barrel edge: src/index.ts RE_EXPORTS src/utils.ts's File node.
|
|
133
|
+
insertReExportEdge(db, indexFileId, utilsFileId);
|
|
114
134
|
expect(detectDeadCode(db)).toEqual([]);
|
|
115
135
|
db.close();
|
|
116
136
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { openDb, closeDb } from '../../../../storage/db.js';
|
|
7
|
+
import { insertNodes } from '../../../../storage/node-store.js';
|
|
8
|
+
import type { MonographNode } from '../../../../types.js';
|
|
9
|
+
import type { PipelineContext } from '../../../../pipeline/types.js';
|
|
10
|
+
import { DEFAULT_OPTIONS } from '../../../../pipeline/types.js';
|
|
11
|
+
import { electronIpcAdapter } from '../../../../pipeline/phases/bridge-adapters/electron-ipc.js';
|
|
12
|
+
|
|
13
|
+
function makeCtx(repoPath: string, db: Database.Database): PipelineContext {
|
|
14
|
+
return {
|
|
15
|
+
repoPath,
|
|
16
|
+
db,
|
|
17
|
+
graph: undefined as never,
|
|
18
|
+
onProgress: () => {},
|
|
19
|
+
options: DEFAULT_OPTIONS,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeFile(root: string, relPath: string, content: string): void {
|
|
24
|
+
const full = join(root, relPath);
|
|
25
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
26
|
+
writeFileSync(full, content, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileNode(id: string, filePath: string): MonographNode {
|
|
30
|
+
return {
|
|
31
|
+
id, label: 'File', name: filePath.split('/').pop()!, normLabel: '', filePath,
|
|
32
|
+
isExported: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('electronIpcAdapter', () => {
|
|
37
|
+
let tmpDir: string;
|
|
38
|
+
let db: Database.Database;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'electron-ipc-bridge-test-'));
|
|
42
|
+
db = openDb(join(tmpDir, 'test.db'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
closeDb(db);
|
|
47
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('detects a repo with both ipcMain and ipcRenderer usage', () => {
|
|
51
|
+
writeFile(tmpDir, 'main.js', `ipcMain.handle('get-data', () => {});`);
|
|
52
|
+
writeFile(tmpDir, 'renderer.js', `ipcRenderer.invoke('get-data');`);
|
|
53
|
+
const ctx = makeCtx(tmpDir, db);
|
|
54
|
+
expect(electronIpcAdapter.detect(ctx, ['main.js', 'renderer.js'])).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not detect main-only usage (no renderer side)', () => {
|
|
58
|
+
writeFile(tmpDir, 'main.js', `ipcMain.handle('get-data', () => {});`);
|
|
59
|
+
const ctx = makeCtx(tmpDir, db);
|
|
60
|
+
expect(electronIpcAdapter.detect(ctx, ['main.js'])).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('links a channel from ipcMain.handle to ipcRenderer.invoke by name', () => {
|
|
64
|
+
writeFile(tmpDir, 'main.js', `ipcMain.handle('get-data', () => fetchData());`);
|
|
65
|
+
writeFile(tmpDir, 'renderer.js', `const data = await ipcRenderer.invoke('get-data');`);
|
|
66
|
+
insertNodes(db, [fileNode('main_file', 'main.js'), fileNode('renderer_file', 'renderer.js')]);
|
|
67
|
+
const ctx = makeCtx(tmpDir, db);
|
|
68
|
+
|
|
69
|
+
const defs = electronIpcAdapter.findDefinitions(ctx, ['main.js', 'renderer.js']);
|
|
70
|
+
expect(defs).toEqual([{ key: 'get-data', nodeId: 'main_file', language: 'javascript' }]);
|
|
71
|
+
|
|
72
|
+
const sites = electronIpcAdapter.findCallSites(ctx, ['main.js', 'renderer.js']);
|
|
73
|
+
expect(sites).toEqual([{ key: 'get-data', nodeId: 'renderer_file', language: 'javascript' }]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('also matches ipcMain.on / ipcRenderer.send', () => {
|
|
77
|
+
writeFile(tmpDir, 'main.js', `ipcMain.on('log', (e, msg) => console.log(msg));`);
|
|
78
|
+
writeFile(tmpDir, 'renderer.js', `ipcRenderer.send('log', 'hello');`);
|
|
79
|
+
insertNodes(db, [fileNode('main_file', 'main.js'), fileNode('renderer_file', 'renderer.js')]);
|
|
80
|
+
const ctx = makeCtx(tmpDir, db);
|
|
81
|
+
|
|
82
|
+
expect(electronIpcAdapter.findDefinitions(ctx, ['main.js']))
|
|
83
|
+
.toEqual([{ key: 'log', nodeId: 'main_file', language: 'javascript' }]);
|
|
84
|
+
expect(electronIpcAdapter.findCallSites(ctx, ['renderer.js']))
|
|
85
|
+
.toEqual([{ key: 'log', nodeId: 'renderer_file', language: 'javascript' }]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { openDb, closeDb } from '../../../../storage/db.js';
|
|
7
|
+
import { insertNodes } from '../../../../storage/node-store.js';
|
|
8
|
+
import type { MonographNode } from '../../../../types.js';
|
|
9
|
+
import type { PipelineContext } from '../../../../pipeline/types.js';
|
|
10
|
+
import { DEFAULT_OPTIONS } from '../../../../pipeline/types.js';
|
|
11
|
+
import { tauriAdapter } from '../../../../pipeline/phases/bridge-adapters/tauri.js';
|
|
12
|
+
|
|
13
|
+
function makeCtx(repoPath: string, db: Database.Database): PipelineContext {
|
|
14
|
+
return {
|
|
15
|
+
repoPath,
|
|
16
|
+
db,
|
|
17
|
+
graph: undefined as never,
|
|
18
|
+
onProgress: () => {},
|
|
19
|
+
options: DEFAULT_OPTIONS,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeFile(root: string, relPath: string, content: string): void {
|
|
24
|
+
const full = join(root, relPath);
|
|
25
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
26
|
+
writeFileSync(full, content, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fnNode(id: string, name: string, filePath: string): MonographNode {
|
|
30
|
+
return {
|
|
31
|
+
id, label: 'Function', name, normLabel: name.toLowerCase(), filePath,
|
|
32
|
+
isExported: true, language: 'rust',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fileNode(id: string, filePath: string): MonographNode {
|
|
37
|
+
return {
|
|
38
|
+
id, label: 'File', name: filePath.split('/').pop()!, normLabel: '', filePath,
|
|
39
|
+
isExported: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('tauriAdapter', () => {
|
|
44
|
+
let tmpDir: string;
|
|
45
|
+
let db: Database.Database;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'tauri-bridge-test-'));
|
|
49
|
+
db = openDb(join(tmpDir, 'test.db'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
closeDb(db);
|
|
54
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('detects a repo with a #[tauri::command]-annotated Rust file', () => {
|
|
58
|
+
writeFile(tmpDir, 'src-tauri/src/main.rs', `
|
|
59
|
+
#[tauri::command]
|
|
60
|
+
fn greet(name: String) -> String {
|
|
61
|
+
format!("Hello, {}!", name)
|
|
62
|
+
}
|
|
63
|
+
`);
|
|
64
|
+
const ctx = makeCtx(tmpDir, db);
|
|
65
|
+
expect(tauriAdapter.detect(ctx, ['src-tauri/src/main.rs'])).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not detect a repo with no Rust files', () => {
|
|
69
|
+
const ctx = makeCtx(tmpDir, db);
|
|
70
|
+
expect(tauriAdapter.detect(ctx, ['src/App.tsx'])).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not detect a Rust file with no #[tauri::command] annotation', () => {
|
|
74
|
+
writeFile(tmpDir, 'src-tauri/src/main.rs', `
|
|
75
|
+
fn internal_helper() -> i32 { 42 }
|
|
76
|
+
`);
|
|
77
|
+
const ctx = makeCtx(tmpDir, db);
|
|
78
|
+
expect(tauriAdapter.detect(ctx, ['src-tauri/src/main.rs'])).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('finds a #[tauri::command] fn matched to its existing Function node', () => {
|
|
82
|
+
writeFile(tmpDir, 'src-tauri/src/main.rs', `
|
|
83
|
+
#[tauri::command]
|
|
84
|
+
fn greet(name: String) -> String {
|
|
85
|
+
format!("Hello, {}!", name)
|
|
86
|
+
}
|
|
87
|
+
`);
|
|
88
|
+
insertNodes(db, [fnNode('rust_fn_1', 'greet', 'src-tauri/src/main.rs')]);
|
|
89
|
+
const ctx = makeCtx(tmpDir, db);
|
|
90
|
+
const defs = tauriAdapter.findDefinitions(ctx, ['src-tauri/src/main.rs']);
|
|
91
|
+
expect(defs).toEqual([{ key: 'greet', nodeId: 'rust_fn_1', language: 'rust' }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('ignores a plain fn with no #[tauri::command] attribute', () => {
|
|
95
|
+
writeFile(tmpDir, 'src-tauri/src/main.rs', `
|
|
96
|
+
fn internal_helper() -> i32 { 42 }
|
|
97
|
+
`);
|
|
98
|
+
insertNodes(db, [fnNode('rust_fn_2', 'internal_helper', 'src-tauri/src/main.rs')]);
|
|
99
|
+
const ctx = makeCtx(tmpDir, db);
|
|
100
|
+
const defs = tauriAdapter.findDefinitions(ctx, ['src-tauri/src/main.rs']);
|
|
101
|
+
expect(defs).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('finds an invoke() call site attached to its containing File node', () => {
|
|
105
|
+
writeFile(tmpDir, 'src/App.tsx', `
|
|
106
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
107
|
+
async function onClick() {
|
|
108
|
+
await invoke('greet', { name: 'world' });
|
|
109
|
+
}
|
|
110
|
+
`);
|
|
111
|
+
insertNodes(db, [fileNode('file_1', 'src/App.tsx')]);
|
|
112
|
+
const ctx = makeCtx(tmpDir, db);
|
|
113
|
+
const sites = tauriAdapter.findCallSites(ctx, ['src/App.tsx']);
|
|
114
|
+
expect(sites).toEqual([{ key: 'greet', nodeId: 'file_1', language: 'javascript' }]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { openDb, closeDb } from '../../../../storage/db.js';
|
|
7
|
+
import { insertNodes } from '../../../../storage/node-store.js';
|
|
8
|
+
import type { MonographNode } from '../../../../types.js';
|
|
9
|
+
import type { PipelineContext } from '../../../../pipeline/types.js';
|
|
10
|
+
import { DEFAULT_OPTIONS } from '../../../../pipeline/types.js';
|
|
11
|
+
import { wailsAdapter } from '../../../../pipeline/phases/bridge-adapters/wails.js';
|
|
12
|
+
|
|
13
|
+
function makeCtx(repoPath: string, db: Database.Database): PipelineContext {
|
|
14
|
+
return {
|
|
15
|
+
repoPath,
|
|
16
|
+
db,
|
|
17
|
+
graph: undefined as never,
|
|
18
|
+
onProgress: () => {},
|
|
19
|
+
options: DEFAULT_OPTIONS,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function methodNode(id: string, name: string, filePath: string): MonographNode {
|
|
24
|
+
return {
|
|
25
|
+
id, label: 'Method', name, normLabel: name.toLowerCase(), filePath,
|
|
26
|
+
isExported: true, language: 'go',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function functionNode(id: string, name: string, filePath: string, language = 'typescript'): MonographNode {
|
|
31
|
+
return {
|
|
32
|
+
id, label: 'Function', name, normLabel: name.toLowerCase(), filePath,
|
|
33
|
+
isExported: true, language,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('wailsAdapter', () => {
|
|
38
|
+
let tmpDir: string;
|
|
39
|
+
let db: Database.Database;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'wails-bridge-test-'));
|
|
43
|
+
db = openDb(join(tmpDir, 'test.db'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
closeDb(db);
|
|
48
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('detects a repo with a wailsjs/go binding directory', () => {
|
|
52
|
+
const ctx = makeCtx(tmpDir, db);
|
|
53
|
+
expect(wailsAdapter.detect(ctx, ['frontend/src/wailsjs/go/main/App.js'])).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not detect a repo with no wails bindings', () => {
|
|
57
|
+
const ctx = makeCtx(tmpDir, db);
|
|
58
|
+
expect(wailsAdapter.detect(ctx, ['app.go', 'frontend/src/App.jsx'])).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('finds Go methods as definitions', () => {
|
|
62
|
+
insertNodes(db, [methodNode('go_method_1', 'SendMessage', 'app.go')]);
|
|
63
|
+
const ctx = makeCtx(tmpDir, db);
|
|
64
|
+
const defs = wailsAdapter.findDefinitions(ctx, []);
|
|
65
|
+
expect(defs).toEqual([{ key: 'SendMessage', nodeId: 'go_method_1', language: 'go' }]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('finds wailsjs binding functions as call sites', () => {
|
|
69
|
+
insertNodes(db, [functionNode('js_fn_1', 'SendMessage', 'frontend/src/wailsjs/go/main/App.js')]);
|
|
70
|
+
const ctx = makeCtx(tmpDir, db);
|
|
71
|
+
const sites = wailsAdapter.findCallSites(ctx, []);
|
|
72
|
+
expect(sites).toEqual([{ key: 'SendMessage', nodeId: 'js_fn_1', language: 'typescript' }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('does not treat a Function outside wailsjs/go/ as a call site', () => {
|
|
76
|
+
insertNodes(db, [functionNode('js_fn_2', 'SendMessage', 'frontend/src/services/api.js')]);
|
|
77
|
+
const ctx = makeCtx(tmpDir, db);
|
|
78
|
+
const sites = wailsAdapter.findCallSites(ctx, []);
|
|
79
|
+
expect(sites).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { buildAsync } from '../../../pipeline/orchestrator.js';
|
|
7
|
+
|
|
8
|
+
function writeFile(root: string, relPath: string, content: string): void {
|
|
9
|
+
const full = join(root, relPath);
|
|
10
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
11
|
+
writeFileSync(full, content, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function edgeRows(dbPath: string): { source_id: string; target_id: string; relation: string; confidence: string; reason: string | null }[] {
|
|
15
|
+
const db = new Database(dbPath, { readonly: true });
|
|
16
|
+
try {
|
|
17
|
+
return db.prepare(`SELECT source_id, target_id, relation, confidence, reason FROM edges`).all() as never[];
|
|
18
|
+
} finally {
|
|
19
|
+
db.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Bridge edges are tagged with a reason of the form "<adapter> bridge (<lang> -> <lang>): ...". */
|
|
24
|
+
function isBridgeEdge(e: { reason: string | null }): boolean {
|
|
25
|
+
return !!e.reason && / bridge \(.+ -> .+\): /.test(e.reason);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('bridge-resolver (full pipeline)', () => {
|
|
29
|
+
let tmpDir: string;
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('links a Wails Go method to its generated JS binding function', async () => {
|
|
36
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'bridge-e2e-wails-'));
|
|
37
|
+
|
|
38
|
+
writeFile(tmpDir, 'app.go', `
|
|
39
|
+
package main
|
|
40
|
+
|
|
41
|
+
type App struct{}
|
|
42
|
+
|
|
43
|
+
func (a *App) SendMessage(text string) error {
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
`);
|
|
47
|
+
writeFile(tmpDir, 'frontend/src/wailsjs/go/main/App.js', `
|
|
48
|
+
export function SendMessage(arg1) {
|
|
49
|
+
return window['go']['main']['App']['SendMessage'](arg1);
|
|
50
|
+
}
|
|
51
|
+
`);
|
|
52
|
+
writeFile(tmpDir, 'frontend/src/App.jsx', `
|
|
53
|
+
import { SendMessage } from './wailsjs/go/main/App';
|
|
54
|
+
function onClick() { SendMessage('hi'); }
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
await buildAsync(tmpDir, { codeOnly: true });
|
|
58
|
+
|
|
59
|
+
const edges = edgeRows(join(tmpDir, '.monomind', 'monograph.db'));
|
|
60
|
+
const bridgeEdge = edges.find((e) => e.relation === 'CALLS' && e.confidence === 'INFERRED' && isBridgeEdge(e));
|
|
61
|
+
expect(bridgeEdge).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('does not emit a bridge edge when there is no matching Go method', async () => {
|
|
65
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'bridge-e2e-wails-negative-'));
|
|
66
|
+
|
|
67
|
+
writeFile(tmpDir, 'app.go', `
|
|
68
|
+
package main
|
|
69
|
+
|
|
70
|
+
type App struct{}
|
|
71
|
+
|
|
72
|
+
func (a *App) UnrelatedMethod() error {
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
`);
|
|
76
|
+
writeFile(tmpDir, 'frontend/src/wailsjs/go/main/App.js', `
|
|
77
|
+
export function SendMessage(arg1) {
|
|
78
|
+
return window['go']['main']['App']['SendMessage'](arg1);
|
|
79
|
+
}
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
await buildAsync(tmpDir, { codeOnly: true });
|
|
83
|
+
|
|
84
|
+
const db = new Database(join(tmpDir, '.monomind', 'monograph.db'), { readonly: true });
|
|
85
|
+
const jsFn = db.prepare(`SELECT id FROM nodes WHERE name = 'SendMessage' AND label = 'Function'`).get() as { id: string } | undefined;
|
|
86
|
+
db.close();
|
|
87
|
+
expect(jsFn).toBeDefined();
|
|
88
|
+
|
|
89
|
+
const edges = edgeRows(join(tmpDir, '.monomind', 'monograph.db'));
|
|
90
|
+
const bridgeEdgeToJsFn = edges.find((e) => e.target_id === jsFn!.id && isBridgeEdge(e));
|
|
91
|
+
expect(bridgeEdgeToJsFn).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does nothing in a repo with no bridge frameworks present', async () => {
|
|
95
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'bridge-e2e-none-'));
|
|
96
|
+
writeFile(tmpDir, 'main.go', `
|
|
97
|
+
package main
|
|
98
|
+
|
|
99
|
+
func main() {
|
|
100
|
+
println("hello")
|
|
101
|
+
}
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
await buildAsync(tmpDir, { codeOnly: true });
|
|
105
|
+
|
|
106
|
+
const edges = edgeRows(join(tmpDir, '.monomind', 'monograph.db'));
|
|
107
|
+
const bridgeEdges = edges.filter(isBridgeEdge);
|
|
108
|
+
expect(bridgeEdges).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
|
|
4
4
|
export class ChangedFilesError extends Error {
|
|
@@ -8,9 +8,18 @@ export class ChangedFilesError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Leading '-' is rejected explicitly (not just by the character class) so a
|
|
12
|
+
// value like "--output=/tmp/pwned" can't be passed through and interpreted
|
|
13
|
+
// by git as a command-line option instead of a ref (git option injection).
|
|
11
14
|
const VALID_REF_RE = /^[a-zA-Z0-9\-_./@~^]+$/;
|
|
12
15
|
|
|
13
16
|
export function validateGitRef(ref: string): string {
|
|
17
|
+
if (ref.startsWith('-')) {
|
|
18
|
+
throw new ChangedFilesError(
|
|
19
|
+
`Invalid git ref: "${ref}". Refs must not start with '-' (would be interpreted as a git option)`,
|
|
20
|
+
'invalid_ref',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
14
23
|
if (VALID_REF_RE.test(ref)) {
|
|
15
24
|
return ref;
|
|
16
25
|
}
|
|
@@ -25,7 +34,10 @@ export async function getChangedFiles(root: string, sinceRef: string): Promise<S
|
|
|
25
34
|
|
|
26
35
|
let output: string;
|
|
27
36
|
try {
|
|
28
|
-
|
|
37
|
+
// execFileSync with array argv — no shell involved, so a `root` (cwd)
|
|
38
|
+
// containing '"' or '$(...)' cannot break out and execute arbitrary
|
|
39
|
+
// commands.
|
|
40
|
+
output = execFileSync('git', ['diff', '--name-only', '-z', sinceRef, 'HEAD'], {
|
|
29
41
|
cwd: root,
|
|
30
42
|
encoding: 'utf8',
|
|
31
43
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
3
|
import { join, relative, resolve } from 'path';
|
|
4
4
|
|
|
@@ -48,15 +48,24 @@ export function getChangedWorkspaces(
|
|
|
48
48
|
sinceRef: string,
|
|
49
49
|
workspaceRoots?: string[],
|
|
50
50
|
): WorkspacePackage[] {
|
|
51
|
-
// Validate git ref (same allowlist as changed-files.ts)
|
|
51
|
+
// Validate git ref (same allowlist as changed-files.ts). Leading '-' is
|
|
52
|
+
// rejected explicitly so a value like "--output=/tmp/pwned" can't be
|
|
53
|
+
// interpreted by git as a command-line option (git option injection).
|
|
54
|
+
if (sinceRef.startsWith('-')) {
|
|
55
|
+
throw new Error(`Invalid git ref: "${sinceRef}". Refs must not start with '-' (would be interpreted as a git option)`);
|
|
56
|
+
}
|
|
52
57
|
if (!/^[a-zA-Z0-9\-_./@~^]+$/.test(sinceRef)) {
|
|
53
58
|
throw new Error(`Invalid git ref: "${sinceRef}"`);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
let changedFiles: Set<string>;
|
|
57
62
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
// execFileSync with array argv — no shell involved, so a `projectRoot`
|
|
64
|
+
// containing '"' or '$(...)' cannot break out and execute arbitrary
|
|
65
|
+
// commands.
|
|
66
|
+
const raw = execFileSync(
|
|
67
|
+
'git',
|
|
68
|
+
['-C', projectRoot, 'diff', '--name-only', `${sinceRef}...HEAD`],
|
|
60
69
|
{ encoding: 'utf8' }
|
|
61
70
|
);
|
|
62
71
|
changedFiles = new Set(raw.split('\n').map(l => l.trim()).filter(Boolean));
|
package/src/analysis/churn.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
3
3
|
import { join, relative } from 'path';
|
|
4
4
|
|
|
5
5
|
export type ChurnTrend = 'accelerating' | 'stable' | 'cooling';
|
|
@@ -59,7 +59,15 @@ export function computeRecencyWeight(ageDays: number): number {
|
|
|
59
59
|
return Math.exp(-Math.LN2 * ageDays / 90);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function classifyChurnTrend(
|
|
62
|
+
export function classifyChurnTrend(
|
|
63
|
+
recentWeighted: number,
|
|
64
|
+
olderWeighted: number,
|
|
65
|
+
commitCount = 2,
|
|
66
|
+
): ChurnTrend {
|
|
67
|
+
// A file with 0-1 commits in the window has no "older" phase to compare against —
|
|
68
|
+
// classifying it as 'cooling' (declining activity) is nonsensical for something
|
|
69
|
+
// that was just created or touched once.
|
|
70
|
+
if (commitCount <= 1) return 'stable';
|
|
63
71
|
if (olderWeighted === 0) {
|
|
64
72
|
return recentWeighted > 0 ? 'accelerating' : 'stable';
|
|
65
73
|
}
|
|
@@ -69,6 +77,40 @@ export function classifyChurnTrend(recentWeighted: number, olderWeighted: number
|
|
|
69
77
|
return 'stable';
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
/** Coarse day-granularity bucket for the current moment, e.g. "2026-07-08". */
|
|
81
|
+
function todayBucket(): string {
|
|
82
|
+
return new Date().toISOString().slice(0, 10);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete churn cache files whose date bucket is older than `maxAgeDays` — the
|
|
87
|
+
* weighted-commit numbers decay with time even at a fixed HEAD, and without this
|
|
88
|
+
* a repo idle for weeks would keep serving stale cache, while cache files from
|
|
89
|
+
* every past HEAD/day combination would accumulate forever.
|
|
90
|
+
*/
|
|
91
|
+
function pruneChurnCache(cacheDir: string, maxAgeDays = 3): void {
|
|
92
|
+
let entries: string[];
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(cacheDir);
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const m = entry.match(/^churn-.+-(\d{4}-\d{2}-\d{2})\.json$/);
|
|
101
|
+
if (!m) continue;
|
|
102
|
+
const bucketMs = Date.parse(m[1]);
|
|
103
|
+
if (isNaN(bucketMs)) continue;
|
|
104
|
+
if ((now - bucketMs) / 86400000 > maxAgeDays) {
|
|
105
|
+
try {
|
|
106
|
+
unlinkSync(join(cacheDir, entry));
|
|
107
|
+
} catch {
|
|
108
|
+
// non-fatal — best-effort prune
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
72
114
|
export async function analyzeChurn(
|
|
73
115
|
root: string,
|
|
74
116
|
since: SinceDuration | string,
|
|
@@ -76,12 +118,16 @@ export async function analyzeChurn(
|
|
|
76
118
|
const sinceDuration: SinceDuration =
|
|
77
119
|
typeof since === 'string' ? parseSince(since) : since;
|
|
78
120
|
|
|
79
|
-
// Cache lookup
|
|
121
|
+
// Cache lookup — keyed by HEAD SHA + window size + a coarse day bucket, so
|
|
122
|
+
// entries naturally invalidate as time passes even at the same HEAD (the
|
|
123
|
+
// recency weighting is time-dependent, not just HEAD-dependent).
|
|
80
124
|
const cacheDir = join(root, '.monograph', 'cache');
|
|
125
|
+
const dateBucket = todayBucket();
|
|
126
|
+
pruneChurnCache(cacheDir);
|
|
81
127
|
let treeHash = '';
|
|
82
128
|
try {
|
|
83
129
|
treeHash = execSync('git rev-parse HEAD', { cwd: root }).toString().trim();
|
|
84
|
-
const cacheFile = join(cacheDir, `churn-${treeHash}-${sinceDuration.days}.json`);
|
|
130
|
+
const cacheFile = join(cacheDir, `churn-${treeHash}-${sinceDuration.days}-${dateBucket}.json`);
|
|
85
131
|
if (existsSync(cacheFile)) {
|
|
86
132
|
try {
|
|
87
133
|
const cached = JSON.parse(readFileSync(cacheFile, 'utf8'));
|
|
@@ -154,8 +200,10 @@ export async function analyzeChurn(
|
|
|
154
200
|
const authorWeights = new Map<number, number>();
|
|
155
201
|
let totalWeighted = 0;
|
|
156
202
|
|
|
157
|
-
// Split
|
|
158
|
-
|
|
203
|
+
// Split by TIME (midpoint of the `since` window), not by commit-array index —
|
|
204
|
+
// otherwise 10 commits all from the last week still get split 5-old/5-recent,
|
|
205
|
+
// misrepresenting genuinely-recent-only activity as having an "older" phase.
|
|
206
|
+
const windowMidpoint = now - (sinceDuration.days * 86400000) / 2;
|
|
159
207
|
let recentWeighted = 0;
|
|
160
208
|
let olderWeighted = 0;
|
|
161
209
|
|
|
@@ -176,15 +224,14 @@ export async function analyzeChurn(
|
|
|
176
224
|
authorWeights.set(authorIdx, existing + weight);
|
|
177
225
|
totalWeighted += weight;
|
|
178
226
|
|
|
179
|
-
|
|
180
|
-
if (i < midpoint) {
|
|
227
|
+
if (commitDate >= windowMidpoint) {
|
|
181
228
|
recentWeighted += weight;
|
|
182
229
|
} else {
|
|
183
230
|
olderWeighted += weight;
|
|
184
231
|
}
|
|
185
232
|
}
|
|
186
233
|
|
|
187
|
-
const trend = classifyChurnTrend(recentWeighted, olderWeighted);
|
|
234
|
+
const trend = classifyChurnTrend(recentWeighted, olderWeighted, sortedCommits.length);
|
|
188
235
|
|
|
189
236
|
const authors: AuthorContribution[] = Array.from(authorWeights.entries()).map(
|
|
190
237
|
([authorIdx, weightedCommits]) => ({ authorIdx, weightedCommits }),
|
|
@@ -205,7 +252,7 @@ export async function analyzeChurn(
|
|
|
205
252
|
if (treeHash) {
|
|
206
253
|
try {
|
|
207
254
|
mkdirSync(cacheDir, { recursive: true });
|
|
208
|
-
const cacheFile = join(cacheDir, `churn-${treeHash}-${sinceDuration.days}.json`);
|
|
255
|
+
const cacheFile = join(cacheDir, `churn-${treeHash}-${sinceDuration.days}-${dateBucket}.json`);
|
|
209
256
|
writeFileSync(cacheFile, JSON.stringify(result), 'utf8');
|
|
210
257
|
} catch {
|
|
211
258
|
// cache write failure is non-fatal
|