@zuvia-software-solutions/code-mapper 1.4.0
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/README.md +215 -0
- package/dist/cli/ai-context.d.ts +19 -0
- package/dist/cli/ai-context.js +168 -0
- package/dist/cli/analyze.d.ts +7 -0
- package/dist/cli/analyze.js +325 -0
- package/dist/cli/augment.d.ts +7 -0
- package/dist/cli/augment.js +27 -0
- package/dist/cli/clean.d.ts +5 -0
- package/dist/cli/clean.js +56 -0
- package/dist/cli/eval-server.d.ts +25 -0
- package/dist/cli/eval-server.js +365 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +102 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +19 -0
- package/dist/cli/list.d.ts +2 -0
- package/dist/cli/list.js +27 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +35 -0
- package/dist/cli/refresh.d.ts +12 -0
- package/dist/cli/refresh.js +165 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +8 -0
- package/dist/cli/setup.d.ts +6 -0
- package/dist/cli/setup.js +218 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +33 -0
- package/dist/cli/tool.d.ts +28 -0
- package/dist/cli/tool.js +87 -0
- package/dist/config/ignore-service.d.ts +32 -0
- package/dist/config/ignore-service.js +282 -0
- package/dist/config/supported-languages.d.ts +23 -0
- package/dist/config/supported-languages.js +52 -0
- package/dist/core/augmentation/engine.d.ts +22 -0
- package/dist/core/augmentation/engine.js +232 -0
- package/dist/core/embeddings/embedder.d.ts +35 -0
- package/dist/core/embeddings/embedder.js +171 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
- package/dist/core/embeddings/embedding-pipeline.js +402 -0
- package/dist/core/embeddings/index.d.ts +5 -0
- package/dist/core/embeddings/index.js +6 -0
- package/dist/core/embeddings/text-generator.d.ts +20 -0
- package/dist/core/embeddings/text-generator.js +159 -0
- package/dist/core/embeddings/types.d.ts +60 -0
- package/dist/core/embeddings/types.js +23 -0
- package/dist/core/graph/graph.d.ts +4 -0
- package/dist/core/graph/graph.js +65 -0
- package/dist/core/graph/types.d.ts +69 -0
- package/dist/core/graph/types.js +3 -0
- package/dist/core/incremental/child-process.d.ts +8 -0
- package/dist/core/incremental/child-process.js +649 -0
- package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
- package/dist/core/incremental/refresh-coordinator.js +147 -0
- package/dist/core/incremental/types.d.ts +78 -0
- package/dist/core/incremental/types.js +153 -0
- package/dist/core/incremental/watcher.d.ts +63 -0
- package/dist/core/incremental/watcher.js +338 -0
- package/dist/core/ingestion/ast-cache.d.ts +12 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +34 -0
- package/dist/core/ingestion/call-processor.js +937 -0
- package/dist/core/ingestion/call-routing.d.ts +40 -0
- package/dist/core/ingestion/call-routing.js +97 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
- package/dist/core/ingestion/cluster-enricher.js +151 -0
- package/dist/core/ingestion/community-processor.d.ts +26 -0
- package/dist/core/ingestion/community-processor.js +272 -0
- package/dist/core/ingestion/constants.d.ts +5 -0
- package/dist/core/ingestion/constants.js +8 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
- package/dist/core/ingestion/entry-point-scoring.js +317 -0
- package/dist/core/ingestion/export-detection.d.ts +11 -0
- package/dist/core/ingestion/export-detection.js +203 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
- package/dist/core/ingestion/filesystem-walker.js +64 -0
- package/dist/core/ingestion/framework-detection.d.ts +42 -0
- package/dist/core/ingestion/framework-detection.js +405 -0
- package/dist/core/ingestion/heritage-processor.d.ts +15 -0
- package/dist/core/ingestion/heritage-processor.js +237 -0
- package/dist/core/ingestion/import-processor.d.ts +31 -0
- package/dist/core/ingestion/import-processor.js +416 -0
- package/dist/core/ingestion/language-config.d.ts +32 -0
- package/dist/core/ingestion/language-config.js +161 -0
- package/dist/core/ingestion/mro-processor.d.ts +32 -0
- package/dist/core/ingestion/mro-processor.js +343 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
- package/dist/core/ingestion/named-binding-extraction.js +343 -0
- package/dist/core/ingestion/parsing-processor.d.ts +20 -0
- package/dist/core/ingestion/parsing-processor.js +282 -0
- package/dist/core/ingestion/pipeline.d.ts +3 -0
- package/dist/core/ingestion/pipeline.js +416 -0
- package/dist/core/ingestion/process-processor.d.ts +42 -0
- package/dist/core/ingestion/process-processor.js +357 -0
- package/dist/core/ingestion/resolution-context.d.ts +40 -0
- package/dist/core/ingestion/resolution-context.js +171 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
- package/dist/core/ingestion/resolvers/csharp.js +101 -0
- package/dist/core/ingestion/resolvers/go.d.ts +8 -0
- package/dist/core/ingestion/resolvers/go.js +33 -0
- package/dist/core/ingestion/resolvers/index.d.ts +14 -0
- package/dist/core/ingestion/resolvers/index.js +10 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
- package/dist/core/ingestion/resolvers/jvm.js +74 -0
- package/dist/core/ingestion/resolvers/php.d.ts +7 -0
- package/dist/core/ingestion/resolvers/php.js +30 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
- package/dist/core/ingestion/resolvers/ruby.js +13 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
- package/dist/core/ingestion/resolvers/rust.js +62 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
- package/dist/core/ingestion/resolvers/standard.js +144 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
- package/dist/core/ingestion/resolvers/utils.js +113 -0
- package/dist/core/ingestion/structure-processor.d.ts +4 -0
- package/dist/core/ingestion/structure-processor.js +39 -0
- package/dist/core/ingestion/symbol-table.d.ts +34 -0
- package/dist/core/ingestion/symbol-table.js +48 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
- package/dist/core/ingestion/tree-sitter-queries.js +691 -0
- package/dist/core/ingestion/type-env.d.ts +52 -0
- package/dist/core/ingestion/type-env.js +349 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/csharp.js +224 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/go.js +261 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
- package/dist/core/ingestion/type-extractors/index.js +30 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/jvm.js +386 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/php.js +280 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/python.js +175 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/ruby.js +218 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/rust.js +290 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/shared.js +322 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/swift.js +140 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
- package/dist/core/ingestion/type-extractors/types.js +4 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/typescript.js +227 -0
- package/dist/core/ingestion/utils.d.ts +73 -0
- package/dist/core/ingestion/utils.js +992 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
- package/dist/core/ingestion/workers/parse-worker.js +1055 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +123 -0
- package/dist/core/lbug/csv-generator.d.ts +28 -0
- package/dist/core/lbug/csv-generator.js +355 -0
- package/dist/core/lbug/lbug-adapter.d.ts +96 -0
- package/dist/core/lbug/lbug-adapter.js +753 -0
- package/dist/core/lbug/schema.d.ts +46 -0
- package/dist/core/lbug/schema.js +402 -0
- package/dist/core/search/bm25-index.d.ts +20 -0
- package/dist/core/search/bm25-index.js +123 -0
- package/dist/core/search/hybrid-search.d.ts +32 -0
- package/dist/core/search/hybrid-search.js +131 -0
- package/dist/core/search/query-cache.d.ts +18 -0
- package/dist/core/search/query-cache.js +47 -0
- package/dist/core/search/query-expansion.d.ts +19 -0
- package/dist/core/search/query-expansion.js +75 -0
- package/dist/core/search/reranker.d.ts +29 -0
- package/dist/core/search/reranker.js +122 -0
- package/dist/core/search/types.d.ts +154 -0
- package/dist/core/search/types.js +51 -0
- package/dist/core/semantic/tsgo-service.d.ts +67 -0
- package/dist/core/semantic/tsgo-service.js +355 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
- package/dist/core/tree-sitter/parser-loader.js +71 -0
- package/dist/lib/memory-guard.d.ts +35 -0
- package/dist/lib/memory-guard.js +70 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +6 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
- package/dist/mcp/compatible-stdio-transport.js +209 -0
- package/dist/mcp/core/embedder.d.ts +24 -0
- package/dist/mcp/core/embedder.js +168 -0
- package/dist/mcp/core/lbug-adapter.d.ts +29 -0
- package/dist/mcp/core/lbug-adapter.js +330 -0
- package/dist/mcp/local/local-backend.d.ts +188 -0
- package/dist/mcp/local/local-backend.js +2759 -0
- package/dist/mcp/resources.d.ts +22 -0
- package/dist/mcp/resources.js +379 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/staleness.d.ts +10 -0
- package/dist/mcp/staleness.js +25 -0
- package/dist/mcp/tools.d.ts +21 -0
- package/dist/mcp/tools.js +202 -0
- package/dist/server/api.d.ts +5 -0
- package/dist/server/api.js +340 -0
- package/dist/server/mcp-http.d.ts +7 -0
- package/dist/server/mcp-http.js +95 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +87 -0
- package/dist/storage/repo-manager.js +249 -0
- package/dist/types/pipeline.d.ts +35 -0
- package/dist/types/pipeline.js +20 -0
- package/hooks/claude/code-mapper-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/models/mlx-embedder.py +185 -0
- package/package.json +100 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// code-mapper/src/core/incremental/refresh-coordinator.ts
|
|
2
|
+
/**
|
|
3
|
+
* @file refresh-coordinator.ts
|
|
4
|
+
* @description Parent-side orchestrator that manages child process lifecycle for
|
|
5
|
+
* incremental DB refreshes — forks the worker, sends dirty files over IPC,
|
|
6
|
+
* and resolves/rejects the caller's promise based on the child's response
|
|
7
|
+
*/
|
|
8
|
+
import { fork } from 'child_process';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { parseChildMessage, assertNever, } from './types.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const CHILD_SCRIPT = path.join(__dirname, 'child-process.js');
|
|
15
|
+
/** Default timeout before the child is forcefully terminated */
|
|
16
|
+
const DEFAULT_REFRESH_TIMEOUT_MS = 30_000;
|
|
17
|
+
/** Grace period between SIGTERM and SIGKILL */
|
|
18
|
+
const KILL_GRACE_MS = 5_000;
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Coordinator
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export class RefreshCoordinator {
|
|
23
|
+
/** In-flight refreshes keyed by repo ID — ensures one refresh per repo at a time */
|
|
24
|
+
inFlight = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Trigger an incremental refresh for the given repo
|
|
27
|
+
*
|
|
28
|
+
* If a refresh for this repo is already in progress the existing promise is
|
|
29
|
+
* returned (deduplication). Otherwise a child process is forked, the dirty
|
|
30
|
+
* file list is sent over IPC, and the returned promise settles once the child
|
|
31
|
+
* reports success or failure
|
|
32
|
+
*/
|
|
33
|
+
async refresh(repo, dirtyFiles, existingPaths, timeoutMs) {
|
|
34
|
+
const key = repo.id;
|
|
35
|
+
// Deduplicate — return the existing promise if already refreshing this repo
|
|
36
|
+
const existing = this.inFlight.get(key);
|
|
37
|
+
if (existing)
|
|
38
|
+
return existing;
|
|
39
|
+
const promise = this.doRefresh(repo, dirtyFiles, existingPaths, timeoutMs).finally(() => {
|
|
40
|
+
this.inFlight.delete(key);
|
|
41
|
+
});
|
|
42
|
+
this.inFlight.set(key, promise);
|
|
43
|
+
return promise;
|
|
44
|
+
}
|
|
45
|
+
// -------------------------------------------------------------------------
|
|
46
|
+
// Internal
|
|
47
|
+
// -------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Core refresh logic — queries existing file paths from the DB, forks the
|
|
50
|
+
* child process, and wires up IPC + timeout handling
|
|
51
|
+
*/
|
|
52
|
+
async doRefresh(repo, dirtyFiles, existingPaths, timeoutMs) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
// Fork the worker child
|
|
55
|
+
const child = fork(CHILD_SCRIPT, [], {
|
|
56
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
// Pass existing file paths via env so the child has full context
|
|
60
|
+
CODE_MAPPER_EXISTING_PATHS: JSON.stringify(existingPaths),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
let settled = false;
|
|
64
|
+
// -- Cleanup helper ---------------------------------------------------
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
child.removeAllListeners();
|
|
67
|
+
if (child.stdout)
|
|
68
|
+
child.stdout.removeAllListeners();
|
|
69
|
+
if (child.stderr)
|
|
70
|
+
child.stderr.removeAllListeners();
|
|
71
|
+
clearTimeout(timeoutHandle);
|
|
72
|
+
clearTimeout(killHandle);
|
|
73
|
+
};
|
|
74
|
+
const settle = (action, value) => {
|
|
75
|
+
if (settled)
|
|
76
|
+
return;
|
|
77
|
+
settled = true;
|
|
78
|
+
cleanup();
|
|
79
|
+
if (action === 'resolve') {
|
|
80
|
+
resolve(value);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
reject(value);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// -- Timeout handling -------------------------------------------------
|
|
87
|
+
let killHandle;
|
|
88
|
+
const effectiveTimeout = timeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
|
|
89
|
+
const timeoutHandle = setTimeout(() => {
|
|
90
|
+
// Graceful shutdown first
|
|
91
|
+
child.kill('SIGTERM');
|
|
92
|
+
killHandle = setTimeout(() => {
|
|
93
|
+
child.kill('SIGKILL');
|
|
94
|
+
}, KILL_GRACE_MS);
|
|
95
|
+
settle('reject', new Error(`Incremental refresh timed out after ${effectiveTimeout}ms for repo ${repo.id}`));
|
|
96
|
+
}, effectiveTimeout);
|
|
97
|
+
// -- IPC message handling ---------------------------------------------
|
|
98
|
+
child.on('message', (raw) => {
|
|
99
|
+
let msg;
|
|
100
|
+
try {
|
|
101
|
+
msg = parseChildMessage(raw);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
settle('reject', new Error(`Invalid IPC message from child: ${err instanceof Error ? err.message : String(err)}`));
|
|
105
|
+
child.kill('SIGTERM');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
switch (msg.kind) {
|
|
109
|
+
case 'progress':
|
|
110
|
+
// Progress messages are informational — no action required
|
|
111
|
+
break;
|
|
112
|
+
case 'success':
|
|
113
|
+
settle('resolve', msg.payload);
|
|
114
|
+
break;
|
|
115
|
+
case 'error':
|
|
116
|
+
settle('reject', Object.assign(new Error(msg.message), msg.stack ? { childStack: msg.stack } : {}));
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
assertNever(msg, `Unhandled child message kind: ${JSON.stringify(msg)}`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// -- Child exit handling ----------------------------------------------
|
|
123
|
+
child.on('error', (err) => {
|
|
124
|
+
settle('reject', new Error(`Child process error: ${err.message}`));
|
|
125
|
+
});
|
|
126
|
+
child.on('exit', (code, signal) => {
|
|
127
|
+
// If already settled (success/error message arrived first), nothing to do
|
|
128
|
+
if (settled)
|
|
129
|
+
return;
|
|
130
|
+
if (code !== 0) {
|
|
131
|
+
settle('reject', new Error(`Child process exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'none'})`));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// -- Send the refresh command to the child ----------------------------
|
|
135
|
+
const message = {
|
|
136
|
+
kind: 'refresh',
|
|
137
|
+
payload: {
|
|
138
|
+
repoRoot: repo.repoRoot,
|
|
139
|
+
dbPath: repo.dbPath,
|
|
140
|
+
storagePath: repo.storagePath,
|
|
141
|
+
dirtyFiles,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
child.send(message);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared type definitions for the incremental refresh system
|
|
3
|
+
* @description Branded types, discriminated union IPC protocol, boundary validators
|
|
4
|
+
*
|
|
5
|
+
* This module is imported by BOTH the parent (MCP server) and child (refresh worker)
|
|
6
|
+
* processes — it defines the compile-time-safe contract between them
|
|
7
|
+
*/
|
|
8
|
+
declare const __brand: unique symbol;
|
|
9
|
+
type Brand<T, B extends string> = T & {
|
|
10
|
+
readonly [__brand]: B;
|
|
11
|
+
};
|
|
12
|
+
/** Absolute filesystem path to a source file */
|
|
13
|
+
export type AbsoluteFilePath = Brand<string, 'AbsoluteFilePath'>;
|
|
14
|
+
/** Path relative to repo root (forward-slash normalized, matches graph filePath) */
|
|
15
|
+
export type RelativeFilePath = Brand<string, 'RelativeFilePath'>;
|
|
16
|
+
/** Absolute path to the LadybugDB database directory */
|
|
17
|
+
export type DbPath = Brand<string, 'DbPath'>;
|
|
18
|
+
/** Absolute path to the repository root */
|
|
19
|
+
export type RepoRoot = Brand<string, 'RepoRoot'>;
|
|
20
|
+
/** Stable repo identifier from LocalBackend */
|
|
21
|
+
export type RepoId = Brand<string, 'RepoId'>;
|
|
22
|
+
export declare function toAbsoluteFilePath(p: string): AbsoluteFilePath;
|
|
23
|
+
export declare function toRelativeFilePath(p: string): RelativeFilePath;
|
|
24
|
+
export declare function toDbPath(p: string): DbPath;
|
|
25
|
+
export declare function toRepoRoot(p: string): RepoRoot;
|
|
26
|
+
export declare function toRepoId(s: string): RepoId;
|
|
27
|
+
export declare const FILE_CHANGE_KINDS: readonly ["modified", "created", "deleted"];
|
|
28
|
+
export type FileChangeKind = typeof FILE_CHANGE_KINDS[number];
|
|
29
|
+
/** A single file change detected by the watcher */
|
|
30
|
+
export interface DirtyFileEntry {
|
|
31
|
+
readonly relativePath: RelativeFilePath;
|
|
32
|
+
readonly changeKind: FileChangeKind;
|
|
33
|
+
}
|
|
34
|
+
export declare const REFRESH_PHASES: readonly ["deleting-old-nodes", "parsing", "inserting-nodes", "resolving-imports", "resolving-calls", "resolving-heritage", "rebuilding-fts", "done"];
|
|
35
|
+
export type RefreshPhase = typeof REFRESH_PHASES[number];
|
|
36
|
+
/** Payload sent from parent to child to initiate a refresh */
|
|
37
|
+
export interface RefreshPayload {
|
|
38
|
+
readonly repoRoot: RepoRoot;
|
|
39
|
+
readonly dbPath: DbPath;
|
|
40
|
+
readonly storagePath: string;
|
|
41
|
+
readonly dirtyFiles: readonly DirtyFileEntry[];
|
|
42
|
+
}
|
|
43
|
+
/** Messages from parent to child */
|
|
44
|
+
export type ParentToChildMessage = {
|
|
45
|
+
readonly kind: 'refresh';
|
|
46
|
+
readonly payload: RefreshPayload;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'shutdown';
|
|
49
|
+
};
|
|
50
|
+
/** Result data returned by a successful refresh */
|
|
51
|
+
export interface RefreshResult {
|
|
52
|
+
readonly filesProcessed: number;
|
|
53
|
+
readonly filesSkipped: number;
|
|
54
|
+
readonly nodesDeleted: number;
|
|
55
|
+
readonly nodesInserted: number;
|
|
56
|
+
readonly edgesInserted: number;
|
|
57
|
+
readonly durationMs: number;
|
|
58
|
+
}
|
|
59
|
+
/** Messages from child to parent */
|
|
60
|
+
export type ChildToParentMessage = {
|
|
61
|
+
readonly kind: 'progress';
|
|
62
|
+
readonly phase: RefreshPhase;
|
|
63
|
+
readonly detail: string;
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: 'success';
|
|
66
|
+
readonly payload: RefreshResult;
|
|
67
|
+
} | {
|
|
68
|
+
readonly kind: 'error';
|
|
69
|
+
readonly message: string;
|
|
70
|
+
readonly stack?: string;
|
|
71
|
+
};
|
|
72
|
+
/** Validate and narrow an unknown IPC message from the parent */
|
|
73
|
+
export declare function parseParentMessage(raw: unknown): ParentToChildMessage;
|
|
74
|
+
/** Validate and narrow an unknown IPC message from the child */
|
|
75
|
+
export declare function parseChildMessage(raw: unknown): ChildToParentMessage;
|
|
76
|
+
/** Compile-time exhaustiveness check — use in default/else branches */
|
|
77
|
+
export declare function assertNever(value: never, msg?: string): never;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// code-mapper/src/core/incremental/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* @file Shared type definitions for the incremental refresh system
|
|
4
|
+
* @description Branded types, discriminated union IPC protocol, boundary validators
|
|
5
|
+
*
|
|
6
|
+
* This module is imported by BOTH the parent (MCP server) and child (refresh worker)
|
|
7
|
+
* processes — it defines the compile-time-safe contract between them
|
|
8
|
+
*/
|
|
9
|
+
import path from 'path';
|
|
10
|
+
// Branded constructors with runtime validation
|
|
11
|
+
export function toAbsoluteFilePath(p) {
|
|
12
|
+
if (!path.isAbsolute(p))
|
|
13
|
+
throw new TypeError(`Expected absolute path, got: ${p}`);
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
export function toRelativeFilePath(p) {
|
|
17
|
+
if (path.isAbsolute(p))
|
|
18
|
+
throw new TypeError(`Expected relative path, got: ${p}`);
|
|
19
|
+
return p.replace(/\\/g, '/');
|
|
20
|
+
}
|
|
21
|
+
export function toDbPath(p) {
|
|
22
|
+
if (!path.isAbsolute(p))
|
|
23
|
+
throw new TypeError(`Expected absolute DB path, got: ${p}`);
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
export function toRepoRoot(p) {
|
|
27
|
+
if (!path.isAbsolute(p))
|
|
28
|
+
throw new TypeError(`Expected absolute repo root, got: ${p}`);
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
export function toRepoId(s) {
|
|
32
|
+
if (!s)
|
|
33
|
+
throw new TypeError('RepoId cannot be empty');
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// File change kinds — const tuple is the single source of truth
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export const FILE_CHANGE_KINDS = ['modified', 'created', 'deleted'];
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Refresh phases — const tuple is the single source of truth
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export const REFRESH_PHASES = [
|
|
44
|
+
'deleting-old-nodes',
|
|
45
|
+
'parsing',
|
|
46
|
+
'inserting-nodes',
|
|
47
|
+
'resolving-imports',
|
|
48
|
+
'resolving-calls',
|
|
49
|
+
'resolving-heritage',
|
|
50
|
+
'rebuilding-fts',
|
|
51
|
+
'done',
|
|
52
|
+
];
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Boundary validators — parse unknown IPC data into typed messages
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function isObject(v) {
|
|
57
|
+
return typeof v === 'object' && v !== null;
|
|
58
|
+
}
|
|
59
|
+
function assertString(v, label) {
|
|
60
|
+
if (typeof v !== 'string')
|
|
61
|
+
throw new TypeError(`${label} must be a string, got ${typeof v}`);
|
|
62
|
+
}
|
|
63
|
+
function assertArray(v, label) {
|
|
64
|
+
if (!Array.isArray(v))
|
|
65
|
+
throw new TypeError(`${label} must be an array`);
|
|
66
|
+
}
|
|
67
|
+
/** Validate and narrow an unknown IPC message from the parent */
|
|
68
|
+
export function parseParentMessage(raw) {
|
|
69
|
+
if (!isObject(raw))
|
|
70
|
+
throw new TypeError('IPC message must be an object');
|
|
71
|
+
assertString(raw.kind, 'kind');
|
|
72
|
+
if (raw.kind === 'shutdown')
|
|
73
|
+
return { kind: 'shutdown' };
|
|
74
|
+
if (raw.kind === 'refresh') {
|
|
75
|
+
if (!isObject(raw.payload))
|
|
76
|
+
throw new TypeError('refresh payload must be an object');
|
|
77
|
+
const p = raw.payload;
|
|
78
|
+
assertString(p.repoRoot, 'repoRoot');
|
|
79
|
+
assertString(p.dbPath, 'dbPath');
|
|
80
|
+
assertString(p.storagePath, 'storagePath');
|
|
81
|
+
assertArray(p.dirtyFiles, 'dirtyFiles');
|
|
82
|
+
const dirtyFiles = [];
|
|
83
|
+
for (const entry of p.dirtyFiles) {
|
|
84
|
+
if (!isObject(entry))
|
|
85
|
+
throw new TypeError('dirtyFiles entry must be an object');
|
|
86
|
+
assertString(entry.relativePath, 'relativePath');
|
|
87
|
+
assertString(entry.changeKind, 'changeKind');
|
|
88
|
+
if (!FILE_CHANGE_KINDS.includes(entry.changeKind)) {
|
|
89
|
+
throw new TypeError(`Invalid changeKind: ${entry.changeKind}`);
|
|
90
|
+
}
|
|
91
|
+
dirtyFiles.push({
|
|
92
|
+
relativePath: entry.relativePath,
|
|
93
|
+
changeKind: entry.changeKind,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
kind: 'refresh',
|
|
98
|
+
payload: {
|
|
99
|
+
repoRoot: p.repoRoot,
|
|
100
|
+
dbPath: p.dbPath,
|
|
101
|
+
storagePath: p.storagePath,
|
|
102
|
+
dirtyFiles,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw new TypeError(`Unknown IPC message kind: ${raw.kind}`);
|
|
107
|
+
}
|
|
108
|
+
/** Validate and narrow an unknown IPC message from the child */
|
|
109
|
+
export function parseChildMessage(raw) {
|
|
110
|
+
if (!isObject(raw))
|
|
111
|
+
throw new TypeError('IPC message must be an object');
|
|
112
|
+
assertString(raw.kind, 'kind');
|
|
113
|
+
if (raw.kind === 'progress') {
|
|
114
|
+
assertString(raw.phase, 'phase');
|
|
115
|
+
if (!REFRESH_PHASES.includes(raw.phase)) {
|
|
116
|
+
throw new TypeError(`Invalid refresh phase: ${raw.phase}`);
|
|
117
|
+
}
|
|
118
|
+
assertString(raw.detail, 'detail');
|
|
119
|
+
return { kind: 'progress', phase: raw.phase, detail: raw.detail };
|
|
120
|
+
}
|
|
121
|
+
if (raw.kind === 'success') {
|
|
122
|
+
if (!isObject(raw.payload))
|
|
123
|
+
throw new TypeError('success payload must be an object');
|
|
124
|
+
const p = raw.payload;
|
|
125
|
+
return {
|
|
126
|
+
kind: 'success',
|
|
127
|
+
payload: {
|
|
128
|
+
filesProcessed: Number(p.filesProcessed) || 0,
|
|
129
|
+
filesSkipped: Number(p.filesSkipped) || 0,
|
|
130
|
+
nodesDeleted: Number(p.nodesDeleted) || 0,
|
|
131
|
+
nodesInserted: Number(p.nodesInserted) || 0,
|
|
132
|
+
edgesInserted: Number(p.edgesInserted) || 0,
|
|
133
|
+
durationMs: Number(p.durationMs) || 0,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (raw.kind === 'error') {
|
|
138
|
+
assertString(raw.message, 'message');
|
|
139
|
+
return {
|
|
140
|
+
kind: 'error',
|
|
141
|
+
message: raw.message,
|
|
142
|
+
stack: typeof raw.stack === 'string' ? raw.stack : undefined,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
throw new TypeError(`Unknown child message kind: ${raw.kind}`);
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Exhaustiveness guard
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/** Compile-time exhaustiveness check — use in default/else branches */
|
|
151
|
+
export function assertNever(value, msg) {
|
|
152
|
+
throw new Error(msg ?? `Unexpected value: ${JSON.stringify(value)}`);
|
|
153
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file File system watcher for incremental refresh
|
|
3
|
+
* @description Monitors source file changes using fs.watch (recursive) on macOS/Windows,
|
|
4
|
+
* falling back to interval-based polling on platforms where recursive watching is
|
|
5
|
+
* unavailable (Linux Node <19). Maintains a debounced dirty-file map that the
|
|
6
|
+
* refresh scheduler drains on each tick
|
|
7
|
+
*/
|
|
8
|
+
import { type RelativeFilePath, type RepoRoot, type DirtyFileEntry } from './types.js';
|
|
9
|
+
export declare class FileSystemWatcher {
|
|
10
|
+
private readonly repoRoot;
|
|
11
|
+
private readonly dirtyFiles;
|
|
12
|
+
private readonly debounceTimers;
|
|
13
|
+
private previousSnapshot;
|
|
14
|
+
private fsWatcher;
|
|
15
|
+
private pollTimer;
|
|
16
|
+
private readonly knownFiles;
|
|
17
|
+
private running;
|
|
18
|
+
constructor(repoRoot: RepoRoot);
|
|
19
|
+
/** Begin watching the repository for source file changes */
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
/** Stop watching and release all resources */
|
|
22
|
+
stop(): void;
|
|
23
|
+
/** Returns true when there is at least one dirty file pending */
|
|
24
|
+
hasDirtyFiles(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Flush all pending debounce timers so their changes become immediately
|
|
27
|
+
* visible in the dirty file map. Must be called before drain() to avoid
|
|
28
|
+
* missing edits that happened within the debounce window.
|
|
29
|
+
*/
|
|
30
|
+
flush(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Re-inject dirty file entries that were previously drained but whose
|
|
33
|
+
* refresh failed. Ensures the next ensureFresh() retries them.
|
|
34
|
+
* Existing entries (from new edits since the drain) are NOT overwritten.
|
|
35
|
+
*/
|
|
36
|
+
inject(entries: readonly DirtyFileEntry[]): void;
|
|
37
|
+
/**
|
|
38
|
+
* Return the current dirty file map and clear it
|
|
39
|
+
*
|
|
40
|
+
* Returns null if there are no dirty files, allowing callers to
|
|
41
|
+
* short-circuit without allocating
|
|
42
|
+
*/
|
|
43
|
+
drain(): ReadonlyMap<RelativeFilePath, DirtyFileEntry> | null;
|
|
44
|
+
private startRecursiveWatch;
|
|
45
|
+
private handleRawEvent;
|
|
46
|
+
private recordChange;
|
|
47
|
+
private startPolling;
|
|
48
|
+
private pollOnce;
|
|
49
|
+
private enqueuePollChange;
|
|
50
|
+
/**
|
|
51
|
+
* Walk the repo tree synchronously and collect mtime+size for every
|
|
52
|
+
* supported source file
|
|
53
|
+
*
|
|
54
|
+
* Uses synchronous I/O because this runs on a short interval and needs
|
|
55
|
+
* a consistent snapshot — async walk would race with itself
|
|
56
|
+
*/
|
|
57
|
+
private takeSnapshotSync;
|
|
58
|
+
/**
|
|
59
|
+
* Walk the repo tree and populate knownFiles so the first batch of
|
|
60
|
+
* watcher events can distinguish 'created' from 'modified'
|
|
61
|
+
*/
|
|
62
|
+
private seedKnownFiles;
|
|
63
|
+
}
|