@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
|
@@ -1,53 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import type { PipelinePhase } from '../types.js';
|
|
2
4
|
import type { MonographEdge } from '../../types.js';
|
|
3
5
|
import { makeId, CONFIDENCE_SCORE } from '../../types.js';
|
|
4
6
|
import { insertEdges } from '../../storage/edge-store.js';
|
|
5
7
|
import type { ParseOutput } from './parse.js';
|
|
6
8
|
import type { StructureOutput } from './structure.js';
|
|
9
|
+
import { resolveModuleSpecifier, buildWorkspacePackageMap } from './scope-resolution.js';
|
|
7
10
|
|
|
8
11
|
export interface CrossFileOutput {
|
|
9
12
|
resolvedEdges: MonographEdge[];
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
// Mirrors parsers/extractor.ts's re-export detection regex — used here to recover
|
|
16
|
+
// the ORIGINAL specifier string behind a RE_EXPORTS edge's mangled placeholder id
|
|
17
|
+
// (makeId('import', specifier) is lossy: '/' and '.' are collapsed to '_', so the
|
|
18
|
+
// placeholder can't be reversed directly — we re-derive it from source instead).
|
|
19
|
+
const RE_EXPORT_RE = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g;
|
|
20
|
+
|
|
12
21
|
// monolean: IMPORTS name-matching removed — it matched import names against a
|
|
13
22
|
// global symbol index (e.g. `import fs` → random Variable named `fs` in wrong file).
|
|
14
23
|
// scope-resolution.ts now handles cross-file CALLS via source-parsed import maps.
|
|
15
24
|
export const crossFilePhase: PipelinePhase<CrossFileOutput> = {
|
|
16
25
|
name: 'cross-file',
|
|
17
26
|
deps: ['parse', 'structure'],
|
|
18
|
-
async execute(
|
|
27
|
+
async execute(ctx, deps) {
|
|
19
28
|
const { allEdges } = deps.get('parse') as ParseOutput;
|
|
20
29
|
const { fileNodes } = deps.get('structure') as StructureOutput;
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return idx === -1 ? s : s.slice(idx + 1);
|
|
25
|
-
}
|
|
31
|
+
const reExportEdges = allEdges.filter((e) => e.relation === 'RE_EXPORTS');
|
|
32
|
+
const resolvedEdges: MonographEdge[] = [];
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
if (reExportEdges.length > 0) {
|
|
35
|
+
// fileId <-> filePath maps for resolving edge endpoints to real file nodes.
|
|
36
|
+
const fileIdToPath = new Map<string, string>();
|
|
37
|
+
const knownFiles = new Set<string>();
|
|
38
|
+
for (const fileNode of fileNodes) {
|
|
39
|
+
if (fileNode.filePath) {
|
|
40
|
+
fileIdToPath.set(fileNode.id, fileNode.filePath);
|
|
41
|
+
knownFiles.add(fileNode.filePath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const filePathToId = new Map<string, string>();
|
|
45
|
+
for (const [id, path] of fileIdToPath) filePathToId.set(path, id);
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
for (const fileNode of fileNodes) {
|
|
34
|
-
const basename = lastSegment(fileNode.filePath ?? '').toLowerCase();
|
|
35
|
-
const dotIdx = basename.lastIndexOf('.');
|
|
36
|
-
const noExt = dotIdx === -1 ? basename : basename.slice(0, dotIdx);
|
|
37
|
-
fileBasenameIndex.set(basename, fileNode.id);
|
|
38
|
-
if (noExt !== basename) fileBasenameIndex.set(noExt, fileNode.id);
|
|
39
|
-
}
|
|
47
|
+
const workspaceMap = buildWorkspacePackageMap(ctx.repoPath);
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
// Group by importing file so each source file is read at most once.
|
|
50
|
+
const edgesBySource = new Map<string, MonographEdge[]>();
|
|
51
|
+
for (const edge of reExportEdges) {
|
|
52
|
+
const arr = edgesBySource.get(edge.sourceId);
|
|
53
|
+
if (arr) arr.push(edge);
|
|
54
|
+
else edgesBySource.set(edge.sourceId, [edge]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [sourceId, edges] of edgesBySource) {
|
|
58
|
+
const filePath = fileIdToPath.get(sourceId);
|
|
59
|
+
if (!filePath) continue;
|
|
60
|
+
|
|
61
|
+
let source: string;
|
|
62
|
+
try {
|
|
63
|
+
source = readFileSync(join(ctx.repoPath, filePath), 'utf-8');
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Recover each `export * / { ... } from '<specifier>'` string, keyed by the
|
|
69
|
+
// same mangled placeholder id the extractor used (makeId('import', specifier))
|
|
70
|
+
// so it can be matched back to the corresponding edge.
|
|
71
|
+
const specifierByPlaceholder = new Map<string, string>();
|
|
72
|
+
RE_EXPORT_RE.lastIndex = 0;
|
|
73
|
+
let m: RegExpExecArray | null;
|
|
74
|
+
while ((m = RE_EXPORT_RE.exec(source)) !== null) {
|
|
75
|
+
const specifier = m[1]!;
|
|
76
|
+
specifierByPlaceholder.set(makeId('import', specifier), specifier);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const edge of edges) {
|
|
80
|
+
const specifier = specifierByPlaceholder.get(edge.targetId);
|
|
81
|
+
if (!specifier) continue;
|
|
82
|
+
|
|
83
|
+
// Resolve relative to THIS importing file's own directory (reusing the
|
|
84
|
+
// same resolver scope-resolution.ts uses for IMPORTS) instead of an
|
|
85
|
+
// ambiguous global basename lookup that collides on repeated basenames
|
|
86
|
+
// like index.ts / utils.ts across the repo.
|
|
87
|
+
const resolvedPath = resolveModuleSpecifier(
|
|
88
|
+
filePath,
|
|
89
|
+
specifier,
|
|
90
|
+
ctx.repoPath,
|
|
91
|
+
knownFiles,
|
|
92
|
+
workspaceMap,
|
|
93
|
+
);
|
|
94
|
+
if (!resolvedPath) continue;
|
|
42
95
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const basename = targetBasename(edge.targetId).toLowerCase();
|
|
46
|
-
const dotIdx = basename.lastIndexOf('.');
|
|
47
|
-
const noExt = dotIdx === -1 ? basename : basename.slice(0, dotIdx);
|
|
48
|
-
const resolvedId = fileBasenameIndex.get(basename) ?? fileBasenameIndex.get(noExt);
|
|
96
|
+
const resolvedId = filePathToId.get(resolvedPath);
|
|
97
|
+
if (!resolvedId || resolvedId === edge.targetId) continue;
|
|
49
98
|
|
|
50
|
-
if (resolvedId && resolvedId !== edge.targetId) {
|
|
51
99
|
resolvedEdges.push({
|
|
52
100
|
...edge,
|
|
53
101
|
id: makeId(edge.sourceId, resolvedId, 'reexports_resolved'),
|
|
@@ -59,8 +107,8 @@ export const crossFilePhase: PipelinePhase<CrossFileOutput> = {
|
|
|
59
107
|
}
|
|
60
108
|
}
|
|
61
109
|
|
|
62
|
-
if (
|
|
63
|
-
insertEdges(
|
|
110
|
+
if (ctx.db && resolvedEdges.length > 0) {
|
|
111
|
+
insertEdges(ctx.db, resolvedEdges);
|
|
64
112
|
}
|
|
65
113
|
|
|
66
114
|
return { resolvedEdges };
|
|
@@ -3,8 +3,8 @@ import { extname, join } from 'path';
|
|
|
3
3
|
import type { PipelinePhase, PipelineContext } from '../types.js';
|
|
4
4
|
import type { MonographNode, MonographEdge } from '../../types.js';
|
|
5
5
|
import { parseFile } from '../../parsers/loader.js';
|
|
6
|
-
import { insertNodes } from '../../storage/node-store.js';
|
|
7
|
-
import { insertEdges } from '../../storage/edge-store.js';
|
|
6
|
+
import { insertNodes, deleteNodesForFile } from '../../storage/node-store.js';
|
|
7
|
+
import { insertEdges, deleteEdgesForFile } from '../../storage/edge-store.js';
|
|
8
8
|
import type { StructureOutput } from './structure.js';
|
|
9
9
|
import { extractVariables, variableToNode } from './variables.js';
|
|
10
10
|
import { ExtractionCache } from '../../cache/extraction-cache.js';
|
|
@@ -24,8 +24,18 @@ export const parsePhase: PipelinePhase<ParseOutput> = {
|
|
|
24
24
|
const { fileNodes } = deps.get('structure') as StructureOutput;
|
|
25
25
|
const symbolNodes: MonographNode[] = [];
|
|
26
26
|
const allEdges: MonographEdge[] = [];
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// freshNodes/freshEdges (the cache-miss-only subset) are NOT accumulated in a
|
|
28
|
+
// parallel array during the loop — that duplicated every fresh node/edge object
|
|
29
|
+
// in memory for the entire pipeline run. Instead we record the [start, end)
|
|
30
|
+
// slice of symbolNodes/allEdges that each cache-miss file contributed, and
|
|
31
|
+
// materialize the fresh subset via a single slice pass at the end, only when
|
|
32
|
+
// it's actually needed (cacheHits > 0). On a cold build (cacheHits === 0) the
|
|
33
|
+
// fresh subset is never materialized at all, since nodesToInsert falls back to
|
|
34
|
+
// symbolNodes/allEdges directly in that case — eliminating the redundant
|
|
35
|
+
// allocation entirely for the memory-heaviest scenario.
|
|
36
|
+
const freshNodeRanges: Array<[number, number]> = [];
|
|
37
|
+
const freshEdgeRanges: Array<[number, number]> = [];
|
|
38
|
+
const staleFilePaths: string[] = [];
|
|
29
39
|
const parseErrors: string[] = [];
|
|
30
40
|
const fileContents = new Map<string, string>();
|
|
31
41
|
let processed = 0;
|
|
@@ -39,8 +49,10 @@ export const parsePhase: PipelinePhase<ParseOutput> = {
|
|
|
39
49
|
const ext = extname(absPath).toLowerCase();
|
|
40
50
|
if (ext === '.md' || ext === '.markdown') { processed++; continue; }
|
|
41
51
|
|
|
42
|
-
// Fast path: mtime+size check avoids reading file content entirely
|
|
43
|
-
|
|
52
|
+
// Fast path: mtime+size check avoids reading file content entirely.
|
|
53
|
+
// Skipped entirely under --force so a force rebuild is a genuine from-scratch
|
|
54
|
+
// parse, not a replay of whatever extraction the cache happens to hold.
|
|
55
|
+
const cached = ctx.options.force ? null : cache.getWithStat(absPath);
|
|
44
56
|
if (cached) {
|
|
45
57
|
symbolNodes.push(...cached.nodes);
|
|
46
58
|
allEdges.push(...cached.edges);
|
|
@@ -54,7 +66,12 @@ export const parsePhase: PipelinePhase<ParseOutput> = {
|
|
|
54
66
|
continue;
|
|
55
67
|
}
|
|
56
68
|
source = readFileSync(absPath, 'utf-8');
|
|
57
|
-
} catch {
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
71
|
+
const detail = code || (err instanceof Error ? err.message : String(err));
|
|
72
|
+
parseErrors.push(`${fileNode.filePath}: unreadable (${detail})`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
58
75
|
fileContents.set(fileNode.filePath ?? absPath, source);
|
|
59
76
|
const result = await parseFile(absPath, source, fileNode.filePath ?? '');
|
|
60
77
|
const fileSymbols: MonographNode[] = [...result.nodes];
|
|
@@ -98,10 +115,16 @@ export const parsePhase: PipelinePhase<ParseOutput> = {
|
|
|
98
115
|
const fileHash = cache.hashContent(source);
|
|
99
116
|
cache.setDeferred(absPath, fileHash, fileSymbols, fileEdges);
|
|
100
117
|
} catch { /* non-fatal */ }
|
|
118
|
+
const nodeStart = symbolNodes.length;
|
|
119
|
+
const edgeStart = allEdges.length;
|
|
101
120
|
symbolNodes.push(...fileSymbols);
|
|
102
121
|
allEdges.push(...fileEdges);
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
freshNodeRanges.push([nodeStart, symbolNodes.length]);
|
|
123
|
+
freshEdgeRanges.push([edgeStart, allEdges.length]);
|
|
124
|
+
// This file was re-parsed (cache miss) — its OLD node/edge set (from the
|
|
125
|
+
// previous build) must be deleted before the fresh rows are inserted below,
|
|
126
|
+
// otherwise a renamed/removed symbol's old row survives forever (ghost rows).
|
|
127
|
+
if (fileNode.filePath) staleFilePaths.push(fileNode.filePath);
|
|
105
128
|
cacheMisses++;
|
|
106
129
|
}
|
|
107
130
|
processed++;
|
|
@@ -114,16 +137,35 @@ export const parsePhase: PipelinePhase<ParseOutput> = {
|
|
|
114
137
|
cache.flush();
|
|
115
138
|
|
|
116
139
|
if (ctx.db) {
|
|
140
|
+
const db = ctx.db;
|
|
117
141
|
// Only insert freshly-parsed nodes — cached nodes are already in the DB
|
|
118
142
|
// from the previous build (node IDs are deterministic from file_path + symbol).
|
|
119
|
-
// On first build (no cache hits), freshNodes === symbolNodes
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
// On first build (no cache hits), freshNodes === symbolNodes, so the fresh
|
|
144
|
+
// subset is never materialized — symbolNodes/allEdges are used directly.
|
|
145
|
+
const freshNodes = cacheHits > 0
|
|
146
|
+
? freshNodeRanges.flatMap(([s, e]) => symbolNodes.slice(s, e))
|
|
147
|
+
: symbolNodes;
|
|
148
|
+
const freshEdges = cacheHits > 0
|
|
149
|
+
? freshEdgeRanges.flatMap(([s, e]) => allEdges.slice(s, e))
|
|
150
|
+
: allEdges;
|
|
151
|
+
const nodesToInsert = freshNodes;
|
|
122
152
|
const knownIds = new Set(symbolNodes.map(n => n.id));
|
|
123
|
-
const edgesToInsert =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
const edgesToInsert = freshEdges.filter(e => knownIds.has(e.targetId));
|
|
154
|
+
|
|
155
|
+
// For every cache-miss file, purge its OLD node/edge set BEFORE inserting the
|
|
156
|
+
// fresh parse results, inside the same transaction — otherwise a renamed or
|
|
157
|
+
// removed symbol's old row would survive forever (INSERT OR REPLACE only
|
|
158
|
+
// overwrites rows whose id still matches; it never deletes rows whose id
|
|
159
|
+
// disappeared because the symbol was renamed).
|
|
160
|
+
const writeAll = db.transaction(() => {
|
|
161
|
+
for (const filePath of staleFilePaths) {
|
|
162
|
+
deleteEdgesForFile(db, filePath);
|
|
163
|
+
deleteNodesForFile(db, filePath);
|
|
164
|
+
}
|
|
165
|
+
insertNodes(db, nodesToInsert);
|
|
166
|
+
insertEdges(db, edgesToInsert);
|
|
167
|
+
});
|
|
168
|
+
writeAll();
|
|
127
169
|
}
|
|
128
170
|
|
|
129
171
|
if (cacheHits > 0 && cacheMisses === 0) {
|
|
@@ -131,8 +173,9 @@ export const parsePhase: PipelinePhase<ParseOutput> = {
|
|
|
131
173
|
}
|
|
132
174
|
|
|
133
175
|
if (cacheHits > 0) {
|
|
176
|
+
const freshNodeCount = freshNodeRanges.reduce((sum, [s, e]) => sum + (e - s), 0);
|
|
134
177
|
ctx.onProgress?.({ phase: 'parse', filesProcessed: processed, totalFiles: fileNodes.length,
|
|
135
|
-
message: `cache: ${cacheHits} hits, ${cacheMisses} misses (${
|
|
178
|
+
message: `cache: ${cacheHits} hits, ${cacheMisses} misses (${freshNodeCount} nodes inserted)` } as any);
|
|
136
179
|
}
|
|
137
180
|
|
|
138
181
|
return { symbolNodes, allEdges, parseErrors, fileContents, cacheStats: { hits: cacheHits, misses: cacheMisses } };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
|
1
|
+
import { readdirSync, statSync, lstatSync, existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
3
|
import micromatch from 'micromatch';
|
|
4
4
|
import type { PipelinePhase, PipelineContext } from '../types.js';
|
|
@@ -50,16 +50,22 @@ export const scanPhase: PipelinePhase<ScanOutput> = {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function walk(dir: string) {
|
|
53
|
-
let
|
|
54
|
-
try {
|
|
53
|
+
let dirents: import('fs').Dirent[];
|
|
54
|
+
try { dirents = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
55
55
|
|
|
56
|
-
for (const
|
|
56
|
+
for (const dirent of dirents) {
|
|
57
|
+
const entry = dirent.name;
|
|
57
58
|
if (ignoreDirs.has(entry)) continue;
|
|
58
59
|
// Skip macOS AppleDouble resource fork files (._*) — common on ExFAT/network volumes
|
|
59
60
|
if (entry.startsWith('._')) continue;
|
|
61
|
+
// Never follow symlinks — a symlink to an ancestor directory causes infinite
|
|
62
|
+
// recursion (stack overflow), and a symlink to a large external tree would
|
|
63
|
+
// get fully indexed as if it were part of the repo. Matches how most
|
|
64
|
+
// code-indexing tools behave by default.
|
|
65
|
+
if (dirent.isSymbolicLink()) continue;
|
|
60
66
|
const fullPath = join(dir, entry);
|
|
61
67
|
let stat: ReturnType<typeof statSync>;
|
|
62
|
-
try { stat =
|
|
68
|
+
try { stat = lstatSync(fullPath); } catch { continue; }
|
|
63
69
|
|
|
64
70
|
if (stat.isDirectory()) {
|
|
65
71
|
// Always traverse directories — negation patterns may rescue files inside ignored dirs
|
|
@@ -286,11 +286,25 @@ const GO_RESOLVE_EXTS = ['.go'];
|
|
|
286
286
|
const JAVA_RESOLVE_EXTS = ['.java'];
|
|
287
287
|
const RUST_RESOLVE_EXTS = ['.rs'];
|
|
288
288
|
|
|
289
|
+
const workspacePackageMapCache = new Map<string, Map<string, string>>();
|
|
290
|
+
|
|
291
|
+
/** Invalidate the cached workspace package map for a repo — call at the start
|
|
292
|
+
* of each build so long-lived processes (watch mode) pick up package.json
|
|
293
|
+
* additions/removals instead of serving a stale map from a prior build. */
|
|
294
|
+
export function clearWorkspacePackageMapCache(repoPath: string): void {
|
|
295
|
+
workspacePackageMapCache.delete(repoPath);
|
|
296
|
+
}
|
|
297
|
+
|
|
289
298
|
/**
|
|
290
299
|
* Build package-name → directory map from workspace package.json files.
|
|
291
300
|
* Scans packages/ for package.json and maps npm name to its relative src path.
|
|
301
|
+
* Cached per repoPath — multiple pipeline phases (cross-file, scope-resolution,
|
|
302
|
+
* the latter's own re-export loop) call this per-file/per-edge within the same
|
|
303
|
+
* build, and the workspace's package.json set doesn't change mid-build.
|
|
292
304
|
*/
|
|
293
|
-
function buildWorkspacePackageMap(repoPath: string): Map<string, string> {
|
|
305
|
+
export function buildWorkspacePackageMap(repoPath: string): Map<string, string> {
|
|
306
|
+
const cached = workspacePackageMapCache.get(repoPath);
|
|
307
|
+
if (cached) return cached;
|
|
294
308
|
const result = new Map<string, string>();
|
|
295
309
|
const packagesDir = join(repoPath, 'packages');
|
|
296
310
|
try {
|
|
@@ -315,10 +329,11 @@ function buildWorkspacePackageMap(repoPath: string): Map<string, string> {
|
|
|
315
329
|
};
|
|
316
330
|
scanDirs(packagesDir, 0);
|
|
317
331
|
} catch { /* no packages dir */ }
|
|
332
|
+
workspacePackageMapCache.set(repoPath, result);
|
|
318
333
|
return result;
|
|
319
334
|
}
|
|
320
335
|
|
|
321
|
-
function resolveModuleSpecifier(
|
|
336
|
+
export function resolveModuleSpecifier(
|
|
322
337
|
importerPath: string,
|
|
323
338
|
specifier: string,
|
|
324
339
|
repoPath: string,
|
|
@@ -346,8 +361,8 @@ function resolveModuleSpecifier(
|
|
|
346
361
|
return null;
|
|
347
362
|
}
|
|
348
363
|
|
|
349
|
-
// Workspace package specifier: @
|
|
350
|
-
// Also handles subpath: @
|
|
364
|
+
// Workspace package specifier: @monoes/hooks → packages/@monomind/hooks/src/index.ts
|
|
365
|
+
// Also handles subpath: @monoes/hooks/src/foo → packages/@monomind/hooks/src/foo.ts
|
|
351
366
|
for (const [pkgName, pkgDir] of workspaceMap) {
|
|
352
367
|
if (specifier === pkgName) {
|
|
353
368
|
// Bare import: resolve to package entry point (src/index.ts)
|
package/src/pipeline/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface PipelineOptions {
|
|
|
20
20
|
ignore: string[];
|
|
21
21
|
/** Max Section nodes to submit to LLM extraction (0 = disabled). Default 0. */
|
|
22
22
|
llmMaxSections: number;
|
|
23
|
+
/** When true, bypass the on-disk extraction cache entirely and re-parse every file from scratch. Default false. */
|
|
24
|
+
force?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const DEFAULT_OPTIONS: PipelineOptions = {
|
|
@@ -29,6 +31,7 @@ export const DEFAULT_OPTIONS: PipelineOptions = {
|
|
|
29
31
|
workerChunkBudgetBytes: 20971520,
|
|
30
32
|
ignore: [],
|
|
31
33
|
llmMaxSections: 0,
|
|
34
|
+
force: false,
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
export interface PipelinePhase<TOutput> {
|
|
@@ -54,16 +54,15 @@ function queryEdgesByRelation(db: MonographDb): EdgeRelationStat[] {
|
|
|
54
54
|
|
|
55
55
|
function queryTopNodesByDegree(db: MonographDb, limit = 10): TopDegreeNode[] {
|
|
56
56
|
const rows = db.prepare(`
|
|
57
|
-
SELECT
|
|
58
|
-
n.id,
|
|
59
|
-
n.name,
|
|
60
|
-
n.label,
|
|
61
|
-
(
|
|
62
|
-
SELECT COUNT(*) FROM edges WHERE source_id = n.id
|
|
63
|
-
) + (
|
|
64
|
-
SELECT COUNT(*) FROM edges WHERE target_id = n.id
|
|
65
|
-
) AS degree
|
|
57
|
+
SELECT n.id, n.name, n.label, COALESCE(d.degree, 0) AS degree
|
|
66
58
|
FROM nodes n
|
|
59
|
+
LEFT JOIN (
|
|
60
|
+
SELECT node_id, SUM(cnt) AS degree FROM (
|
|
61
|
+
SELECT source_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY source_id
|
|
62
|
+
UNION ALL
|
|
63
|
+
SELECT target_id AS node_id, COUNT(*) AS cnt FROM edges GROUP BY target_id
|
|
64
|
+
) GROUP BY node_id
|
|
65
|
+
) d ON d.node_id = n.id
|
|
67
66
|
ORDER BY degree DESC
|
|
68
67
|
LIMIT ?
|
|
69
68
|
`).all(limit) as TopDegreeNode[];
|
|
@@ -28,10 +28,13 @@ export function checkStaleness(db: MonographDb, repoPath: string): StalenessRepo
|
|
|
28
28
|
const indexedCommitShort = indexedCommitFull ? indexedCommitFull.slice(0, 7) : null;
|
|
29
29
|
const currentCommitShort = currentCommit ? currentCommit.slice(0, 7) : null;
|
|
30
30
|
|
|
31
|
-
// 3. If no stored commit
|
|
32
|
-
if (
|
|
31
|
+
// 3. If SHAs match → fresh. If no stored commit → staleness unknown (not "fresh").
|
|
32
|
+
if (indexedCommitShort === currentCommitShort) {
|
|
33
33
|
return { isStale: false, indexedAt: null, indexedCommit: indexedCommitShort, currentCommit: currentCommitShort, changedSince: [], staleSince: null };
|
|
34
34
|
}
|
|
35
|
+
if (!indexedCommitFull) {
|
|
36
|
+
return { isStale: true, indexedAt: null, indexedCommit: null, currentCommit: currentCommitShort, changedSince: [], staleSince: null };
|
|
37
|
+
}
|
|
35
38
|
|
|
36
39
|
// Guard: indexedCommitFull is read from SQLite — validate before shell interpolation
|
|
37
40
|
if (!/^[0-9a-f]{7,40}$/i.test(indexedCommitFull)) {
|
package/src/storage/db.ts
CHANGED
|
@@ -5,12 +5,31 @@ import {
|
|
|
5
5
|
CREATE_NODES, CREATE_EDGES, CREATE_COMMUNITIES,
|
|
6
6
|
CREATE_INDEX_META, CREATE_NODES_FTS, CREATE_INDEXES, FTS_SYNC_TRIGGERS,
|
|
7
7
|
CREATE_EMBEDDINGS, CREATE_WIKI_PAGES,
|
|
8
|
+
CREATE_AGENT_INTERACTIONS, CREATE_AGENT_INTERACTIONS_IDX,
|
|
9
|
+
CREATE_AGENT_INTERACTIONS_ORG_IDX, CREATE_AGENT_INTERACTIONS_TYPE_IDX,
|
|
10
|
+
CREATE_AGENT_INTERACTIONS_TS_IDX,
|
|
8
11
|
} from './schema.js';
|
|
9
12
|
import { MonographError } from '../types.js';
|
|
10
13
|
|
|
11
14
|
export type MonographDb = Database.Database;
|
|
12
15
|
|
|
13
|
-
export
|
|
16
|
+
export interface OpenDbOptions {
|
|
17
|
+
/**
|
|
18
|
+
* When true, throw a clear error instead of silently creating a fresh,
|
|
19
|
+
* empty-but-migrated database if `dbPath` doesn't exist yet. Defaults to
|
|
20
|
+
* false to preserve legacy build-time behavior (the `monograph build`
|
|
21
|
+
* codepath legitimately creates new DBs). Read-only analysis tools should
|
|
22
|
+
* pass `true` so pointing them at a repo whose real index lives elsewhere
|
|
23
|
+
* (e.g. running from a subdirectory) fails loudly instead of reporting
|
|
24
|
+
* fake-successful-looking empty results.
|
|
25
|
+
*/
|
|
26
|
+
fileMustExist?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function openDb(dbPath: string, options: OpenDbOptions = {}): MonographDb {
|
|
30
|
+
if (options.fileMustExist && !existsSync(dbPath)) {
|
|
31
|
+
throw new MonographError(`Monograph database does not exist at ${dbPath}. Run monograph build first.`);
|
|
32
|
+
}
|
|
14
33
|
try {
|
|
15
34
|
mkdirSync(dirname(dbPath), { recursive: true });
|
|
16
35
|
const db = new Database(dbPath);
|
|
@@ -45,6 +64,11 @@ function applyMigrations(db: MonographDb): void {
|
|
|
45
64
|
db.exec(FTS_SYNC_TRIGGERS);
|
|
46
65
|
db.exec(CREATE_EMBEDDINGS);
|
|
47
66
|
db.exec(CREATE_WIKI_PAGES);
|
|
67
|
+
db.exec(CREATE_AGENT_INTERACTIONS);
|
|
68
|
+
db.exec(CREATE_AGENT_INTERACTIONS_IDX);
|
|
69
|
+
db.exec(CREATE_AGENT_INTERACTIONS_ORG_IDX);
|
|
70
|
+
db.exec(CREATE_AGENT_INTERACTIONS_TYPE_IDX);
|
|
71
|
+
db.exec(CREATE_AGENT_INTERACTIONS_TS_IDX);
|
|
48
72
|
|
|
49
73
|
// Schema version table — tracks incremental column additions.
|
|
50
74
|
db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)`);
|
|
@@ -66,6 +90,16 @@ export function atomicRebuild(dbPath: string, buildFn: (db: MonographDb) => void
|
|
|
66
90
|
buildFn(db);
|
|
67
91
|
db.close();
|
|
68
92
|
renameSync(tmpPath, dbPath);
|
|
93
|
+
// The old db's WAL-mode sidecar files belong to the file we just replaced —
|
|
94
|
+
// if left behind, the next process to open dbPath pairs the NEW db file with
|
|
95
|
+
// a STALE -wal/-shm whose salt/checkpoint state doesn't match, which can
|
|
96
|
+
// surface as "file is not a database" or subtle corruption.
|
|
97
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
98
|
+
const sidecar = dbPath + suffix;
|
|
99
|
+
if (existsSync(sidecar)) {
|
|
100
|
+
try { unlinkSync(sidecar); } catch { /* best-effort cleanup */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
69
103
|
} catch (err) {
|
|
70
104
|
db.close();
|
|
71
105
|
if (existsSync(tmpPath)) {
|
|
@@ -73,10 +73,17 @@ export function getEdgesForTarget(db: Database.Database, targetId: string): Mono
|
|
|
73
73
|
return rows.map(rowToEdge);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Delete every edge that touches a node belonging to `filePath`, on either end.
|
|
78
|
+
* Must be called BEFORE the corresponding nodes are deleted (the subselects need
|
|
79
|
+
* the file's nodes to still exist to identify which edges to remove) — e.g.
|
|
80
|
+
* `deleteEdgesForFile` then `deleteNodesForFile`, never the reverse.
|
|
81
|
+
*/
|
|
76
82
|
export function deleteEdgesForFile(db: Database.Database, filePath: string): void {
|
|
77
83
|
db.prepare(`
|
|
78
84
|
DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file_path = ?)
|
|
79
|
-
|
|
85
|
+
OR target_id IN (SELECT id FROM nodes WHERE file_path = ?)
|
|
86
|
+
`).run(filePath, filePath);
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export function countEdges(db: Database.Database): number {
|
package/src/storage/fts-store.ts
CHANGED
|
@@ -13,6 +13,20 @@ export interface FtsResult {
|
|
|
13
13
|
endLine: number | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Quote a single FTS5 search term as a string literal when it contains characters
|
|
18
|
+
* that would otherwise be parsed as FTS5 syntax (quotes, parens, boolean keywords
|
|
19
|
+
* like AND/OR/NOT, colons, hyphens, etc). Wrapping in double-quotes forces FTS5 to
|
|
20
|
+
* treat the term as a literal string; any literal `"` inside the term is escaped by
|
|
21
|
+
* doubling it, per FTS5's own string-literal quoting rules.
|
|
22
|
+
*/
|
|
23
|
+
function quoteFtsTerm(term: string): string {
|
|
24
|
+
// Bare alphanumeric/underscore terms need no quoting and are the common case.
|
|
25
|
+
if (/^[A-Za-z0-9_]+$/.test(term)) return term;
|
|
26
|
+
const escaped = term.replace(/"/g, '""');
|
|
27
|
+
return `"${escaped}"`;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export function ftsSearch(
|
|
17
31
|
db: Database.Database,
|
|
18
32
|
query: string,
|
|
@@ -24,8 +38,13 @@ export function ftsSearch(
|
|
|
24
38
|
const safeQuery = query.replace(/[*]/g, ' ').trim();
|
|
25
39
|
if (!safeQuery) return [];
|
|
26
40
|
|
|
27
|
-
// Trigram handles substring matching natively — no need to append * to each term
|
|
28
|
-
|
|
41
|
+
// Trigram handles substring matching natively — no need to append * to each term.
|
|
42
|
+
// Quote each term individually so characters like `"`, `(`, `)`, or bare boolean
|
|
43
|
+
// keywords (AND/OR/NOT) don't get parsed as FTS5 query syntax.
|
|
44
|
+
const ftsQuery = safeQuery
|
|
45
|
+
.split(/\s+/)
|
|
46
|
+
.map(quoteFtsTerm)
|
|
47
|
+
.join(' ');
|
|
29
48
|
|
|
30
49
|
let matchSql = `
|
|
31
50
|
SELECT n.id, n.name, n.norm_label, n.file_path, n.label,
|
|
@@ -52,16 +71,19 @@ export function ftsSearch(
|
|
|
52
71
|
// FTS MATCH can throw on malformed queries; fall through to LIKE path
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// Run the LIKE fallback whenever MATCH threw (malformed/boolean-keyword query) or
|
|
75
|
+
// returned zero rows — not just for short (≤2 char) queries. Short queries need it
|
|
76
|
+
// because trigram requires ≥3 characters to fire; longer queries need it whenever
|
|
77
|
+
// MATCH couldn't handle the syntax, so a query-syntax failure doesn't silently read
|
|
78
|
+
// as "no results".
|
|
79
|
+
if (safeQuery.length <= 2 || matchRows.length === 0) {
|
|
80
|
+
const escapedQuery = safeQuery.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
59
81
|
const likePattern = `%${escapedQuery}%`;
|
|
60
82
|
let likeSql = `
|
|
61
83
|
SELECT n.id, n.name, n.norm_label, n.file_path, n.label,
|
|
62
84
|
n.start_line, n.end_line, 0 AS rank
|
|
63
85
|
FROM nodes n
|
|
64
|
-
WHERE (n.name LIKE ? OR n.norm_label LIKE ? OR n.file_path LIKE ?)
|
|
86
|
+
WHERE (n.name LIKE ? ESCAPE '\\' OR n.norm_label LIKE ? ESCAPE '\\' OR n.file_path LIKE ? ESCAPE '\\')
|
|
65
87
|
`;
|
|
66
88
|
const likeParams: unknown[] = [likePattern, likePattern, likePattern];
|
|
67
89
|
if (label) {
|
|
@@ -186,13 +208,13 @@ export function hybridSearch(
|
|
|
186
208
|
// ── Strategy 2: LIKE fallback ──────────────────────────────────────────────
|
|
187
209
|
// Always run for short queries (≤3 chars) or when FTS returned nothing.
|
|
188
210
|
if (safeQuery.length <= 3 || ftsRows.length === 0) {
|
|
189
|
-
const escapedSafeQuery = safeQuery.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
211
|
+
const escapedSafeQuery = safeQuery.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
190
212
|
const likePattern = `%${escapedSafeQuery}%`;
|
|
191
213
|
let likeSql = `
|
|
192
214
|
SELECT n.id, n.name, n.norm_label, n.file_path, n.label,
|
|
193
215
|
n.start_line, n.end_line
|
|
194
216
|
FROM nodes n
|
|
195
|
-
WHERE (n.name LIKE ? OR n.norm_label LIKE ?)
|
|
217
|
+
WHERE (n.name LIKE ? ESCAPE '\\' OR n.norm_label LIKE ? ESCAPE '\\')
|
|
196
218
|
`;
|
|
197
219
|
const likeParams: unknown[] = [likePattern, likePattern];
|
|
198
220
|
if (label) {
|
package/src/storage/schema.ts
CHANGED
|
@@ -124,6 +124,36 @@ export const CREATE_FILE_CACHE = `
|
|
|
124
124
|
)
|
|
125
125
|
`;
|
|
126
126
|
|
|
127
|
+
export const CREATE_AGENT_INTERACTIONS = `
|
|
128
|
+
CREATE TABLE IF NOT EXISTS agent_interactions (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
session_id TEXT NOT NULL,
|
|
131
|
+
org_name TEXT,
|
|
132
|
+
agent_type TEXT NOT NULL,
|
|
133
|
+
parent_agent TEXT,
|
|
134
|
+
prompt_summary TEXT,
|
|
135
|
+
result_summary TEXT,
|
|
136
|
+
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
tokens_out INTEGER NOT NULL DEFAULT 0,
|
|
138
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
139
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
140
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
141
|
+
timestamp INTEGER NOT NULL,
|
|
142
|
+
metadata TEXT
|
|
143
|
+
)`;
|
|
144
|
+
|
|
145
|
+
export const CREATE_AGENT_INTERACTIONS_IDX = `
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_agent_interactions_session ON agent_interactions(session_id)`;
|
|
147
|
+
|
|
148
|
+
export const CREATE_AGENT_INTERACTIONS_ORG_IDX = `
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_agent_interactions_org ON agent_interactions(org_name)`;
|
|
150
|
+
|
|
151
|
+
export const CREATE_AGENT_INTERACTIONS_TYPE_IDX = `
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_agent_interactions_type ON agent_interactions(agent_type)`;
|
|
153
|
+
|
|
154
|
+
export const CREATE_AGENT_INTERACTIONS_TS_IDX = `
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_agent_interactions_ts ON agent_interactions(timestamp)`;
|
|
156
|
+
|
|
127
157
|
export const FTS_SYNC_TRIGGERS = `
|
|
128
158
|
CREATE TRIGGER IF NOT EXISTS nodes_fts_insert AFTER INSERT ON nodes BEGIN
|
|
129
159
|
INSERT INTO nodes_fts(rowid, id, name, norm_label, file_path, label)
|
package/src/watch/watcher.ts
CHANGED
|
@@ -34,8 +34,17 @@ export async function watchAsync(
|
|
|
34
34
|
do {
|
|
35
35
|
rerun = false;
|
|
36
36
|
opts.onProgress?.({ phase: 'watch', message: `Changed: ${files.slice(0, 3).join(', ')}` });
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
try {
|
|
38
|
+
await buildAsync(repoPath, { onProgress: opts.onProgress, force: opts.force, codeOnly: opts.codeOnly, llmMaxSections: opts.llmMaxSections ?? 0 });
|
|
39
|
+
opts.onProgress?.({ phase: 'watch', message: 'Graph rebuilt.' });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// A rebuild failure (locked DB past busy_timeout, disk full, transient
|
|
42
|
+
// I/O error) must not crash the watch process — log it, notify listeners,
|
|
43
|
+
// and keep watching for the next change instead of letting an unhandled
|
|
44
|
+
// rejection escape this listener.
|
|
45
|
+
watcher.emit('monograph:error', err);
|
|
46
|
+
opts.onProgress?.({ phase: 'watch', message: `Rebuild failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
47
|
+
}
|
|
39
48
|
} while (rerun);
|
|
40
49
|
} finally {
|
|
41
50
|
building = false;
|
package/src/web/analyze-api.ts
CHANGED
|
@@ -52,11 +52,13 @@ async function handleAnalyze(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Set SSE headers
|
|
55
|
+
const origin = req.headers['origin'] ?? '';
|
|
56
|
+
const allowedOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin) ? origin : '';
|
|
55
57
|
res.writeHead(200, {
|
|
56
58
|
'Content-Type': 'text/event-stream',
|
|
57
59
|
'Cache-Control': 'no-cache',
|
|
58
60
|
'Connection': 'keep-alive',
|
|
59
|
-
'Access-Control-Allow-Origin':
|
|
61
|
+
...(allowedOrigin && { 'Access-Control-Allow-Origin': allowedOrigin }),
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
const dbPath = resolve(join(repoPath, '.monomind', 'monograph.db'));
|