trellis 1.0.8 → 2.0.6
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/LICENSE +21 -0
- package/README.md +564 -83
- package/bin/trellis.mjs +2 -0
- package/dist/cli/index.js +4718 -0
- package/dist/core/index.js +12 -0
- package/dist/decisions/index.js +19 -0
- package/dist/embeddings/index.js +43 -0
- package/dist/index-1j1anhmr.js +4038 -0
- package/dist/index-3s0eak0p.js +1556 -0
- package/dist/index-8pce39mh.js +272 -0
- package/dist/index-a76rekgs.js +67 -0
- package/dist/index-cy9k1g6v.js +684 -0
- package/dist/index-fd4e26s4.js +69 -0
- package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
- package/dist/index-gnw8d7d6.js +51 -0
- package/dist/index-vkpkfwhq.js +817 -0
- package/dist/index.js +118 -2876
- package/dist/links/index.js +55 -0
- package/dist/transformers-m9je15kg.js +32491 -0
- package/dist/vcs/index.js +110 -0
- package/logo.png +0 -0
- package/logo.svg +9 -0
- package/package.json +79 -76
- package/src/cli/index.ts +2340 -0
- package/src/core/index.ts +35 -0
- package/src/core/kernel/middleware.ts +44 -0
- package/src/core/persist/backend.ts +64 -0
- package/src/core/store/eav-store.ts +467 -0
- package/src/decisions/auto-capture.ts +136 -0
- package/src/decisions/hooks.ts +163 -0
- package/src/decisions/index.ts +261 -0
- package/src/decisions/types.ts +103 -0
- package/src/embeddings/chunker.ts +327 -0
- package/src/embeddings/index.ts +41 -0
- package/src/embeddings/model.ts +95 -0
- package/src/embeddings/search.ts +305 -0
- package/src/embeddings/store.ts +313 -0
- package/src/embeddings/types.ts +85 -0
- package/src/engine.ts +1083 -0
- package/src/garden/cluster.ts +330 -0
- package/src/garden/garden.ts +306 -0
- package/src/garden/index.ts +29 -0
- package/src/git/git-exporter.ts +286 -0
- package/src/git/git-importer.ts +329 -0
- package/src/git/git-reader.ts +189 -0
- package/src/git/index.ts +22 -0
- package/src/identity/governance.ts +211 -0
- package/src/identity/identity.ts +224 -0
- package/src/identity/index.ts +30 -0
- package/src/identity/signing-middleware.ts +97 -0
- package/src/index.ts +20 -0
- package/src/links/index.ts +49 -0
- package/src/links/lifecycle.ts +400 -0
- package/src/links/parser.ts +484 -0
- package/src/links/ref-index.ts +186 -0
- package/src/links/resolver.ts +314 -0
- package/src/links/types.ts +108 -0
- package/src/mcp/index.ts +22 -0
- package/src/mcp/server.ts +1278 -0
- package/src/semantic/csharp-parser.ts +493 -0
- package/src/semantic/go-parser.ts +585 -0
- package/src/semantic/index.ts +34 -0
- package/src/semantic/java-parser.ts +456 -0
- package/src/semantic/python-parser.ts +659 -0
- package/src/semantic/ruby-parser.ts +446 -0
- package/src/semantic/rust-parser.ts +784 -0
- package/src/semantic/semantic-merge.ts +210 -0
- package/src/semantic/ts-parser.ts +681 -0
- package/src/semantic/types.ts +175 -0
- package/src/sync/index.ts +32 -0
- package/src/sync/memory-transport.ts +66 -0
- package/src/sync/reconciler.ts +237 -0
- package/src/sync/sync-engine.ts +258 -0
- package/src/sync/types.ts +104 -0
- package/src/vcs/blob-store.ts +124 -0
- package/src/vcs/branch.ts +150 -0
- package/src/vcs/checkpoint.ts +64 -0
- package/src/vcs/decompose.ts +469 -0
- package/src/vcs/diff.ts +409 -0
- package/src/vcs/engine-context.ts +26 -0
- package/src/vcs/index.ts +23 -0
- package/src/vcs/issue.ts +800 -0
- package/src/vcs/merge.ts +425 -0
- package/src/vcs/milestone.ts +124 -0
- package/src/vcs/ops.ts +59 -0
- package/src/vcs/types.ts +213 -0
- package/src/vcs/vcs-middleware.ts +81 -0
- package/src/watcher/fs-watcher.ts +217 -0
- package/src/watcher/index.ts +9 -0
- package/src/watcher/ingestion.ts +116 -0
- package/dist/ai/index.js +0 -688
- package/dist/cli/server.js +0 -3321
- package/dist/cli/tql.js +0 -5282
- package/dist/client/tql-client.js +0 -108
- package/dist/graph/index.js +0 -2248
- package/dist/kernel/logic-middleware.js +0 -179
- package/dist/kernel/middleware.js +0 -0
- package/dist/kernel/operations.js +0 -32
- package/dist/kernel/schema-middleware.js +0 -34
- package/dist/kernel/security-middleware.js +0 -53
- package/dist/kernel/trellis-kernel.js +0 -2239
- package/dist/kernel/workspace.js +0 -91
- package/dist/persist/backend.js +0 -0
- package/dist/persist/sqlite-backend.js +0 -123
- package/dist/query/index.js +0 -1643
- package/dist/server/index.js +0 -3309
- package/dist/workflows/index.js +0 -3160
|
@@ -0,0 +1,4038 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
EAVStore
|
|
4
|
+
} from "./index-gkvhzm9f.js";
|
|
5
|
+
import {
|
|
6
|
+
BlobStore,
|
|
7
|
+
addCriterion,
|
|
8
|
+
assignIssue,
|
|
9
|
+
blockIssue,
|
|
10
|
+
buildFileStateAtOp,
|
|
11
|
+
checkCompletionReadiness,
|
|
12
|
+
closeIssue,
|
|
13
|
+
createBranch,
|
|
14
|
+
createCheckpoint,
|
|
15
|
+
createIssue,
|
|
16
|
+
createMilestone,
|
|
17
|
+
decompose,
|
|
18
|
+
deleteBranch,
|
|
19
|
+
diffFileStates,
|
|
20
|
+
diffOpRange,
|
|
21
|
+
getActiveIssues,
|
|
22
|
+
getIssue,
|
|
23
|
+
listBranches,
|
|
24
|
+
listCheckpoints,
|
|
25
|
+
listIssues,
|
|
26
|
+
listMilestones,
|
|
27
|
+
loadBranchState,
|
|
28
|
+
pauseIssue,
|
|
29
|
+
reopenIssue,
|
|
30
|
+
resumeIssue,
|
|
31
|
+
runCriteria,
|
|
32
|
+
saveBranchState,
|
|
33
|
+
setCriterionStatus,
|
|
34
|
+
startIssue,
|
|
35
|
+
switchBranch,
|
|
36
|
+
threeWayMerge,
|
|
37
|
+
triageIssue,
|
|
38
|
+
unblockIssue,
|
|
39
|
+
updateIssue
|
|
40
|
+
} from "./index-3s0eak0p.js";
|
|
41
|
+
import {
|
|
42
|
+
getDecision,
|
|
43
|
+
getDecisionChain,
|
|
44
|
+
queryDecisions,
|
|
45
|
+
recordDecision
|
|
46
|
+
} from "./index-8pce39mh.js";
|
|
47
|
+
import {
|
|
48
|
+
DEFAULT_CONFIG,
|
|
49
|
+
createVcsOp
|
|
50
|
+
} from "./index-fd4e26s4.js";
|
|
51
|
+
|
|
52
|
+
// src/watcher/fs-watcher.ts
|
|
53
|
+
import { watch } from "fs";
|
|
54
|
+
import { readdir, stat, readFile } from "fs/promises";
|
|
55
|
+
import { join, relative } from "path";
|
|
56
|
+
async function hashFile(filePath) {
|
|
57
|
+
const content = await readFile(filePath);
|
|
58
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
|
|
59
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
60
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
61
|
+
}
|
|
62
|
+
function shouldIgnore(relPath, patterns) {
|
|
63
|
+
for (const pattern of patterns) {
|
|
64
|
+
if (pattern.startsWith("*.")) {
|
|
65
|
+
const ext = pattern.slice(1);
|
|
66
|
+
if (relPath.endsWith(ext))
|
|
67
|
+
return true;
|
|
68
|
+
} else if (relPath.includes(pattern)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class FileWatcher {
|
|
76
|
+
config;
|
|
77
|
+
watchers = [];
|
|
78
|
+
debounceTimers = new Map;
|
|
79
|
+
knownFiles = new Map;
|
|
80
|
+
running = false;
|
|
81
|
+
constructor(config) {
|
|
82
|
+
this.config = config;
|
|
83
|
+
}
|
|
84
|
+
async scan() {
|
|
85
|
+
const events = [];
|
|
86
|
+
const entries = await this.walkDir(this.config.rootPath);
|
|
87
|
+
for (const absPath of entries) {
|
|
88
|
+
const relPath = relative(this.config.rootPath, absPath);
|
|
89
|
+
if (shouldIgnore(relPath, this.config.ignorePatterns))
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const hash = await hashFile(absPath);
|
|
93
|
+
const stats = await stat(absPath);
|
|
94
|
+
this.knownFiles.set(relPath, hash);
|
|
95
|
+
events.push({
|
|
96
|
+
type: "add",
|
|
97
|
+
path: relPath,
|
|
98
|
+
contentHash: hash,
|
|
99
|
+
size: stats.size,
|
|
100
|
+
timestamp: new Date().toISOString()
|
|
101
|
+
});
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
return events;
|
|
105
|
+
}
|
|
106
|
+
start() {
|
|
107
|
+
if (this.running)
|
|
108
|
+
return;
|
|
109
|
+
this.running = true;
|
|
110
|
+
try {
|
|
111
|
+
const watcher = watch(this.config.rootPath, { recursive: true }, (eventType, filename) => {
|
|
112
|
+
if (!filename)
|
|
113
|
+
return;
|
|
114
|
+
const relPath = filename.toString();
|
|
115
|
+
if (shouldIgnore(relPath, this.config.ignorePatterns))
|
|
116
|
+
return;
|
|
117
|
+
this.debouncedHandle(relPath);
|
|
118
|
+
});
|
|
119
|
+
this.watchers.push(watcher);
|
|
120
|
+
} catch {
|
|
121
|
+
console.warn("Recursive watch not supported; using scan-based polling.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
stop() {
|
|
125
|
+
this.running = false;
|
|
126
|
+
for (const w of this.watchers) {
|
|
127
|
+
w.close();
|
|
128
|
+
}
|
|
129
|
+
this.watchers = [];
|
|
130
|
+
for (const timer of this.debounceTimers.values()) {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
this.debounceTimers.clear();
|
|
134
|
+
}
|
|
135
|
+
getKnownFiles() {
|
|
136
|
+
return new Map(this.knownFiles);
|
|
137
|
+
}
|
|
138
|
+
debouncedHandle(relPath) {
|
|
139
|
+
const existing = this.debounceTimers.get(relPath);
|
|
140
|
+
if (existing)
|
|
141
|
+
clearTimeout(existing);
|
|
142
|
+
const timer = setTimeout(async () => {
|
|
143
|
+
this.debounceTimers.delete(relPath);
|
|
144
|
+
await this.handleChange(relPath);
|
|
145
|
+
}, this.config.debounceMs);
|
|
146
|
+
this.debounceTimers.set(relPath, timer);
|
|
147
|
+
}
|
|
148
|
+
async handleChange(relPath) {
|
|
149
|
+
const absPath = join(this.config.rootPath, relPath);
|
|
150
|
+
const known = this.knownFiles.get(relPath);
|
|
151
|
+
try {
|
|
152
|
+
const stats = await stat(absPath);
|
|
153
|
+
if (!stats.isFile())
|
|
154
|
+
return;
|
|
155
|
+
const hash = await hashFile(absPath);
|
|
156
|
+
if (!known) {
|
|
157
|
+
this.knownFiles.set(relPath, hash);
|
|
158
|
+
await this.config.onEvent({
|
|
159
|
+
type: "add",
|
|
160
|
+
path: relPath,
|
|
161
|
+
contentHash: hash,
|
|
162
|
+
size: stats.size,
|
|
163
|
+
timestamp: new Date().toISOString()
|
|
164
|
+
});
|
|
165
|
+
} else if (known !== hash) {
|
|
166
|
+
const oldHash = known;
|
|
167
|
+
this.knownFiles.set(relPath, hash);
|
|
168
|
+
await this.config.onEvent({
|
|
169
|
+
type: "modify",
|
|
170
|
+
path: relPath,
|
|
171
|
+
contentHash: hash,
|
|
172
|
+
oldContentHash: oldHash,
|
|
173
|
+
size: stats.size,
|
|
174
|
+
timestamp: new Date().toISOString()
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
if (known) {
|
|
179
|
+
this.knownFiles.delete(relPath);
|
|
180
|
+
await this.config.onEvent({
|
|
181
|
+
type: "delete",
|
|
182
|
+
path: relPath,
|
|
183
|
+
contentHash: known,
|
|
184
|
+
timestamp: new Date().toISOString()
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async walkDir(dir) {
|
|
190
|
+
const results = [];
|
|
191
|
+
try {
|
|
192
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
const fullPath = join(dir, entry.name);
|
|
195
|
+
const relFromRoot = relative(this.config.rootPath, fullPath);
|
|
196
|
+
if (shouldIgnore(relFromRoot, this.config.ignorePatterns))
|
|
197
|
+
continue;
|
|
198
|
+
if (entry.isDirectory()) {
|
|
199
|
+
const sub = await this.walkDir(fullPath);
|
|
200
|
+
results.push(...sub);
|
|
201
|
+
} else if (entry.isFile()) {
|
|
202
|
+
results.push(fullPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {}
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/watcher/ingestion.ts
|
|
211
|
+
import { extname } from "path";
|
|
212
|
+
var EXT_LANGUAGE = {
|
|
213
|
+
".ts": "typescript",
|
|
214
|
+
".tsx": "typescript",
|
|
215
|
+
".js": "javascript",
|
|
216
|
+
".jsx": "javascript",
|
|
217
|
+
".py": "python",
|
|
218
|
+
".rs": "rust",
|
|
219
|
+
".go": "go",
|
|
220
|
+
".rb": "ruby",
|
|
221
|
+
".java": "java",
|
|
222
|
+
".c": "c",
|
|
223
|
+
".cpp": "cpp",
|
|
224
|
+
".h": "c",
|
|
225
|
+
".hpp": "cpp",
|
|
226
|
+
".cs": "csharp",
|
|
227
|
+
".swift": "swift",
|
|
228
|
+
".kt": "kotlin",
|
|
229
|
+
".md": "markdown",
|
|
230
|
+
".json": "json",
|
|
231
|
+
".yaml": "yaml",
|
|
232
|
+
".yml": "yaml",
|
|
233
|
+
".toml": "toml",
|
|
234
|
+
".html": "html",
|
|
235
|
+
".css": "css",
|
|
236
|
+
".scss": "scss",
|
|
237
|
+
".vue": "vue",
|
|
238
|
+
".svelte": "svelte"
|
|
239
|
+
};
|
|
240
|
+
function detectLanguage(filePath) {
|
|
241
|
+
const ext = extname(filePath).toLowerCase();
|
|
242
|
+
return EXT_LANGUAGE[ext];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
class Ingestion {
|
|
246
|
+
agentId;
|
|
247
|
+
lastOpHash;
|
|
248
|
+
onOp;
|
|
249
|
+
constructor(opts) {
|
|
250
|
+
this.agentId = opts.agentId;
|
|
251
|
+
this.lastOpHash = opts.lastOpHash;
|
|
252
|
+
this.onOp = opts.onOp;
|
|
253
|
+
}
|
|
254
|
+
async process(event) {
|
|
255
|
+
let kind;
|
|
256
|
+
switch (event.type) {
|
|
257
|
+
case "add":
|
|
258
|
+
kind = "vcs:fileAdd";
|
|
259
|
+
break;
|
|
260
|
+
case "modify":
|
|
261
|
+
kind = "vcs:fileModify";
|
|
262
|
+
break;
|
|
263
|
+
case "delete":
|
|
264
|
+
kind = "vcs:fileDelete";
|
|
265
|
+
break;
|
|
266
|
+
case "rename":
|
|
267
|
+
kind = "vcs:fileRename";
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
const op = await createVcsOp(kind, {
|
|
271
|
+
agentId: this.agentId,
|
|
272
|
+
previousHash: this.lastOpHash,
|
|
273
|
+
vcs: {
|
|
274
|
+
filePath: event.path,
|
|
275
|
+
oldFilePath: event.oldPath,
|
|
276
|
+
contentHash: event.contentHash,
|
|
277
|
+
oldContentHash: event.oldContentHash,
|
|
278
|
+
size: event.size,
|
|
279
|
+
language: detectLanguage(event.path)
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.lastOpHash = op.hash;
|
|
283
|
+
await this.onOp(op);
|
|
284
|
+
return op;
|
|
285
|
+
}
|
|
286
|
+
async processBatch(events) {
|
|
287
|
+
const ops = [];
|
|
288
|
+
for (const event of events) {
|
|
289
|
+
ops.push(await this.process(event));
|
|
290
|
+
}
|
|
291
|
+
return ops;
|
|
292
|
+
}
|
|
293
|
+
getLastOpHash() {
|
|
294
|
+
return this.lastOpHash;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/engine.ts
|
|
299
|
+
import {
|
|
300
|
+
existsSync,
|
|
301
|
+
mkdirSync,
|
|
302
|
+
readFileSync,
|
|
303
|
+
writeFileSync,
|
|
304
|
+
copyFileSync
|
|
305
|
+
} from "fs";
|
|
306
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
307
|
+
import { join as join2, dirname } from "path";
|
|
308
|
+
|
|
309
|
+
// src/garden/cluster.ts
|
|
310
|
+
var FILE_OP_KINDS = new Set([
|
|
311
|
+
"vcs:fileAdd",
|
|
312
|
+
"vcs:fileModify",
|
|
313
|
+
"vcs:fileDelete",
|
|
314
|
+
"vcs:fileRename"
|
|
315
|
+
]);
|
|
316
|
+
function isFileOp(op) {
|
|
317
|
+
return FILE_OP_KINDS.has(op.kind);
|
|
318
|
+
}
|
|
319
|
+
function extractFiles(ops) {
|
|
320
|
+
const files = new Set;
|
|
321
|
+
for (const op of ops) {
|
|
322
|
+
if (op.vcs?.filePath)
|
|
323
|
+
files.add(op.vcs.filePath);
|
|
324
|
+
if (op.vcs?.oldFilePath)
|
|
325
|
+
files.add(op.vcs.oldFilePath);
|
|
326
|
+
}
|
|
327
|
+
return [...files];
|
|
328
|
+
}
|
|
329
|
+
function generateClusterId(prefix, ops) {
|
|
330
|
+
const hash = ops[0]?.hash?.slice(0, 8) ?? "unknown";
|
|
331
|
+
return `cluster:${prefix}-${hash}`;
|
|
332
|
+
}
|
|
333
|
+
var contextSwitchDetector = {
|
|
334
|
+
name: "context-switch",
|
|
335
|
+
detect(ops, milestonedOpHashes) {
|
|
336
|
+
const fileOps = ops.filter((o) => isFileOp(o) && !milestonedOpHashes.has(o.hash));
|
|
337
|
+
if (fileOps.length < 2)
|
|
338
|
+
return [];
|
|
339
|
+
const groups = [];
|
|
340
|
+
let currentGroup = [];
|
|
341
|
+
let currentFiles = new Set;
|
|
342
|
+
for (const op of fileOps) {
|
|
343
|
+
const opFile = op.vcs?.filePath;
|
|
344
|
+
if (!opFile)
|
|
345
|
+
continue;
|
|
346
|
+
const opDir = opFile.split("/").slice(0, -1).join("/") || ".";
|
|
347
|
+
if (currentGroup.length === 0) {
|
|
348
|
+
currentGroup.push(op);
|
|
349
|
+
currentFiles.add(opDir);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const currentDirs = [...currentFiles];
|
|
353
|
+
const hasOverlap = currentDirs.some((d) => opDir.startsWith(d) || d.startsWith(opDir) || d === opDir);
|
|
354
|
+
if (hasOverlap) {
|
|
355
|
+
currentGroup.push(op);
|
|
356
|
+
currentFiles.add(opDir);
|
|
357
|
+
} else {
|
|
358
|
+
if (currentGroup.length > 0) {
|
|
359
|
+
groups.push(currentGroup);
|
|
360
|
+
}
|
|
361
|
+
currentGroup = [op];
|
|
362
|
+
currentFiles = new Set([opDir]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (currentGroup.length > 0) {
|
|
366
|
+
groups.push(currentGroup);
|
|
367
|
+
}
|
|
368
|
+
const clusters = [];
|
|
369
|
+
for (let i = 0;i < groups.length - 1; i++) {
|
|
370
|
+
const group = groups[i];
|
|
371
|
+
if (group.length < 2)
|
|
372
|
+
continue;
|
|
373
|
+
clusters.push({
|
|
374
|
+
id: generateClusterId("ctx", group),
|
|
375
|
+
ops: group,
|
|
376
|
+
firstOp: group[0].hash,
|
|
377
|
+
lastOp: group[group.length - 1].hash,
|
|
378
|
+
affectedFiles: extractFiles(group),
|
|
379
|
+
affectedSymbols: [],
|
|
380
|
+
estimatedIntent: "",
|
|
381
|
+
createdAt: group[0].timestamp,
|
|
382
|
+
abandonedAt: group[group.length - 1].timestamp,
|
|
383
|
+
status: "abandoned",
|
|
384
|
+
detectedBy: "context-switch"
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return clusters;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var revertDetector = {
|
|
391
|
+
name: "revert",
|
|
392
|
+
detect(ops, milestonedOpHashes) {
|
|
393
|
+
const clusters = [];
|
|
394
|
+
const hashHistory = new Map;
|
|
395
|
+
for (let i = 0;i < ops.length; i++) {
|
|
396
|
+
const op = ops[i];
|
|
397
|
+
if (!isFileOp(op) || !op.vcs?.filePath || !op.vcs?.contentHash)
|
|
398
|
+
continue;
|
|
399
|
+
const filePath = op.vcs.filePath;
|
|
400
|
+
if (!hashHistory.has(filePath)) {
|
|
401
|
+
hashHistory.set(filePath, []);
|
|
402
|
+
}
|
|
403
|
+
const history = hashHistory.get(filePath);
|
|
404
|
+
const currentHash = op.vcs.contentHash;
|
|
405
|
+
const priorIdx = history.findIndex((h) => h.hash === currentHash);
|
|
406
|
+
if (priorIdx >= 0 && priorIdx < history.length - 1) {
|
|
407
|
+
const revertedStartIdx = history[priorIdx + 1].opIdx;
|
|
408
|
+
const revertedEndIdx = history[history.length - 1].opIdx;
|
|
409
|
+
const revertedOps = ops.slice(revertedStartIdx, revertedEndIdx + 1).filter((o) => isFileOp(o) && o.vcs?.filePath === filePath && !milestonedOpHashes.has(o.hash));
|
|
410
|
+
if (revertedOps.length >= 2) {
|
|
411
|
+
clusters.push({
|
|
412
|
+
id: generateClusterId("rev", revertedOps),
|
|
413
|
+
ops: revertedOps,
|
|
414
|
+
firstOp: revertedOps[0].hash,
|
|
415
|
+
lastOp: revertedOps[revertedOps.length - 1].hash,
|
|
416
|
+
affectedFiles: [filePath],
|
|
417
|
+
affectedSymbols: [],
|
|
418
|
+
estimatedIntent: "",
|
|
419
|
+
createdAt: revertedOps[0].timestamp,
|
|
420
|
+
abandonedAt: op.timestamp,
|
|
421
|
+
status: "abandoned",
|
|
422
|
+
detectedBy: "revert"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
history.push({ hash: currentHash, opIdx: i });
|
|
427
|
+
}
|
|
428
|
+
return clusters;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
var staleBranchDetector = {
|
|
432
|
+
name: "stale-branch",
|
|
433
|
+
detect(ops, milestonedOpHashes) {
|
|
434
|
+
const clusters = [];
|
|
435
|
+
const branchOps = new Map;
|
|
436
|
+
let currentBranch = "main";
|
|
437
|
+
for (const op of ops) {
|
|
438
|
+
if (op.kind === "vcs:branchCreate" && op.vcs?.branchName) {
|
|
439
|
+
currentBranch = op.vcs.branchName;
|
|
440
|
+
if (!branchOps.has(currentBranch)) {
|
|
441
|
+
branchOps.set(currentBranch, []);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (isFileOp(op) && !milestonedOpHashes.has(op.hash)) {
|
|
445
|
+
if (!branchOps.has(currentBranch)) {
|
|
446
|
+
branchOps.set(currentBranch, []);
|
|
447
|
+
}
|
|
448
|
+
branchOps.get(currentBranch).push(op);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const [branchName, fileOps] of branchOps) {
|
|
452
|
+
if (branchName === "main")
|
|
453
|
+
continue;
|
|
454
|
+
if (fileOps.length < 2)
|
|
455
|
+
continue;
|
|
456
|
+
const lastOpTime = new Date(fileOps[fileOps.length - 1].timestamp).getTime();
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
const daysSinceLastOp = (now - lastOpTime) / (1000 * 60 * 60 * 24);
|
|
459
|
+
if (daysSinceLastOp > 7) {
|
|
460
|
+
clusters.push({
|
|
461
|
+
id: generateClusterId("stale", fileOps),
|
|
462
|
+
ops: fileOps,
|
|
463
|
+
firstOp: fileOps[0].hash,
|
|
464
|
+
lastOp: fileOps[fileOps.length - 1].hash,
|
|
465
|
+
affectedFiles: extractFiles(fileOps),
|
|
466
|
+
affectedSymbols: [],
|
|
467
|
+
estimatedIntent: "",
|
|
468
|
+
createdAt: fileOps[0].timestamp,
|
|
469
|
+
abandonedAt: fileOps[fileOps.length - 1].timestamp,
|
|
470
|
+
status: "abandoned",
|
|
471
|
+
detectedBy: "stale-branch"
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return clusters;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var defaultDetectors = [
|
|
479
|
+
contextSwitchDetector,
|
|
480
|
+
revertDetector,
|
|
481
|
+
staleBranchDetector
|
|
482
|
+
];
|
|
483
|
+
function detectClusters(ops, milestonedOpHashes, detectors = defaultDetectors) {
|
|
484
|
+
const seen = new Set;
|
|
485
|
+
const results = [];
|
|
486
|
+
for (const detector of detectors) {
|
|
487
|
+
const clusters = detector.detect(ops, milestonedOpHashes);
|
|
488
|
+
for (const cluster of clusters) {
|
|
489
|
+
if (!seen.has(cluster.id)) {
|
|
490
|
+
seen.add(cluster.id);
|
|
491
|
+
results.push(cluster);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
results.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
496
|
+
return results;
|
|
497
|
+
}
|
|
498
|
+
// src/garden/garden.ts
|
|
499
|
+
class IdeaGarden {
|
|
500
|
+
ctx;
|
|
501
|
+
detectors;
|
|
502
|
+
_cache = null;
|
|
503
|
+
_revivedIds = new Set;
|
|
504
|
+
_embedder = null;
|
|
505
|
+
constructor(ctx, detectors) {
|
|
506
|
+
this.ctx = ctx;
|
|
507
|
+
this.detectors = detectors ?? defaultDetectors;
|
|
508
|
+
}
|
|
509
|
+
setEmbedder(embedder) {
|
|
510
|
+
this._embedder = embedder;
|
|
511
|
+
}
|
|
512
|
+
invalidate() {
|
|
513
|
+
this._cache = null;
|
|
514
|
+
}
|
|
515
|
+
listClusters() {
|
|
516
|
+
if (!this._cache) {
|
|
517
|
+
const ops = this.ctx.readAllOps();
|
|
518
|
+
const milestoned = this.ctx.getMilestonedOpHashes();
|
|
519
|
+
this._cache = detectClusters(ops, milestoned, this.detectors);
|
|
520
|
+
for (const cluster of this._cache) {
|
|
521
|
+
if (this._revivedIds.has(cluster.id)) {
|
|
522
|
+
cluster.status = "revived";
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return this._cache;
|
|
527
|
+
}
|
|
528
|
+
getCluster(clusterId) {
|
|
529
|
+
return this.listClusters().find((c) => c.id === clusterId) ?? null;
|
|
530
|
+
}
|
|
531
|
+
search(opts = {}) {
|
|
532
|
+
let clusters = this.listClusters();
|
|
533
|
+
if (opts.status) {
|
|
534
|
+
clusters = clusters.filter((c) => c.status === opts.status);
|
|
535
|
+
}
|
|
536
|
+
if (opts.file) {
|
|
537
|
+
const fileTerm = opts.file.toLowerCase();
|
|
538
|
+
clusters = clusters.filter((c) => c.affectedFiles.some((f) => f.toLowerCase().includes(fileTerm)));
|
|
539
|
+
}
|
|
540
|
+
if (opts.keyword) {
|
|
541
|
+
const kw = opts.keyword.toLowerCase();
|
|
542
|
+
clusters = clusters.filter((c) => {
|
|
543
|
+
if (c.affectedFiles.some((f) => f.toLowerCase().includes(kw)))
|
|
544
|
+
return true;
|
|
545
|
+
if (c.estimatedIntent.toLowerCase().includes(kw))
|
|
546
|
+
return true;
|
|
547
|
+
if (c.ops.some((o) => o.kind.toLowerCase().includes(kw)))
|
|
548
|
+
return true;
|
|
549
|
+
return false;
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (opts.limit && opts.limit > 0) {
|
|
553
|
+
clusters = clusters.slice(0, opts.limit);
|
|
554
|
+
}
|
|
555
|
+
return clusters;
|
|
556
|
+
}
|
|
557
|
+
async semanticSearch(opts = {}) {
|
|
558
|
+
const keywordResults = this.search({ ...opts, limit: undefined });
|
|
559
|
+
if (!this._embedder || opts.semantic === false) {
|
|
560
|
+
const scored2 = keywordResults.map((c) => ({ cluster: c, score: 1 }));
|
|
561
|
+
if (opts.limit && opts.limit > 0)
|
|
562
|
+
return scored2.slice(0, opts.limit);
|
|
563
|
+
return scored2;
|
|
564
|
+
}
|
|
565
|
+
const query = opts.keyword ?? opts.file ?? "";
|
|
566
|
+
if (!query) {
|
|
567
|
+
const scored2 = keywordResults.map((c) => ({ cluster: c, score: 1 }));
|
|
568
|
+
if (opts.limit && opts.limit > 0)
|
|
569
|
+
return scored2.slice(0, opts.limit);
|
|
570
|
+
return scored2;
|
|
571
|
+
}
|
|
572
|
+
const allClusters = this.listClusters();
|
|
573
|
+
const embeddingResults = await this._embedder.search(query, { limit: 50 });
|
|
574
|
+
const fileScores = new Map;
|
|
575
|
+
for (const r of embeddingResults) {
|
|
576
|
+
if (r.chunk.filePath) {
|
|
577
|
+
const existing = fileScores.get(r.chunk.filePath) ?? 0;
|
|
578
|
+
fileScores.set(r.chunk.filePath, Math.max(existing, r.score));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const scored = [];
|
|
582
|
+
const keywordIds = new Set(keywordResults.map((c) => c.id));
|
|
583
|
+
for (const cluster of allClusters) {
|
|
584
|
+
if (opts.status && cluster.status !== opts.status)
|
|
585
|
+
continue;
|
|
586
|
+
if (opts.file) {
|
|
587
|
+
const fileTerm = opts.file.toLowerCase();
|
|
588
|
+
if (!cluster.affectedFiles.some((f) => f.toLowerCase().includes(fileTerm)))
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
let maxScore = 0;
|
|
592
|
+
for (const file of cluster.affectedFiles) {
|
|
593
|
+
const s = fileScores.get(file) ?? 0;
|
|
594
|
+
if (s > maxScore)
|
|
595
|
+
maxScore = s;
|
|
596
|
+
}
|
|
597
|
+
if (keywordIds.has(cluster.id)) {
|
|
598
|
+
maxScore = Math.max(maxScore, 0.5);
|
|
599
|
+
}
|
|
600
|
+
if (maxScore > 0) {
|
|
601
|
+
scored.push({ cluster, score: maxScore });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
scored.sort((a, b) => b.score - a.score);
|
|
605
|
+
if (opts.limit && opts.limit > 0)
|
|
606
|
+
return scored.slice(0, opts.limit);
|
|
607
|
+
return scored;
|
|
608
|
+
}
|
|
609
|
+
revive(clusterId) {
|
|
610
|
+
const cluster = this.getCluster(clusterId);
|
|
611
|
+
if (!cluster)
|
|
612
|
+
return null;
|
|
613
|
+
cluster.status = "revived";
|
|
614
|
+
this._revivedIds.add(clusterId);
|
|
615
|
+
this.invalidate();
|
|
616
|
+
return cluster.ops;
|
|
617
|
+
}
|
|
618
|
+
stats() {
|
|
619
|
+
const clusters = this.listClusters();
|
|
620
|
+
const allFiles = new Set;
|
|
621
|
+
let totalOps = 0;
|
|
622
|
+
for (const c of clusters) {
|
|
623
|
+
totalOps += c.ops.length;
|
|
624
|
+
for (const f of c.affectedFiles)
|
|
625
|
+
allFiles.add(f);
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
total: clusters.length,
|
|
629
|
+
abandoned: clusters.filter((c) => c.status === "abandoned").length,
|
|
630
|
+
draft: clusters.filter((c) => c.status === "draft").length,
|
|
631
|
+
revived: clusters.filter((c) => c.status === "revived").length,
|
|
632
|
+
totalOps,
|
|
633
|
+
totalFiles: allFiles.size
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function buildMilestonedOpHashes(ops) {
|
|
638
|
+
const milestoned = new Set;
|
|
639
|
+
const milestoneOps = ops.filter((o) => o.kind === "vcs:milestoneCreate");
|
|
640
|
+
for (const mOp of milestoneOps) {
|
|
641
|
+
const from = mOp.vcs?.fromOpHash;
|
|
642
|
+
const to = mOp.vcs?.toOpHash;
|
|
643
|
+
if (!from || !to)
|
|
644
|
+
continue;
|
|
645
|
+
const fromIdx = ops.findIndex((o) => o.hash === from);
|
|
646
|
+
const toIdx = ops.findIndex((o) => o.hash === to);
|
|
647
|
+
if (fromIdx >= 0 && toIdx >= 0) {
|
|
648
|
+
for (let i = fromIdx;i <= toIdx; i++) {
|
|
649
|
+
milestoned.add(ops[i].hash);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
milestoned.add(mOp.hash);
|
|
653
|
+
}
|
|
654
|
+
return milestoned;
|
|
655
|
+
}
|
|
656
|
+
// src/semantic/ts-parser.ts
|
|
657
|
+
var typescriptParser = {
|
|
658
|
+
languages: ["typescript", "javascript", "tsx", "jsx"],
|
|
659
|
+
parse(content, filePath) {
|
|
660
|
+
const fileEntityId = `file:${filePath}`;
|
|
661
|
+
const language = detectLanguage2(filePath);
|
|
662
|
+
return {
|
|
663
|
+
fileEntityId,
|
|
664
|
+
filePath,
|
|
665
|
+
language,
|
|
666
|
+
declarations: extractDeclarations(content, filePath),
|
|
667
|
+
imports: extractImports(content),
|
|
668
|
+
exports: extractExports(content)
|
|
669
|
+
};
|
|
670
|
+
},
|
|
671
|
+
diff(oldResult, newResult) {
|
|
672
|
+
return computeSemanticDiff(oldResult, newResult);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
function detectLanguage2(filePath) {
|
|
676
|
+
if (filePath.endsWith(".tsx"))
|
|
677
|
+
return "tsx";
|
|
678
|
+
if (filePath.endsWith(".jsx"))
|
|
679
|
+
return "jsx";
|
|
680
|
+
if (filePath.endsWith(".ts"))
|
|
681
|
+
return "typescript";
|
|
682
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs"))
|
|
683
|
+
return "javascript";
|
|
684
|
+
return "unknown";
|
|
685
|
+
}
|
|
686
|
+
function extractDeclarations(content, filePath) {
|
|
687
|
+
const declarations = [];
|
|
688
|
+
const lines = content.split(`
|
|
689
|
+
`);
|
|
690
|
+
let i = 0;
|
|
691
|
+
while (i < lines.length) {
|
|
692
|
+
const line = lines[i];
|
|
693
|
+
const trimmed = line.trim();
|
|
694
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("import ") || trimmed.startsWith("export default ") || trimmed.startsWith("export {") && !trimmed.includes("class") && !trimmed.includes("function")) {
|
|
695
|
+
i++;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const stripped = trimmed.replace(/^export\s+/, "").replace(/^declare\s+/, "").replace(/^abstract\s+/, "").replace(/^async\s+/, "");
|
|
699
|
+
const result = tryExtractDeclaration(stripped, trimmed, lines, i, filePath);
|
|
700
|
+
if (result) {
|
|
701
|
+
declarations.push(result.entity);
|
|
702
|
+
i = result.endLine + 1;
|
|
703
|
+
} else {
|
|
704
|
+
i++;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return declarations;
|
|
708
|
+
}
|
|
709
|
+
function tryExtractDeclaration(stripped, originalLine, lines, startLine, filePath) {
|
|
710
|
+
let match = stripped.match(/^function\s+(\w+)/);
|
|
711
|
+
if (match) {
|
|
712
|
+
return extractBlock(match[1], "FunctionDef", lines, startLine, filePath);
|
|
713
|
+
}
|
|
714
|
+
match = stripped.match(/^class\s+(\w+)/);
|
|
715
|
+
if (match) {
|
|
716
|
+
return extractBlock(match[1], "ClassDef", lines, startLine, filePath);
|
|
717
|
+
}
|
|
718
|
+
match = stripped.match(/^interface\s+(\w+)/);
|
|
719
|
+
if (match) {
|
|
720
|
+
return extractBlock(match[1], "InterfaceDef", lines, startLine, filePath);
|
|
721
|
+
}
|
|
722
|
+
match = stripped.match(/^type\s+(\w+)/);
|
|
723
|
+
if (match) {
|
|
724
|
+
return extractTypeAlias(match[1], lines, startLine, filePath);
|
|
725
|
+
}
|
|
726
|
+
match = stripped.match(/^enum\s+(\w+)/);
|
|
727
|
+
if (match) {
|
|
728
|
+
return extractBlock(match[1], "EnumDef", lines, startLine, filePath);
|
|
729
|
+
}
|
|
730
|
+
match = stripped.match(/^(?:const|let|var)\s+(\w+)/);
|
|
731
|
+
if (match) {
|
|
732
|
+
return extractVariable(match[1], lines, startLine, filePath);
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
function extractBlock(name, kind, lines, startLine, filePath) {
|
|
737
|
+
let depth = 0;
|
|
738
|
+
let foundOpen = false;
|
|
739
|
+
let endLine = startLine;
|
|
740
|
+
for (let i = startLine;i < lines.length; i++) {
|
|
741
|
+
const line = lines[i];
|
|
742
|
+
for (const ch of line) {
|
|
743
|
+
if (ch === "{") {
|
|
744
|
+
depth++;
|
|
745
|
+
foundOpen = true;
|
|
746
|
+
} else if (ch === "}") {
|
|
747
|
+
depth--;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (foundOpen && depth <= 0) {
|
|
751
|
+
endLine = i;
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
if (i > startLine + 50) {
|
|
755
|
+
endLine = i;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
endLine = i;
|
|
759
|
+
}
|
|
760
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
761
|
+
`);
|
|
762
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
763
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
764
|
+
const endOffset = startOffset + rawText.length;
|
|
765
|
+
const children = kind === "ClassDef" || kind === "InterfaceDef" ? extractClassMembers(rawText, name, filePath) : [];
|
|
766
|
+
return {
|
|
767
|
+
entity: {
|
|
768
|
+
id: makeEntityId(filePath, kind, name),
|
|
769
|
+
kind,
|
|
770
|
+
name,
|
|
771
|
+
scopePath: name,
|
|
772
|
+
span: [startOffset, endOffset],
|
|
773
|
+
rawText,
|
|
774
|
+
signature: normalizeSignature(rawText),
|
|
775
|
+
children
|
|
776
|
+
},
|
|
777
|
+
endLine
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function extractTypeAlias(name, lines, startLine, filePath) {
|
|
781
|
+
let endLine = startLine;
|
|
782
|
+
let depth = 0;
|
|
783
|
+
for (let i = startLine;i < lines.length; i++) {
|
|
784
|
+
const line = lines[i];
|
|
785
|
+
for (const ch of line) {
|
|
786
|
+
if (ch === "{" || ch === "(" || ch === "<")
|
|
787
|
+
depth++;
|
|
788
|
+
else if (ch === "}" || ch === ")" || ch === ">")
|
|
789
|
+
depth--;
|
|
790
|
+
}
|
|
791
|
+
if (line.includes(";") && depth <= 0) {
|
|
792
|
+
endLine = i;
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
endLine = i;
|
|
796
|
+
}
|
|
797
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
798
|
+
`);
|
|
799
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
800
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
801
|
+
return {
|
|
802
|
+
entity: {
|
|
803
|
+
id: makeEntityId(filePath, "TypeAlias", name),
|
|
804
|
+
kind: "TypeAlias",
|
|
805
|
+
name,
|
|
806
|
+
scopePath: name,
|
|
807
|
+
span: [startOffset, startOffset + rawText.length],
|
|
808
|
+
rawText,
|
|
809
|
+
signature: normalizeSignature(rawText),
|
|
810
|
+
children: []
|
|
811
|
+
},
|
|
812
|
+
endLine
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function extractVariable(name, lines, startLine, filePath) {
|
|
816
|
+
let endLine = startLine;
|
|
817
|
+
let depth = 0;
|
|
818
|
+
for (let i = startLine;i < lines.length; i++) {
|
|
819
|
+
const line = lines[i];
|
|
820
|
+
for (const ch of line) {
|
|
821
|
+
if (ch === "{" || ch === "(" || ch === "[")
|
|
822
|
+
depth++;
|
|
823
|
+
else if (ch === "}" || ch === ")" || ch === "]")
|
|
824
|
+
depth--;
|
|
825
|
+
}
|
|
826
|
+
if (depth <= 0 && (line.includes(";") || i > startLine && lines[i + 1]?.trim().match(/^(?:export|const|let|var|function|class|interface|type|enum|import)\s/))) {
|
|
827
|
+
endLine = i;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
endLine = i;
|
|
831
|
+
}
|
|
832
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
833
|
+
`);
|
|
834
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
835
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
836
|
+
return {
|
|
837
|
+
entity: {
|
|
838
|
+
id: makeEntityId(filePath, "VariableDecl", name),
|
|
839
|
+
kind: "VariableDecl",
|
|
840
|
+
name,
|
|
841
|
+
scopePath: name,
|
|
842
|
+
span: [startOffset, startOffset + rawText.length],
|
|
843
|
+
rawText,
|
|
844
|
+
signature: normalizeSignature(rawText),
|
|
845
|
+
children: []
|
|
846
|
+
},
|
|
847
|
+
endLine
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function extractClassMembers(classText, className, filePath) {
|
|
851
|
+
const children = [];
|
|
852
|
+
const lines = classText.split(`
|
|
853
|
+
`);
|
|
854
|
+
for (let i = 1;i < lines.length - 1; i++) {
|
|
855
|
+
const line = lines[i].trim();
|
|
856
|
+
if (!line || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*"))
|
|
857
|
+
continue;
|
|
858
|
+
const methodMatch = line.match(/^(?:(?:public|private|protected|static|async|abstract|readonly)\s+)*(\w+)\s*\(/);
|
|
859
|
+
if (methodMatch && methodMatch[1] !== "if" && methodMatch[1] !== "for" && methodMatch[1] !== "while") {
|
|
860
|
+
const methodName = methodMatch[1];
|
|
861
|
+
const kind = methodName === "constructor" ? "Constructor" : "MethodDef";
|
|
862
|
+
children.push({
|
|
863
|
+
id: makeEntityId(filePath, kind, `${className}.${methodName}`),
|
|
864
|
+
kind,
|
|
865
|
+
name: methodName,
|
|
866
|
+
scopePath: `${className}.${methodName}`,
|
|
867
|
+
span: [0, 0],
|
|
868
|
+
rawText: line,
|
|
869
|
+
signature: normalizeSignature(line),
|
|
870
|
+
children: []
|
|
871
|
+
});
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const propMatch = line.match(/^(?:(?:public|private|protected|static|readonly)\s+)*(\w+)\s*[?:]/);
|
|
875
|
+
if (propMatch) {
|
|
876
|
+
const propName = propMatch[1];
|
|
877
|
+
children.push({
|
|
878
|
+
id: makeEntityId(filePath, "PropertyDef", `${className}.${propName}`),
|
|
879
|
+
kind: "PropertyDef",
|
|
880
|
+
name: propName,
|
|
881
|
+
scopePath: `${className}.${propName}`,
|
|
882
|
+
span: [0, 0],
|
|
883
|
+
rawText: line,
|
|
884
|
+
signature: normalizeSignature(line),
|
|
885
|
+
children: []
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return children;
|
|
890
|
+
}
|
|
891
|
+
function extractImports(content) {
|
|
892
|
+
const imports = [];
|
|
893
|
+
const lines = content.split(`
|
|
894
|
+
`);
|
|
895
|
+
for (let i = 0;i < lines.length; i++) {
|
|
896
|
+
const line = lines[i].trim();
|
|
897
|
+
if (!line.startsWith("import "))
|
|
898
|
+
continue;
|
|
899
|
+
let full = line;
|
|
900
|
+
while (!full.includes(";") && !full.match(/from\s+['"]/) && i + 1 < lines.length) {
|
|
901
|
+
i++;
|
|
902
|
+
full += " " + lines[i].trim();
|
|
903
|
+
}
|
|
904
|
+
if (!full.includes(";") && i + 1 < lines.length) {
|
|
905
|
+
i++;
|
|
906
|
+
full += " " + lines[i].trim();
|
|
907
|
+
}
|
|
908
|
+
const rel = parseImport(full);
|
|
909
|
+
if (rel)
|
|
910
|
+
imports.push(rel);
|
|
911
|
+
}
|
|
912
|
+
return imports;
|
|
913
|
+
}
|
|
914
|
+
function parseImport(text) {
|
|
915
|
+
const namedMatch = text.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
916
|
+
if (namedMatch) {
|
|
917
|
+
const specifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
918
|
+
return {
|
|
919
|
+
source: namedMatch[2],
|
|
920
|
+
specifiers,
|
|
921
|
+
isDefault: false,
|
|
922
|
+
isNamespace: false,
|
|
923
|
+
rawText: text,
|
|
924
|
+
span: [0, text.length]
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const defaultMatch = text.match(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
928
|
+
if (defaultMatch) {
|
|
929
|
+
return {
|
|
930
|
+
source: defaultMatch[2],
|
|
931
|
+
specifiers: [defaultMatch[1]],
|
|
932
|
+
isDefault: true,
|
|
933
|
+
isNamespace: false,
|
|
934
|
+
rawText: text,
|
|
935
|
+
span: [0, text.length]
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const nsMatch = text.match(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
939
|
+
if (nsMatch) {
|
|
940
|
+
return {
|
|
941
|
+
source: nsMatch[2],
|
|
942
|
+
specifiers: [nsMatch[1]],
|
|
943
|
+
isDefault: false,
|
|
944
|
+
isNamespace: true,
|
|
945
|
+
rawText: text,
|
|
946
|
+
span: [0, text.length]
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const sideEffectMatch = text.match(/import\s+['"]([^'"]+)['"]/);
|
|
950
|
+
if (sideEffectMatch) {
|
|
951
|
+
return {
|
|
952
|
+
source: sideEffectMatch[1],
|
|
953
|
+
specifiers: [],
|
|
954
|
+
isDefault: false,
|
|
955
|
+
isNamespace: false,
|
|
956
|
+
rawText: text,
|
|
957
|
+
span: [0, text.length]
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const typeMatch = text.match(/import\s+type\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
961
|
+
if (typeMatch) {
|
|
962
|
+
const specifiers = typeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
963
|
+
return {
|
|
964
|
+
source: typeMatch[2],
|
|
965
|
+
specifiers,
|
|
966
|
+
isDefault: false,
|
|
967
|
+
isNamespace: false,
|
|
968
|
+
rawText: text,
|
|
969
|
+
span: [0, text.length]
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
function extractExports(content) {
|
|
975
|
+
const exports = [];
|
|
976
|
+
const lines = content.split(`
|
|
977
|
+
`);
|
|
978
|
+
for (const line of lines) {
|
|
979
|
+
const trimmed = line.trim();
|
|
980
|
+
if (trimmed.startsWith("export default ")) {
|
|
981
|
+
const nameMatch = trimmed.match(/export default (?:class|function)?\s*(\w+)?/);
|
|
982
|
+
exports.push({
|
|
983
|
+
name: nameMatch?.[1] ?? "default",
|
|
984
|
+
isDefault: true,
|
|
985
|
+
rawText: trimmed,
|
|
986
|
+
span: [0, trimmed.length]
|
|
987
|
+
});
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const reExportMatch = trimmed.match(/export\s+\{([^}]+)\}(?:\s+from\s+['"]([^'"]+)['"])?/);
|
|
991
|
+
if (reExportMatch && !trimmed.match(/export\s+(?:function|class|interface|type|enum|const|let|var)/)) {
|
|
992
|
+
const names = reExportMatch[1].split(",").map((s) => s.trim().split(/\s+as\s+/).pop()).filter(Boolean);
|
|
993
|
+
for (const name of names) {
|
|
994
|
+
exports.push({
|
|
995
|
+
name,
|
|
996
|
+
isDefault: false,
|
|
997
|
+
source: reExportMatch[2],
|
|
998
|
+
rawText: trimmed,
|
|
999
|
+
span: [0, trimmed.length]
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const starMatch = trimmed.match(/export\s+\*\s+from\s+['"]([^'"]+)['"]/);
|
|
1005
|
+
if (starMatch) {
|
|
1006
|
+
exports.push({
|
|
1007
|
+
name: "*",
|
|
1008
|
+
isDefault: false,
|
|
1009
|
+
source: starMatch[1],
|
|
1010
|
+
rawText: trimmed,
|
|
1011
|
+
span: [0, trimmed.length]
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return exports;
|
|
1016
|
+
}
|
|
1017
|
+
function computeSemanticDiff(oldResult, newResult) {
|
|
1018
|
+
const patches = [];
|
|
1019
|
+
const fileId = newResult.fileEntityId;
|
|
1020
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
1021
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
1022
|
+
const oldByName = new Map(oldResult.declarations.map((d) => [d.name, d]));
|
|
1023
|
+
const newByName = new Map(newResult.declarations.map((d) => [d.name, d]));
|
|
1024
|
+
for (const [id, entity] of newDecls) {
|
|
1025
|
+
if (!oldDecls.has(id)) {
|
|
1026
|
+
const oldEntity = findRenamedEntity(entity, oldResult.declarations, newDecls);
|
|
1027
|
+
if (oldEntity) {
|
|
1028
|
+
patches.push({
|
|
1029
|
+
kind: "symbolRename",
|
|
1030
|
+
entityId: oldEntity.id,
|
|
1031
|
+
oldName: oldEntity.name,
|
|
1032
|
+
newName: entity.name
|
|
1033
|
+
});
|
|
1034
|
+
} else {
|
|
1035
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
for (const [id, entity] of oldDecls) {
|
|
1040
|
+
if (!newDecls.has(id)) {
|
|
1041
|
+
const wasRenamed = findRenamedEntity(entity, newResult.declarations, oldDecls);
|
|
1042
|
+
if (!wasRenamed) {
|
|
1043
|
+
patches.push({ kind: "symbolRemove", entityId: id, entityName: entity.name });
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (const [id, newEntity] of newDecls) {
|
|
1048
|
+
const oldEntity = oldDecls.get(id);
|
|
1049
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
1050
|
+
patches.push({
|
|
1051
|
+
kind: "symbolModify",
|
|
1052
|
+
entityId: id,
|
|
1053
|
+
entityName: newEntity.name,
|
|
1054
|
+
oldSignature: oldEntity.signature,
|
|
1055
|
+
newSignature: newEntity.signature,
|
|
1056
|
+
oldRawText: oldEntity.rawText,
|
|
1057
|
+
newRawText: newEntity.rawText
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
1062
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
1063
|
+
for (const [source, imp] of newImports) {
|
|
1064
|
+
const oldImp = oldImports.get(source);
|
|
1065
|
+
if (!oldImp) {
|
|
1066
|
+
patches.push({
|
|
1067
|
+
kind: "importAdd",
|
|
1068
|
+
fileId,
|
|
1069
|
+
source,
|
|
1070
|
+
specifiers: imp.specifiers,
|
|
1071
|
+
rawText: imp.rawText
|
|
1072
|
+
});
|
|
1073
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
1074
|
+
patches.push({
|
|
1075
|
+
kind: "importModify",
|
|
1076
|
+
fileId,
|
|
1077
|
+
source,
|
|
1078
|
+
oldSpecifiers: oldImp.specifiers,
|
|
1079
|
+
newSpecifiers: imp.specifiers
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
for (const [source] of oldImports) {
|
|
1084
|
+
if (!newImports.has(source)) {
|
|
1085
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
1089
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
1090
|
+
for (const [name, exp] of newExports) {
|
|
1091
|
+
if (!oldExports.has(name)) {
|
|
1092
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
for (const [name] of oldExports) {
|
|
1096
|
+
if (!newExports.has(name)) {
|
|
1097
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return patches;
|
|
1101
|
+
}
|
|
1102
|
+
function findRenamedEntity(entity, candidates, existingIds) {
|
|
1103
|
+
for (const candidate of candidates) {
|
|
1104
|
+
if (candidate.kind !== entity.kind)
|
|
1105
|
+
continue;
|
|
1106
|
+
if (candidate.name === entity.name)
|
|
1107
|
+
continue;
|
|
1108
|
+
if (existingIds.has(candidate.id))
|
|
1109
|
+
continue;
|
|
1110
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
1111
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
1112
|
+
if (normalizedOld === normalizedNew) {
|
|
1113
|
+
return candidate;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
function makeEntityId(filePath, kind, name) {
|
|
1119
|
+
return `${kind}:${filePath}:${name}`;
|
|
1120
|
+
}
|
|
1121
|
+
function normalizeSignature(text) {
|
|
1122
|
+
return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/;\s*$/, "").trim();
|
|
1123
|
+
}
|
|
1124
|
+
// src/semantic/python-parser.ts
|
|
1125
|
+
var pythonParser = {
|
|
1126
|
+
languages: ["python"],
|
|
1127
|
+
parse(content, filePath) {
|
|
1128
|
+
const fileEntityId = `file:${filePath}`;
|
|
1129
|
+
return {
|
|
1130
|
+
fileEntityId,
|
|
1131
|
+
filePath,
|
|
1132
|
+
language: "python",
|
|
1133
|
+
declarations: extractDeclarations2(content, filePath),
|
|
1134
|
+
imports: extractImports2(content),
|
|
1135
|
+
exports: extractExports2(content)
|
|
1136
|
+
};
|
|
1137
|
+
},
|
|
1138
|
+
diff(oldResult, newResult) {
|
|
1139
|
+
return computeSemanticDiff2(oldResult, newResult);
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
function extractDeclarations2(content, filePath) {
|
|
1143
|
+
const declarations = [];
|
|
1144
|
+
const lines = content.split(`
|
|
1145
|
+
`);
|
|
1146
|
+
let i = 0;
|
|
1147
|
+
while (i < lines.length) {
|
|
1148
|
+
const line = lines[i];
|
|
1149
|
+
const trimmed = line.trim();
|
|
1150
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
|
|
1151
|
+
i++;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (line.length > 0 && line[0] === " " || line[0] === "\t") {
|
|
1155
|
+
i++;
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const decorators = [];
|
|
1159
|
+
const decoratorStart = i;
|
|
1160
|
+
while (i < lines.length && lines[i].trim().startsWith("@")) {
|
|
1161
|
+
decorators.push(lines[i].trim());
|
|
1162
|
+
i++;
|
|
1163
|
+
}
|
|
1164
|
+
if (i >= lines.length)
|
|
1165
|
+
break;
|
|
1166
|
+
const declLine = lines[i].trim();
|
|
1167
|
+
let match = declLine.match(/^class\s+(\w+)/);
|
|
1168
|
+
if (match) {
|
|
1169
|
+
const result = extractIndentedBlock(match[1], "ClassDef", lines, decorators.length > 0 ? decoratorStart : i, i, filePath, decorators);
|
|
1170
|
+
declarations.push(result.entity);
|
|
1171
|
+
i = result.endLine + 1;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
match = declLine.match(/^async\s+def\s+(\w+)/);
|
|
1175
|
+
if (match) {
|
|
1176
|
+
const result = extractIndentedBlock(match[1], "FunctionDef", lines, decorators.length > 0 ? decoratorStart : i, i, filePath, decorators);
|
|
1177
|
+
declarations.push(result.entity);
|
|
1178
|
+
i = result.endLine + 1;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
match = declLine.match(/^def\s+(\w+)/);
|
|
1182
|
+
if (match) {
|
|
1183
|
+
const result = extractIndentedBlock(match[1], "FunctionDef", lines, decorators.length > 0 ? decoratorStart : i, i, filePath, decorators);
|
|
1184
|
+
declarations.push(result.entity);
|
|
1185
|
+
i = result.endLine + 1;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
match = declLine.match(/^([A-Za-z_]\w*)\s*(?::\s*\w[^=]*)?\s*=/);
|
|
1189
|
+
if (match && match[1] !== "__all__" && !declLine.startsWith("import ") && !declLine.startsWith("from ")) {
|
|
1190
|
+
const result = extractAssignment(match[1], lines, i, filePath);
|
|
1191
|
+
if (decorators.length === 0) {
|
|
1192
|
+
declarations.push(result.entity);
|
|
1193
|
+
}
|
|
1194
|
+
i = result.endLine + 1;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (decorators.length > 0) {
|
|
1198
|
+
i++;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
i++;
|
|
1202
|
+
}
|
|
1203
|
+
return declarations;
|
|
1204
|
+
}
|
|
1205
|
+
function extractIndentedBlock(name, kind, lines, startLine, defLine, filePath, decorators) {
|
|
1206
|
+
let headerEnd = defLine;
|
|
1207
|
+
while (headerEnd < lines.length && !lines[headerEnd].includes(":")) {
|
|
1208
|
+
headerEnd++;
|
|
1209
|
+
}
|
|
1210
|
+
let bodyIndent = -1;
|
|
1211
|
+
let endLine = headerEnd;
|
|
1212
|
+
for (let i = headerEnd + 1;i < lines.length; i++) {
|
|
1213
|
+
const line = lines[i];
|
|
1214
|
+
const trimmed = line.trim();
|
|
1215
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1216
|
+
endLine = i;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
const indent = line.length - line.trimStart().length;
|
|
1220
|
+
if (bodyIndent < 0) {
|
|
1221
|
+
bodyIndent = indent;
|
|
1222
|
+
endLine = i;
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (indent >= bodyIndent) {
|
|
1226
|
+
endLine = i;
|
|
1227
|
+
} else {
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
while (endLine > headerEnd && lines[endLine].trim() === "") {
|
|
1232
|
+
endLine--;
|
|
1233
|
+
}
|
|
1234
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
1235
|
+
`);
|
|
1236
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
1237
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
1238
|
+
const children = kind === "ClassDef" ? extractClassMembers2(lines, headerEnd, endLine, bodyIndent, name, filePath) : [];
|
|
1239
|
+
return {
|
|
1240
|
+
entity: {
|
|
1241
|
+
id: makeEntityId2(filePath, kind, name),
|
|
1242
|
+
kind,
|
|
1243
|
+
name,
|
|
1244
|
+
scopePath: name,
|
|
1245
|
+
span: [startOffset, startOffset + rawText.length],
|
|
1246
|
+
rawText,
|
|
1247
|
+
signature: normalizeSignature2(rawText),
|
|
1248
|
+
children
|
|
1249
|
+
},
|
|
1250
|
+
endLine
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function extractAssignment(name, lines, startLine, filePath) {
|
|
1254
|
+
let endLine = startLine;
|
|
1255
|
+
let depth = 0;
|
|
1256
|
+
for (let i = startLine;i < lines.length; i++) {
|
|
1257
|
+
const line = lines[i];
|
|
1258
|
+
for (const ch of line) {
|
|
1259
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
1260
|
+
depth++;
|
|
1261
|
+
else if (ch === ")" || ch === "]" || ch === "}")
|
|
1262
|
+
depth--;
|
|
1263
|
+
}
|
|
1264
|
+
endLine = i;
|
|
1265
|
+
if (depth <= 0 && i > startLine)
|
|
1266
|
+
break;
|
|
1267
|
+
if (depth <= 0 && i === startLine) {
|
|
1268
|
+
if (i + 1 < lines.length) {
|
|
1269
|
+
const next = lines[i + 1];
|
|
1270
|
+
if (!next.trim() || !next.startsWith(" ") && !next.startsWith("\t")) {
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
1279
|
+
`);
|
|
1280
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
1281
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
1282
|
+
return {
|
|
1283
|
+
entity: {
|
|
1284
|
+
id: makeEntityId2(filePath, "VariableDecl", name),
|
|
1285
|
+
kind: "VariableDecl",
|
|
1286
|
+
name,
|
|
1287
|
+
scopePath: name,
|
|
1288
|
+
span: [startOffset, startOffset + rawText.length],
|
|
1289
|
+
rawText,
|
|
1290
|
+
signature: normalizeSignature2(rawText),
|
|
1291
|
+
children: []
|
|
1292
|
+
},
|
|
1293
|
+
endLine
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function extractClassMembers2(lines, headerEnd, blockEnd, bodyIndent, className, filePath) {
|
|
1297
|
+
const children = [];
|
|
1298
|
+
if (bodyIndent < 0)
|
|
1299
|
+
return children;
|
|
1300
|
+
for (let i = headerEnd + 1;i <= blockEnd; i++) {
|
|
1301
|
+
const line = lines[i];
|
|
1302
|
+
const trimmed = line.trim();
|
|
1303
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith('"""') || trimmed.startsWith("'''"))
|
|
1304
|
+
continue;
|
|
1305
|
+
const indent = line.length - line.trimStart().length;
|
|
1306
|
+
if (indent !== bodyIndent)
|
|
1307
|
+
continue;
|
|
1308
|
+
if (trimmed.startsWith("@"))
|
|
1309
|
+
continue;
|
|
1310
|
+
let match = trimmed.match(/^(?:async\s+)?def\s+(\w+)/);
|
|
1311
|
+
if (match) {
|
|
1312
|
+
const methodName = match[1];
|
|
1313
|
+
const kind = methodName === "__init__" ? "Constructor" : "MethodDef";
|
|
1314
|
+
children.push({
|
|
1315
|
+
id: makeEntityId2(filePath, kind, `${className}.${methodName}`),
|
|
1316
|
+
kind,
|
|
1317
|
+
name: methodName,
|
|
1318
|
+
scopePath: `${className}.${methodName}`,
|
|
1319
|
+
span: [0, 0],
|
|
1320
|
+
rawText: trimmed,
|
|
1321
|
+
signature: normalizeSignature2(trimmed),
|
|
1322
|
+
children: []
|
|
1323
|
+
});
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
match = trimmed.match(/^(\w+)\s*(?::\s*\S[^=]*)?\s*=/);
|
|
1327
|
+
if (match) {
|
|
1328
|
+
children.push({
|
|
1329
|
+
id: makeEntityId2(filePath, "PropertyDef", `${className}.${match[1]}`),
|
|
1330
|
+
kind: "PropertyDef",
|
|
1331
|
+
name: match[1],
|
|
1332
|
+
scopePath: `${className}.${match[1]}`,
|
|
1333
|
+
span: [0, 0],
|
|
1334
|
+
rawText: trimmed,
|
|
1335
|
+
signature: normalizeSignature2(trimmed),
|
|
1336
|
+
children: []
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return children;
|
|
1341
|
+
}
|
|
1342
|
+
function extractImports2(content) {
|
|
1343
|
+
const imports = [];
|
|
1344
|
+
const lines = content.split(`
|
|
1345
|
+
`);
|
|
1346
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1347
|
+
const trimmed = lines[i].trim();
|
|
1348
|
+
let match = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
1349
|
+
if (match) {
|
|
1350
|
+
let specText = match[2];
|
|
1351
|
+
if (specText.includes("(") && !specText.includes(")")) {
|
|
1352
|
+
while (i + 1 < lines.length && !specText.includes(")")) {
|
|
1353
|
+
i++;
|
|
1354
|
+
specText += " " + lines[i].trim();
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
specText = specText.replace(/[()]/g, "").replace(/#.*$/, "");
|
|
1358
|
+
const specifiers = specText.split(",").map((s) => s.trim().split(/\s+as\s+/).pop()).filter(Boolean);
|
|
1359
|
+
const isWildcard = specifiers.includes("*");
|
|
1360
|
+
imports.push({
|
|
1361
|
+
source: match[1],
|
|
1362
|
+
specifiers: isWildcard ? ["*"] : specifiers,
|
|
1363
|
+
isDefault: false,
|
|
1364
|
+
isNamespace: isWildcard,
|
|
1365
|
+
rawText: trimmed,
|
|
1366
|
+
span: [0, trimmed.length]
|
|
1367
|
+
});
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
match = trimmed.match(/^import\s+([\w.,\s]+)/);
|
|
1371
|
+
if (match) {
|
|
1372
|
+
const modules = match[1].split(",").map((m) => m.trim());
|
|
1373
|
+
for (const mod of modules) {
|
|
1374
|
+
const parts = mod.split(/\s+as\s+/);
|
|
1375
|
+
const source = parts[0].trim();
|
|
1376
|
+
const alias = parts.length > 1 ? parts[1].trim() : source;
|
|
1377
|
+
imports.push({
|
|
1378
|
+
source,
|
|
1379
|
+
specifiers: [alias],
|
|
1380
|
+
isDefault: true,
|
|
1381
|
+
isNamespace: false,
|
|
1382
|
+
rawText: trimmed,
|
|
1383
|
+
span: [0, trimmed.length]
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return imports;
|
|
1389
|
+
}
|
|
1390
|
+
function extractExports2(content) {
|
|
1391
|
+
const exports = [];
|
|
1392
|
+
const allMatch = content.match(/__all__\s*=\s*\[([^\]]*)\]/s);
|
|
1393
|
+
if (allMatch) {
|
|
1394
|
+
const names = allMatch[1].replace(/['"]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1395
|
+
for (const name of names) {
|
|
1396
|
+
exports.push({
|
|
1397
|
+
name,
|
|
1398
|
+
isDefault: false,
|
|
1399
|
+
rawText: `__all__: ${name}`,
|
|
1400
|
+
span: [0, 0]
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return exports;
|
|
1405
|
+
}
|
|
1406
|
+
function computeSemanticDiff2(oldResult, newResult) {
|
|
1407
|
+
const patches = [];
|
|
1408
|
+
const fileId = newResult.fileEntityId;
|
|
1409
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
1410
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
1411
|
+
for (const [id, entity] of newDecls) {
|
|
1412
|
+
if (!oldDecls.has(id)) {
|
|
1413
|
+
const oldEntity = findRenamedEntity2(entity, oldResult.declarations, newDecls);
|
|
1414
|
+
if (oldEntity) {
|
|
1415
|
+
patches.push({
|
|
1416
|
+
kind: "symbolRename",
|
|
1417
|
+
entityId: oldEntity.id,
|
|
1418
|
+
oldName: oldEntity.name,
|
|
1419
|
+
newName: entity.name
|
|
1420
|
+
});
|
|
1421
|
+
} else {
|
|
1422
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
for (const [id, entity] of oldDecls) {
|
|
1427
|
+
if (!newDecls.has(id)) {
|
|
1428
|
+
const wasRenamed = findRenamedEntity2(entity, newResult.declarations, oldDecls);
|
|
1429
|
+
if (!wasRenamed) {
|
|
1430
|
+
patches.push({
|
|
1431
|
+
kind: "symbolRemove",
|
|
1432
|
+
entityId: id,
|
|
1433
|
+
entityName: entity.name
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
for (const [id, newEntity] of newDecls) {
|
|
1439
|
+
const oldEntity = oldDecls.get(id);
|
|
1440
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
1441
|
+
patches.push({
|
|
1442
|
+
kind: "symbolModify",
|
|
1443
|
+
entityId: id,
|
|
1444
|
+
entityName: newEntity.name,
|
|
1445
|
+
oldSignature: oldEntity.signature,
|
|
1446
|
+
newSignature: newEntity.signature,
|
|
1447
|
+
oldRawText: oldEntity.rawText,
|
|
1448
|
+
newRawText: newEntity.rawText
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
1453
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
1454
|
+
for (const [source, imp] of newImports) {
|
|
1455
|
+
const oldImp = oldImports.get(source);
|
|
1456
|
+
if (!oldImp) {
|
|
1457
|
+
patches.push({
|
|
1458
|
+
kind: "importAdd",
|
|
1459
|
+
fileId,
|
|
1460
|
+
source,
|
|
1461
|
+
specifiers: imp.specifiers,
|
|
1462
|
+
rawText: imp.rawText
|
|
1463
|
+
});
|
|
1464
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
1465
|
+
patches.push({
|
|
1466
|
+
kind: "importModify",
|
|
1467
|
+
fileId,
|
|
1468
|
+
source,
|
|
1469
|
+
oldSpecifiers: oldImp.specifiers,
|
|
1470
|
+
newSpecifiers: imp.specifiers
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
for (const [source] of oldImports) {
|
|
1475
|
+
if (!newImports.has(source)) {
|
|
1476
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
1480
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
1481
|
+
for (const [name, exp] of newExports) {
|
|
1482
|
+
if (!oldExports.has(name)) {
|
|
1483
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
for (const [name] of oldExports) {
|
|
1487
|
+
if (!newExports.has(name)) {
|
|
1488
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return patches;
|
|
1492
|
+
}
|
|
1493
|
+
function findRenamedEntity2(entity, candidates, existingIds) {
|
|
1494
|
+
for (const candidate of candidates) {
|
|
1495
|
+
if (candidate.kind !== entity.kind)
|
|
1496
|
+
continue;
|
|
1497
|
+
if (candidate.name === entity.name)
|
|
1498
|
+
continue;
|
|
1499
|
+
if (existingIds.has(candidate.id))
|
|
1500
|
+
continue;
|
|
1501
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
1502
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
1503
|
+
if (normalizedOld === normalizedNew) {
|
|
1504
|
+
return candidate;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
function makeEntityId2(filePath, kind, name) {
|
|
1510
|
+
return `${kind}:${filePath}:${name}`;
|
|
1511
|
+
}
|
|
1512
|
+
function normalizeSignature2(text) {
|
|
1513
|
+
return text.replace(/#[^\n]*/g, "").replace(/"""[\s\S]*?"""/g, "").replace(/'''[\s\S]*?'''/g, "").replace(/\s+/g, " ").trim();
|
|
1514
|
+
}
|
|
1515
|
+
// src/semantic/go-parser.ts
|
|
1516
|
+
var goParser = {
|
|
1517
|
+
languages: ["go"],
|
|
1518
|
+
parse(content, filePath) {
|
|
1519
|
+
const fileEntityId = `file:${filePath}`;
|
|
1520
|
+
return {
|
|
1521
|
+
fileEntityId,
|
|
1522
|
+
filePath,
|
|
1523
|
+
language: "go",
|
|
1524
|
+
declarations: extractDeclarations3(content, filePath),
|
|
1525
|
+
imports: extractImports3(content),
|
|
1526
|
+
exports: extractExports3(content, filePath)
|
|
1527
|
+
};
|
|
1528
|
+
},
|
|
1529
|
+
diff(oldResult, newResult) {
|
|
1530
|
+
return computeSemanticDiff3(oldResult, newResult);
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
function extractDeclarations3(content, filePath) {
|
|
1534
|
+
const declarations = [];
|
|
1535
|
+
const lines = content.split(`
|
|
1536
|
+
`);
|
|
1537
|
+
let i = 0;
|
|
1538
|
+
while (i < lines.length) {
|
|
1539
|
+
const line = lines[i];
|
|
1540
|
+
const trimmed = line.trim();
|
|
1541
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("package ") || trimmed.startsWith("import ")) {
|
|
1542
|
+
i++;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (trimmed === "import (") {
|
|
1546
|
+
while (i < lines.length && !lines[i].trim().startsWith(")")) {
|
|
1547
|
+
i++;
|
|
1548
|
+
}
|
|
1549
|
+
i++;
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
let match = trimmed.match(/^func\s+(\w+)\s*\(/);
|
|
1553
|
+
if (match) {
|
|
1554
|
+
const result = extractBraceBlock(match[1], "FunctionDef", lines, i, filePath);
|
|
1555
|
+
declarations.push(result.entity);
|
|
1556
|
+
i = result.endLine + 1;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
match = trimmed.match(/^func\s+\([^)]+\)\s+(\w+)\s*\(/);
|
|
1560
|
+
if (match) {
|
|
1561
|
+
const result = extractBraceBlock(match[1], "MethodDef", lines, i, filePath);
|
|
1562
|
+
declarations.push(result.entity);
|
|
1563
|
+
i = result.endLine + 1;
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
match = trimmed.match(/^type\s+(\w+)\s+struct\b/);
|
|
1567
|
+
if (match) {
|
|
1568
|
+
const result = extractBraceBlock(match[1], "ClassDef", lines, i, filePath);
|
|
1569
|
+
result.entity.children = extractStructFields(lines, i, result.endLine, match[1], filePath);
|
|
1570
|
+
declarations.push(result.entity);
|
|
1571
|
+
i = result.endLine + 1;
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
match = trimmed.match(/^type\s+(\w+)\s+interface\b/);
|
|
1575
|
+
if (match) {
|
|
1576
|
+
const result = extractBraceBlock(match[1], "InterfaceDef", lines, i, filePath);
|
|
1577
|
+
result.entity.children = extractInterfaceMethods(lines, i, result.endLine, match[1], filePath);
|
|
1578
|
+
declarations.push(result.entity);
|
|
1579
|
+
i = result.endLine + 1;
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
match = trimmed.match(/^type\s+(\w+)\s+/);
|
|
1583
|
+
if (match) {
|
|
1584
|
+
const result = extractSingleOrBlock(match[1], "TypeAlias", lines, i, filePath);
|
|
1585
|
+
declarations.push(result.entity);
|
|
1586
|
+
i = result.endLine + 1;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (trimmed.startsWith("const ") || trimmed === "const (") {
|
|
1590
|
+
if (trimmed === "const (") {
|
|
1591
|
+
const endLine = findClosingParen(lines, i);
|
|
1592
|
+
const rawText = lines.slice(i, endLine + 1).join(`
|
|
1593
|
+
`);
|
|
1594
|
+
const consts = extractConstBlock(lines, i, endLine, filePath);
|
|
1595
|
+
declarations.push(...consts);
|
|
1596
|
+
i = endLine + 1;
|
|
1597
|
+
} else {
|
|
1598
|
+
match = trimmed.match(/^const\s+(\w+)/);
|
|
1599
|
+
if (match) {
|
|
1600
|
+
const result = extractSingleLine(match[1], "VariableDecl", lines, i, filePath);
|
|
1601
|
+
declarations.push(result.entity);
|
|
1602
|
+
i = result.endLine + 1;
|
|
1603
|
+
} else {
|
|
1604
|
+
i++;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (trimmed.startsWith("var ") || trimmed === "var (") {
|
|
1610
|
+
if (trimmed === "var (") {
|
|
1611
|
+
const endLine = findClosingParen(lines, i);
|
|
1612
|
+
const vars = extractVarBlock(lines, i, endLine, filePath);
|
|
1613
|
+
declarations.push(...vars);
|
|
1614
|
+
i = endLine + 1;
|
|
1615
|
+
} else {
|
|
1616
|
+
match = trimmed.match(/^var\s+(\w+)/);
|
|
1617
|
+
if (match) {
|
|
1618
|
+
const result = extractSingleLine(match[1], "VariableDecl", lines, i, filePath);
|
|
1619
|
+
declarations.push(result.entity);
|
|
1620
|
+
i = result.endLine + 1;
|
|
1621
|
+
} else {
|
|
1622
|
+
i++;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
i++;
|
|
1628
|
+
}
|
|
1629
|
+
return declarations;
|
|
1630
|
+
}
|
|
1631
|
+
function extractBraceBlock(name, kind, lines, startLine, filePath) {
|
|
1632
|
+
let depth = 0;
|
|
1633
|
+
let foundOpen = false;
|
|
1634
|
+
let endLine = startLine;
|
|
1635
|
+
for (let i = startLine;i < lines.length; i++) {
|
|
1636
|
+
for (const ch of lines[i]) {
|
|
1637
|
+
if (ch === "{") {
|
|
1638
|
+
depth++;
|
|
1639
|
+
foundOpen = true;
|
|
1640
|
+
} else if (ch === "}") {
|
|
1641
|
+
depth--;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (foundOpen && depth <= 0) {
|
|
1645
|
+
endLine = i;
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
if (i > startLine + 200) {
|
|
1649
|
+
endLine = i;
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
endLine = i;
|
|
1653
|
+
}
|
|
1654
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
1655
|
+
`);
|
|
1656
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
1657
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
1658
|
+
return {
|
|
1659
|
+
entity: {
|
|
1660
|
+
id: makeEntityId3(filePath, kind, name),
|
|
1661
|
+
kind,
|
|
1662
|
+
name,
|
|
1663
|
+
scopePath: name,
|
|
1664
|
+
span: [startOffset, startOffset + rawText.length],
|
|
1665
|
+
rawText,
|
|
1666
|
+
signature: normalizeSignature3(rawText),
|
|
1667
|
+
children: []
|
|
1668
|
+
},
|
|
1669
|
+
endLine
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function extractSingleOrBlock(name, kind, lines, startLine, filePath) {
|
|
1673
|
+
const trimmed = lines[startLine].trim();
|
|
1674
|
+
if (trimmed.includes("{")) {
|
|
1675
|
+
return extractBraceBlock(name, kind, lines, startLine, filePath);
|
|
1676
|
+
}
|
|
1677
|
+
return extractSingleLine(name, kind, lines, startLine, filePath);
|
|
1678
|
+
}
|
|
1679
|
+
function extractSingleLine(name, kind, lines, startLine, filePath) {
|
|
1680
|
+
const rawText = lines[startLine];
|
|
1681
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
1682
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
1683
|
+
return {
|
|
1684
|
+
entity: {
|
|
1685
|
+
id: makeEntityId3(filePath, kind, name),
|
|
1686
|
+
kind,
|
|
1687
|
+
name,
|
|
1688
|
+
scopePath: name,
|
|
1689
|
+
span: [startOffset, startOffset + rawText.length],
|
|
1690
|
+
rawText,
|
|
1691
|
+
signature: normalizeSignature3(rawText),
|
|
1692
|
+
children: []
|
|
1693
|
+
},
|
|
1694
|
+
endLine: startLine
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
function findClosingParen(lines, startLine) {
|
|
1698
|
+
for (let i = startLine + 1;i < lines.length; i++) {
|
|
1699
|
+
if (lines[i].trim() === ")")
|
|
1700
|
+
return i;
|
|
1701
|
+
}
|
|
1702
|
+
return lines.length - 1;
|
|
1703
|
+
}
|
|
1704
|
+
function extractStructFields(lines, startLine, endLine, structName, filePath) {
|
|
1705
|
+
const children = [];
|
|
1706
|
+
for (let i = startLine + 1;i < endLine; i++) {
|
|
1707
|
+
const trimmed = lines[i].trim();
|
|
1708
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed === "{" || trimmed === "}")
|
|
1709
|
+
continue;
|
|
1710
|
+
const match = trimmed.match(/^(\w+)\s+/);
|
|
1711
|
+
if (match) {
|
|
1712
|
+
children.push({
|
|
1713
|
+
id: makeEntityId3(filePath, "PropertyDef", `${structName}.${match[1]}`),
|
|
1714
|
+
kind: "PropertyDef",
|
|
1715
|
+
name: match[1],
|
|
1716
|
+
scopePath: `${structName}.${match[1]}`,
|
|
1717
|
+
span: [0, 0],
|
|
1718
|
+
rawText: trimmed,
|
|
1719
|
+
signature: normalizeSignature3(trimmed),
|
|
1720
|
+
children: []
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return children;
|
|
1725
|
+
}
|
|
1726
|
+
function extractInterfaceMethods(lines, startLine, endLine, ifaceName, filePath) {
|
|
1727
|
+
const children = [];
|
|
1728
|
+
for (let i = startLine + 1;i < endLine; i++) {
|
|
1729
|
+
const trimmed = lines[i].trim();
|
|
1730
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed === "{" || trimmed === "}")
|
|
1731
|
+
continue;
|
|
1732
|
+
const match = trimmed.match(/^(\w+)\s*\(/);
|
|
1733
|
+
if (match) {
|
|
1734
|
+
children.push({
|
|
1735
|
+
id: makeEntityId3(filePath, "MethodDef", `${ifaceName}.${match[1]}`),
|
|
1736
|
+
kind: "MethodDef",
|
|
1737
|
+
name: match[1],
|
|
1738
|
+
scopePath: `${ifaceName}.${match[1]}`,
|
|
1739
|
+
span: [0, 0],
|
|
1740
|
+
rawText: trimmed,
|
|
1741
|
+
signature: normalizeSignature3(trimmed),
|
|
1742
|
+
children: []
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return children;
|
|
1747
|
+
}
|
|
1748
|
+
function extractConstBlock(lines, startLine, endLine, filePath) {
|
|
1749
|
+
const entities = [];
|
|
1750
|
+
for (let i = startLine + 1;i < endLine; i++) {
|
|
1751
|
+
const trimmed = lines[i].trim();
|
|
1752
|
+
if (!trimmed || trimmed.startsWith("//"))
|
|
1753
|
+
continue;
|
|
1754
|
+
const match = trimmed.match(/^(\w+)/);
|
|
1755
|
+
if (match) {
|
|
1756
|
+
entities.push({
|
|
1757
|
+
id: makeEntityId3(filePath, "VariableDecl", match[1]),
|
|
1758
|
+
kind: "VariableDecl",
|
|
1759
|
+
name: match[1],
|
|
1760
|
+
scopePath: match[1],
|
|
1761
|
+
span: [0, 0],
|
|
1762
|
+
rawText: trimmed,
|
|
1763
|
+
signature: normalizeSignature3(trimmed),
|
|
1764
|
+
children: []
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return entities;
|
|
1769
|
+
}
|
|
1770
|
+
function extractVarBlock(lines, startLine, endLine, filePath) {
|
|
1771
|
+
return extractConstBlock(lines, startLine, endLine, filePath);
|
|
1772
|
+
}
|
|
1773
|
+
function extractImports3(content) {
|
|
1774
|
+
const imports = [];
|
|
1775
|
+
const lines = content.split(`
|
|
1776
|
+
`);
|
|
1777
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1778
|
+
const trimmed = lines[i].trim();
|
|
1779
|
+
let match = trimmed.match(/^import\s+"([^"]+)"/);
|
|
1780
|
+
if (match) {
|
|
1781
|
+
imports.push({
|
|
1782
|
+
source: match[1],
|
|
1783
|
+
specifiers: [match[1].split("/").pop()],
|
|
1784
|
+
isDefault: true,
|
|
1785
|
+
isNamespace: false,
|
|
1786
|
+
rawText: trimmed,
|
|
1787
|
+
span: [0, trimmed.length]
|
|
1788
|
+
});
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
if (trimmed === "import (") {
|
|
1792
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
1793
|
+
const impLine = lines[j].trim();
|
|
1794
|
+
if (impLine === ")") {
|
|
1795
|
+
i = j;
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
if (!impLine || impLine.startsWith("//"))
|
|
1799
|
+
continue;
|
|
1800
|
+
const aliasMatch = impLine.match(/^(\w+)\s+"([^"]+)"/);
|
|
1801
|
+
if (aliasMatch) {
|
|
1802
|
+
imports.push({
|
|
1803
|
+
source: aliasMatch[2],
|
|
1804
|
+
specifiers: [aliasMatch[1]],
|
|
1805
|
+
isDefault: true,
|
|
1806
|
+
isNamespace: false,
|
|
1807
|
+
rawText: impLine,
|
|
1808
|
+
span: [0, impLine.length]
|
|
1809
|
+
});
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
const dotMatch = impLine.match(/^\.\s+"([^"]+)"/);
|
|
1813
|
+
if (dotMatch) {
|
|
1814
|
+
imports.push({
|
|
1815
|
+
source: dotMatch[1],
|
|
1816
|
+
specifiers: ["*"],
|
|
1817
|
+
isDefault: false,
|
|
1818
|
+
isNamespace: true,
|
|
1819
|
+
rawText: impLine,
|
|
1820
|
+
span: [0, impLine.length]
|
|
1821
|
+
});
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const stdMatch = impLine.match(/^"([^"]+)"/);
|
|
1825
|
+
if (stdMatch) {
|
|
1826
|
+
imports.push({
|
|
1827
|
+
source: stdMatch[1],
|
|
1828
|
+
specifiers: [stdMatch[1].split("/").pop()],
|
|
1829
|
+
isDefault: true,
|
|
1830
|
+
isNamespace: false,
|
|
1831
|
+
rawText: impLine,
|
|
1832
|
+
span: [0, impLine.length]
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return imports;
|
|
1839
|
+
}
|
|
1840
|
+
function extractExports3(content, filePath) {
|
|
1841
|
+
const exports = [];
|
|
1842
|
+
const decls = extractDeclarations3(content, filePath);
|
|
1843
|
+
for (const d of decls) {
|
|
1844
|
+
if (d.name[0] >= "A" && d.name[0] <= "Z") {
|
|
1845
|
+
exports.push({
|
|
1846
|
+
name: d.name,
|
|
1847
|
+
isDefault: false,
|
|
1848
|
+
rawText: d.rawText.split(`
|
|
1849
|
+
`)[0],
|
|
1850
|
+
span: d.span
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return exports;
|
|
1855
|
+
}
|
|
1856
|
+
function computeSemanticDiff3(oldResult, newResult) {
|
|
1857
|
+
const patches = [];
|
|
1858
|
+
const fileId = newResult.fileEntityId;
|
|
1859
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
1860
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
1861
|
+
for (const [id, entity] of newDecls) {
|
|
1862
|
+
if (!oldDecls.has(id)) {
|
|
1863
|
+
const oldEntity = findRenamedEntity3(entity, oldResult.declarations, newDecls);
|
|
1864
|
+
if (oldEntity) {
|
|
1865
|
+
patches.push({ kind: "symbolRename", entityId: oldEntity.id, oldName: oldEntity.name, newName: entity.name });
|
|
1866
|
+
} else {
|
|
1867
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
for (const [id, entity] of oldDecls) {
|
|
1872
|
+
if (!newDecls.has(id)) {
|
|
1873
|
+
const wasRenamed = findRenamedEntity3(entity, newResult.declarations, oldDecls);
|
|
1874
|
+
if (!wasRenamed) {
|
|
1875
|
+
patches.push({ kind: "symbolRemove", entityId: id, entityName: entity.name });
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
for (const [id, newEntity] of newDecls) {
|
|
1880
|
+
const oldEntity = oldDecls.get(id);
|
|
1881
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
1882
|
+
patches.push({
|
|
1883
|
+
kind: "symbolModify",
|
|
1884
|
+
entityId: id,
|
|
1885
|
+
entityName: newEntity.name,
|
|
1886
|
+
oldSignature: oldEntity.signature,
|
|
1887
|
+
newSignature: newEntity.signature,
|
|
1888
|
+
oldRawText: oldEntity.rawText,
|
|
1889
|
+
newRawText: newEntity.rawText
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
1894
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
1895
|
+
for (const [source, imp] of newImports) {
|
|
1896
|
+
const oldImp = oldImports.get(source);
|
|
1897
|
+
if (!oldImp) {
|
|
1898
|
+
patches.push({ kind: "importAdd", fileId, source, specifiers: imp.specifiers, rawText: imp.rawText });
|
|
1899
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
1900
|
+
patches.push({ kind: "importModify", fileId, source, oldSpecifiers: oldImp.specifiers, newSpecifiers: imp.specifiers });
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
for (const [source] of oldImports) {
|
|
1904
|
+
if (!newImports.has(source)) {
|
|
1905
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
1909
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
1910
|
+
for (const [name, exp] of newExports) {
|
|
1911
|
+
if (!oldExports.has(name)) {
|
|
1912
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
for (const [name] of oldExports) {
|
|
1916
|
+
if (!newExports.has(name)) {
|
|
1917
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return patches;
|
|
1921
|
+
}
|
|
1922
|
+
function findRenamedEntity3(entity, candidates, existingIds) {
|
|
1923
|
+
for (const candidate of candidates) {
|
|
1924
|
+
if (candidate.kind !== entity.kind)
|
|
1925
|
+
continue;
|
|
1926
|
+
if (candidate.name === entity.name)
|
|
1927
|
+
continue;
|
|
1928
|
+
if (existingIds.has(candidate.id))
|
|
1929
|
+
continue;
|
|
1930
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
1931
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
1932
|
+
if (normalizedOld === normalizedNew)
|
|
1933
|
+
return candidate;
|
|
1934
|
+
}
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
function makeEntityId3(filePath, kind, name) {
|
|
1938
|
+
return `${kind}:${filePath}:${name}`;
|
|
1939
|
+
}
|
|
1940
|
+
function normalizeSignature3(text) {
|
|
1941
|
+
return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
1942
|
+
}
|
|
1943
|
+
// src/semantic/rust-parser.ts
|
|
1944
|
+
var rustParser = {
|
|
1945
|
+
languages: ["rust"],
|
|
1946
|
+
parse(content, filePath) {
|
|
1947
|
+
const fileEntityId = `file:${filePath}`;
|
|
1948
|
+
return {
|
|
1949
|
+
fileEntityId,
|
|
1950
|
+
filePath,
|
|
1951
|
+
language: "rust",
|
|
1952
|
+
declarations: extractDeclarations4(content, filePath),
|
|
1953
|
+
imports: extractImports4(content),
|
|
1954
|
+
exports: extractExports4(content, filePath)
|
|
1955
|
+
};
|
|
1956
|
+
},
|
|
1957
|
+
diff(oldResult, newResult) {
|
|
1958
|
+
return computeSemanticDiff4(oldResult, newResult);
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
function extractDeclarations4(content, filePath) {
|
|
1962
|
+
const declarations = [];
|
|
1963
|
+
const lines = content.split(`
|
|
1964
|
+
`);
|
|
1965
|
+
let i = 0;
|
|
1966
|
+
while (i < lines.length) {
|
|
1967
|
+
const line = lines[i];
|
|
1968
|
+
const trimmed = line.trim();
|
|
1969
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("use ") || trimmed.startsWith("extern ") || trimmed.startsWith("mod ") || trimmed.startsWith("#![")) {
|
|
1970
|
+
i++;
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
const attrs = [];
|
|
1974
|
+
const attrStart = i;
|
|
1975
|
+
while (i < lines.length && lines[i].trim().startsWith("#[")) {
|
|
1976
|
+
attrs.push(lines[i].trim());
|
|
1977
|
+
i++;
|
|
1978
|
+
}
|
|
1979
|
+
if (i >= lines.length)
|
|
1980
|
+
break;
|
|
1981
|
+
const declLine = lines[i].trim();
|
|
1982
|
+
const stripped = declLine.replace(/^pub(\s*\([^)]*\))?\s+/, "").replace(/^async\s+/, "").replace(/^unsafe\s+/, "").replace(/^const\s+(?=fn)/, "");
|
|
1983
|
+
let match = stripped.match(/^struct\s+(\w+)/);
|
|
1984
|
+
if (match) {
|
|
1985
|
+
if (declLine.includes(";") && !declLine.includes("{")) {
|
|
1986
|
+
const result = extractToSemicolon(match[1], "ClassDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
1987
|
+
declarations.push(result.entity);
|
|
1988
|
+
i = result.endLine + 1;
|
|
1989
|
+
} else {
|
|
1990
|
+
const result = extractBraceBlock2(match[1], "ClassDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
1991
|
+
result.entity.children = extractStructFields2(lines, i, result.endLine, match[1], filePath);
|
|
1992
|
+
declarations.push(result.entity);
|
|
1993
|
+
i = result.endLine + 1;
|
|
1994
|
+
}
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
match = stripped.match(/^enum\s+(\w+)/);
|
|
1998
|
+
if (match) {
|
|
1999
|
+
const result = extractBraceBlock2(match[1], "EnumDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2000
|
+
declarations.push(result.entity);
|
|
2001
|
+
i = result.endLine + 1;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
match = stripped.match(/^trait\s+(\w+)/);
|
|
2005
|
+
if (match) {
|
|
2006
|
+
const result = extractBraceBlock2(match[1], "InterfaceDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2007
|
+
result.entity.children = extractTraitMethods(lines, i, result.endLine, match[1], filePath);
|
|
2008
|
+
declarations.push(result.entity);
|
|
2009
|
+
i = result.endLine + 1;
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
match = stripped.match(/^impl(?:<[^>]*>)?\s+(?:(\w+)\s+for\s+)?(\w+)/);
|
|
2013
|
+
if (match && stripped.includes("{")) {
|
|
2014
|
+
const typeName = match[2];
|
|
2015
|
+
const traitName = match[1];
|
|
2016
|
+
const implName = traitName ? `${traitName} for ${typeName}` : typeName;
|
|
2017
|
+
const result = extractBraceBlock2(implName, "ClassDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2018
|
+
result.entity.children = extractImplMethods(lines, i, result.endLine, implName, filePath);
|
|
2019
|
+
result.entity.id = makeEntityId4(filePath, "ClassDef", `impl:${implName}`);
|
|
2020
|
+
declarations.push(result.entity);
|
|
2021
|
+
i = result.endLine + 1;
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
match = stripped.match(/^fn\s+(\w+)/);
|
|
2025
|
+
if (match) {
|
|
2026
|
+
const result = extractBraceBlock2(match[1], "FunctionDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2027
|
+
declarations.push(result.entity);
|
|
2028
|
+
i = result.endLine + 1;
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
match = stripped.match(/^type\s+(\w+)/);
|
|
2032
|
+
if (match) {
|
|
2033
|
+
const result = extractToSemicolon(match[1], "TypeAlias", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2034
|
+
declarations.push(result.entity);
|
|
2035
|
+
i = result.endLine + 1;
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
match = stripped.match(/^const\s+(\w+)/);
|
|
2039
|
+
if (match) {
|
|
2040
|
+
const result = extractToSemicolon(match[1], "VariableDecl", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2041
|
+
declarations.push(result.entity);
|
|
2042
|
+
i = result.endLine + 1;
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
match = stripped.match(/^static\s+(?:mut\s+)?(\w+)/);
|
|
2046
|
+
if (match) {
|
|
2047
|
+
const result = extractToSemicolon(match[1], "VariableDecl", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2048
|
+
declarations.push(result.entity);
|
|
2049
|
+
i = result.endLine + 1;
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
match = stripped.match(/^macro_rules!\s+(\w+)/);
|
|
2053
|
+
if (match) {
|
|
2054
|
+
const result = extractBraceBlock2(match[1], "FunctionDef", lines, attrs.length > 0 ? attrStart : i, i, filePath, attrs);
|
|
2055
|
+
result.entity.id = makeEntityId4(filePath, "FunctionDef", `macro:${match[1]}`);
|
|
2056
|
+
declarations.push(result.entity);
|
|
2057
|
+
i = result.endLine + 1;
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
if (attrs.length > 0) {
|
|
2061
|
+
i++;
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
i++;
|
|
2065
|
+
}
|
|
2066
|
+
return declarations;
|
|
2067
|
+
}
|
|
2068
|
+
function extractBraceBlock2(name, kind, lines, startLine, defLine, filePath, attrs) {
|
|
2069
|
+
let depth = 0;
|
|
2070
|
+
let foundOpen = false;
|
|
2071
|
+
let endLine = defLine;
|
|
2072
|
+
for (let i = defLine;i < lines.length; i++) {
|
|
2073
|
+
for (const ch of lines[i]) {
|
|
2074
|
+
if (ch === "{") {
|
|
2075
|
+
depth++;
|
|
2076
|
+
foundOpen = true;
|
|
2077
|
+
} else if (ch === "}") {
|
|
2078
|
+
depth--;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (foundOpen && depth <= 0) {
|
|
2082
|
+
endLine = i;
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
if (i > defLine + 300) {
|
|
2086
|
+
endLine = i;
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
endLine = i;
|
|
2090
|
+
}
|
|
2091
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
2092
|
+
`);
|
|
2093
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
2094
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
2095
|
+
return {
|
|
2096
|
+
entity: {
|
|
2097
|
+
id: makeEntityId4(filePath, kind, name),
|
|
2098
|
+
kind,
|
|
2099
|
+
name,
|
|
2100
|
+
scopePath: name,
|
|
2101
|
+
span: [startOffset, startOffset + rawText.length],
|
|
2102
|
+
rawText,
|
|
2103
|
+
signature: normalizeSignature4(rawText),
|
|
2104
|
+
children: []
|
|
2105
|
+
},
|
|
2106
|
+
endLine
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function extractToSemicolon(name, kind, lines, startLine, defLine, filePath, attrs) {
|
|
2110
|
+
let endLine = defLine;
|
|
2111
|
+
for (let i = defLine;i < lines.length; i++) {
|
|
2112
|
+
if (lines[i].includes(";")) {
|
|
2113
|
+
endLine = i;
|
|
2114
|
+
break;
|
|
2115
|
+
}
|
|
2116
|
+
endLine = i;
|
|
2117
|
+
}
|
|
2118
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
2119
|
+
`);
|
|
2120
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
2121
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
2122
|
+
return {
|
|
2123
|
+
entity: {
|
|
2124
|
+
id: makeEntityId4(filePath, kind, name),
|
|
2125
|
+
kind,
|
|
2126
|
+
name,
|
|
2127
|
+
scopePath: name,
|
|
2128
|
+
span: [startOffset, startOffset + rawText.length],
|
|
2129
|
+
rawText,
|
|
2130
|
+
signature: normalizeSignature4(rawText),
|
|
2131
|
+
children: []
|
|
2132
|
+
},
|
|
2133
|
+
endLine
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
function extractStructFields2(lines, startLine, endLine, structName, filePath) {
|
|
2137
|
+
const children = [];
|
|
2138
|
+
let inBody = false;
|
|
2139
|
+
for (let i = startLine;i <= endLine; i++) {
|
|
2140
|
+
const trimmed = lines[i].trim();
|
|
2141
|
+
if (trimmed.includes("{")) {
|
|
2142
|
+
inBody = true;
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
if (!inBody)
|
|
2146
|
+
continue;
|
|
2147
|
+
if (trimmed === "}" || !trimmed || trimmed.startsWith("//"))
|
|
2148
|
+
continue;
|
|
2149
|
+
const match = trimmed.match(/^(?:pub\s+)?(\w+)\s*:/);
|
|
2150
|
+
if (match) {
|
|
2151
|
+
children.push({
|
|
2152
|
+
id: makeEntityId4(filePath, "PropertyDef", `${structName}.${match[1]}`),
|
|
2153
|
+
kind: "PropertyDef",
|
|
2154
|
+
name: match[1],
|
|
2155
|
+
scopePath: `${structName}.${match[1]}`,
|
|
2156
|
+
span: [0, 0],
|
|
2157
|
+
rawText: trimmed,
|
|
2158
|
+
signature: normalizeSignature4(trimmed),
|
|
2159
|
+
children: []
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return children;
|
|
2164
|
+
}
|
|
2165
|
+
function extractTraitMethods(lines, startLine, endLine, traitName, filePath) {
|
|
2166
|
+
return extractBlockMethods(lines, startLine, endLine, traitName, filePath);
|
|
2167
|
+
}
|
|
2168
|
+
function extractImplMethods(lines, startLine, endLine, implName, filePath) {
|
|
2169
|
+
return extractBlockMethods(lines, startLine, endLine, implName, filePath);
|
|
2170
|
+
}
|
|
2171
|
+
function extractBlockMethods(lines, startLine, endLine, parentName, filePath) {
|
|
2172
|
+
const children = [];
|
|
2173
|
+
let depth = 0;
|
|
2174
|
+
for (let i = startLine;i <= endLine; i++) {
|
|
2175
|
+
const line = lines[i];
|
|
2176
|
+
const depthBefore = depth;
|
|
2177
|
+
for (const ch of line) {
|
|
2178
|
+
if (ch === "{")
|
|
2179
|
+
depth++;
|
|
2180
|
+
else if (ch === "}")
|
|
2181
|
+
depth--;
|
|
2182
|
+
}
|
|
2183
|
+
if (depthBefore === 1) {
|
|
2184
|
+
const trimmed = line.trim().replace(/^pub(\s*\([^)]*\))?\s+/, "").replace(/^async\s+/, "").replace(/^unsafe\s+/, "").replace(/^const\s+(?=fn)/, "");
|
|
2185
|
+
const match = trimmed.match(/^fn\s+(\w+)/);
|
|
2186
|
+
if (match) {
|
|
2187
|
+
const kind = match[1] === "new" ? "Constructor" : "MethodDef";
|
|
2188
|
+
children.push({
|
|
2189
|
+
id: makeEntityId4(filePath, kind, `${parentName}.${match[1]}`),
|
|
2190
|
+
kind,
|
|
2191
|
+
name: match[1],
|
|
2192
|
+
scopePath: `${parentName}.${match[1]}`,
|
|
2193
|
+
span: [0, 0],
|
|
2194
|
+
rawText: line.trim(),
|
|
2195
|
+
signature: normalizeSignature4(line.trim()),
|
|
2196
|
+
children: []
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return children;
|
|
2202
|
+
}
|
|
2203
|
+
function extractImports4(content) {
|
|
2204
|
+
const imports = [];
|
|
2205
|
+
const lines = content.split(`
|
|
2206
|
+
`);
|
|
2207
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2208
|
+
const trimmed = lines[i].trim();
|
|
2209
|
+
if (!trimmed.startsWith("use ") && !trimmed.startsWith("pub use "))
|
|
2210
|
+
continue;
|
|
2211
|
+
let full = trimmed;
|
|
2212
|
+
while (!full.includes(";") && i + 1 < lines.length) {
|
|
2213
|
+
i++;
|
|
2214
|
+
full += " " + lines[i].trim();
|
|
2215
|
+
}
|
|
2216
|
+
const cleaned = full.replace(/^pub\s+/, "").replace(/^use\s+/, "").replace(/;$/, "").trim();
|
|
2217
|
+
const braceMatch = cleaned.match(/^(.+)::\{([^}]+)\}$/);
|
|
2218
|
+
if (braceMatch) {
|
|
2219
|
+
const source = braceMatch[1];
|
|
2220
|
+
const specifiers = braceMatch[2].split(",").map((s) => s.trim().split("::").pop().replace(/\s+as\s+\w+/, "")).filter(Boolean);
|
|
2221
|
+
imports.push({
|
|
2222
|
+
source,
|
|
2223
|
+
specifiers,
|
|
2224
|
+
isDefault: false,
|
|
2225
|
+
isNamespace: false,
|
|
2226
|
+
rawText: full,
|
|
2227
|
+
span: [0, full.length]
|
|
2228
|
+
});
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
const simpleMatch = cleaned.match(/^(.+)::(\w+|\*)$/);
|
|
2232
|
+
if (simpleMatch) {
|
|
2233
|
+
const isWild = simpleMatch[2] === "*";
|
|
2234
|
+
imports.push({
|
|
2235
|
+
source: cleaned,
|
|
2236
|
+
specifiers: [simpleMatch[2]],
|
|
2237
|
+
isDefault: !isWild,
|
|
2238
|
+
isNamespace: isWild,
|
|
2239
|
+
rawText: full,
|
|
2240
|
+
span: [0, full.length]
|
|
2241
|
+
});
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
const aliasMatch = cleaned.match(/^(\S+)\s+as\s+(\w+)$/);
|
|
2245
|
+
if (aliasMatch) {
|
|
2246
|
+
imports.push({
|
|
2247
|
+
source: aliasMatch[1],
|
|
2248
|
+
specifiers: [aliasMatch[2]],
|
|
2249
|
+
isDefault: true,
|
|
2250
|
+
isNamespace: false,
|
|
2251
|
+
rawText: full,
|
|
2252
|
+
span: [0, full.length]
|
|
2253
|
+
});
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
imports.push({
|
|
2257
|
+
source: cleaned,
|
|
2258
|
+
specifiers: [cleaned.split("::").pop() ?? cleaned],
|
|
2259
|
+
isDefault: true,
|
|
2260
|
+
isNamespace: false,
|
|
2261
|
+
rawText: full,
|
|
2262
|
+
span: [0, full.length]
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
return imports;
|
|
2266
|
+
}
|
|
2267
|
+
function extractExports4(content, filePath) {
|
|
2268
|
+
const exports = [];
|
|
2269
|
+
const lines = content.split(`
|
|
2270
|
+
`);
|
|
2271
|
+
for (const line of lines) {
|
|
2272
|
+
const trimmed = line.trim();
|
|
2273
|
+
if (!trimmed.startsWith("pub ") && !trimmed.startsWith("pub("))
|
|
2274
|
+
continue;
|
|
2275
|
+
const match = trimmed.match(/^pub(?:\s*\([^)]*\))?\s+(?:async\s+|unsafe\s+|const\s+)?(?:fn|struct|enum|trait|type|const|static)\s+(\w+)/);
|
|
2276
|
+
if (match) {
|
|
2277
|
+
exports.push({
|
|
2278
|
+
name: match[1],
|
|
2279
|
+
isDefault: false,
|
|
2280
|
+
rawText: trimmed.split("{")[0].trim(),
|
|
2281
|
+
span: [0, 0]
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
return exports;
|
|
2286
|
+
}
|
|
2287
|
+
function computeSemanticDiff4(oldResult, newResult) {
|
|
2288
|
+
const patches = [];
|
|
2289
|
+
const fileId = newResult.fileEntityId;
|
|
2290
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
2291
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
2292
|
+
for (const [id, entity] of newDecls) {
|
|
2293
|
+
if (!oldDecls.has(id)) {
|
|
2294
|
+
const oldEntity = findRenamedEntity4(entity, oldResult.declarations, newDecls);
|
|
2295
|
+
if (oldEntity) {
|
|
2296
|
+
patches.push({
|
|
2297
|
+
kind: "symbolRename",
|
|
2298
|
+
entityId: oldEntity.id,
|
|
2299
|
+
oldName: oldEntity.name,
|
|
2300
|
+
newName: entity.name
|
|
2301
|
+
});
|
|
2302
|
+
} else {
|
|
2303
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
for (const [id, entity] of oldDecls) {
|
|
2308
|
+
if (!newDecls.has(id)) {
|
|
2309
|
+
const wasRenamed = findRenamedEntity4(entity, newResult.declarations, oldDecls);
|
|
2310
|
+
if (!wasRenamed) {
|
|
2311
|
+
patches.push({
|
|
2312
|
+
kind: "symbolRemove",
|
|
2313
|
+
entityId: id,
|
|
2314
|
+
entityName: entity.name
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
for (const [id, newEntity] of newDecls) {
|
|
2320
|
+
const oldEntity = oldDecls.get(id);
|
|
2321
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
2322
|
+
patches.push({
|
|
2323
|
+
kind: "symbolModify",
|
|
2324
|
+
entityId: id,
|
|
2325
|
+
entityName: newEntity.name,
|
|
2326
|
+
oldSignature: oldEntity.signature,
|
|
2327
|
+
newSignature: newEntity.signature,
|
|
2328
|
+
oldRawText: oldEntity.rawText,
|
|
2329
|
+
newRawText: newEntity.rawText
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
2334
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
2335
|
+
for (const [source, imp] of newImports) {
|
|
2336
|
+
const oldImp = oldImports.get(source);
|
|
2337
|
+
if (!oldImp) {
|
|
2338
|
+
patches.push({
|
|
2339
|
+
kind: "importAdd",
|
|
2340
|
+
fileId,
|
|
2341
|
+
source,
|
|
2342
|
+
specifiers: imp.specifiers,
|
|
2343
|
+
rawText: imp.rawText
|
|
2344
|
+
});
|
|
2345
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
2346
|
+
patches.push({
|
|
2347
|
+
kind: "importModify",
|
|
2348
|
+
fileId,
|
|
2349
|
+
source,
|
|
2350
|
+
oldSpecifiers: oldImp.specifiers,
|
|
2351
|
+
newSpecifiers: imp.specifiers
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
for (const [source] of oldImports) {
|
|
2356
|
+
if (!newImports.has(source)) {
|
|
2357
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
2361
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
2362
|
+
for (const [name, exp] of newExports) {
|
|
2363
|
+
if (!oldExports.has(name)) {
|
|
2364
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
for (const [name] of oldExports) {
|
|
2368
|
+
if (!newExports.has(name)) {
|
|
2369
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return patches;
|
|
2373
|
+
}
|
|
2374
|
+
function findRenamedEntity4(entity, candidates, existingIds) {
|
|
2375
|
+
for (const candidate of candidates) {
|
|
2376
|
+
if (candidate.kind !== entity.kind)
|
|
2377
|
+
continue;
|
|
2378
|
+
if (candidate.name === entity.name)
|
|
2379
|
+
continue;
|
|
2380
|
+
if (existingIds.has(candidate.id))
|
|
2381
|
+
continue;
|
|
2382
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
2383
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
2384
|
+
if (normalizedOld === normalizedNew)
|
|
2385
|
+
return candidate;
|
|
2386
|
+
}
|
|
2387
|
+
return null;
|
|
2388
|
+
}
|
|
2389
|
+
function makeEntityId4(filePath, kind, name) {
|
|
2390
|
+
return `${kind}:${filePath}:${name}`;
|
|
2391
|
+
}
|
|
2392
|
+
function normalizeSignature4(text) {
|
|
2393
|
+
return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
2394
|
+
}
|
|
2395
|
+
// src/semantic/ruby-parser.ts
|
|
2396
|
+
var rubyParser = {
|
|
2397
|
+
languages: ["ruby"],
|
|
2398
|
+
parse(content, filePath) {
|
|
2399
|
+
const fileEntityId = `file:${filePath}`;
|
|
2400
|
+
return {
|
|
2401
|
+
fileEntityId,
|
|
2402
|
+
filePath,
|
|
2403
|
+
language: "ruby",
|
|
2404
|
+
declarations: extractDeclarations5(content, filePath),
|
|
2405
|
+
imports: extractImports5(content),
|
|
2406
|
+
exports: extractExports5(content)
|
|
2407
|
+
};
|
|
2408
|
+
},
|
|
2409
|
+
diff(oldResult, newResult) {
|
|
2410
|
+
return computeSemanticDiff5(oldResult, newResult);
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
function extractDeclarations5(content, filePath) {
|
|
2414
|
+
const declarations = [];
|
|
2415
|
+
const lines = content.split(`
|
|
2416
|
+
`);
|
|
2417
|
+
const scopeStack = [];
|
|
2418
|
+
let i = 0;
|
|
2419
|
+
while (i < lines.length) {
|
|
2420
|
+
const line = lines[i];
|
|
2421
|
+
const trimmed = line.trim();
|
|
2422
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2423
|
+
i++;
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
let match = trimmed.match(/^class\s+([A-Z]\w*(?:::\w+)*)/);
|
|
2427
|
+
if (match) {
|
|
2428
|
+
const name = match[1];
|
|
2429
|
+
scopeStack.push(name);
|
|
2430
|
+
const result = extractEndBlock(name, "ClassDef", lines, i, filePath, scopeStack);
|
|
2431
|
+
result.entity.children = extractClassMembers3(lines, i, result.endLine, name, filePath);
|
|
2432
|
+
declarations.push(result.entity);
|
|
2433
|
+
scopeStack.pop();
|
|
2434
|
+
i = result.endLine + 1;
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
match = trimmed.match(/^module\s+([A-Z]\w*(?:::\w+)*)/);
|
|
2438
|
+
if (match) {
|
|
2439
|
+
const name = match[1];
|
|
2440
|
+
scopeStack.push(name);
|
|
2441
|
+
const result = extractEndBlock(name, "ClassDef", lines, i, filePath, scopeStack);
|
|
2442
|
+
result.entity.children = extractClassMembers3(lines, i, result.endLine, name, filePath);
|
|
2443
|
+
declarations.push(result.entity);
|
|
2444
|
+
scopeStack.pop();
|
|
2445
|
+
i = result.endLine + 1;
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
match = trimmed.match(/^def\s+(self\.)?(\w+[?!=]?)/);
|
|
2449
|
+
if (match) {
|
|
2450
|
+
const name = match[1] ? `self.${match[2]}` : match[2];
|
|
2451
|
+
const result = extractEndBlock(name, "FunctionDef", lines, i, filePath, scopeStack);
|
|
2452
|
+
declarations.push(result.entity);
|
|
2453
|
+
i = result.endLine + 1;
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
match = trimmed.match(/^([A-Z][A-Z0-9_]*)\s*=/);
|
|
2457
|
+
if (match) {
|
|
2458
|
+
declarations.push({
|
|
2459
|
+
id: makeEntityId5(filePath, "VariableDecl", match[1]),
|
|
2460
|
+
kind: "VariableDecl",
|
|
2461
|
+
name: match[1],
|
|
2462
|
+
scopePath: match[1],
|
|
2463
|
+
span: [0, 0],
|
|
2464
|
+
rawText: trimmed,
|
|
2465
|
+
signature: normalizeSignature5(trimmed),
|
|
2466
|
+
children: []
|
|
2467
|
+
});
|
|
2468
|
+
i++;
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
i++;
|
|
2472
|
+
}
|
|
2473
|
+
return declarations;
|
|
2474
|
+
}
|
|
2475
|
+
function extractEndBlock(name, kind, lines, startLine, filePath, scopeStack) {
|
|
2476
|
+
let depth = 0;
|
|
2477
|
+
let endLine = startLine;
|
|
2478
|
+
for (let i = startLine;i < lines.length; i++) {
|
|
2479
|
+
const trimmed = lines[i].trim();
|
|
2480
|
+
if (isBlockOpener(trimmed))
|
|
2481
|
+
depth++;
|
|
2482
|
+
if (trimmed === "end" || trimmed.startsWith("end ") || trimmed.startsWith("end#"))
|
|
2483
|
+
depth--;
|
|
2484
|
+
if (depth <= 0 && i > startLine) {
|
|
2485
|
+
endLine = i;
|
|
2486
|
+
break;
|
|
2487
|
+
}
|
|
2488
|
+
endLine = i;
|
|
2489
|
+
}
|
|
2490
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
2491
|
+
`);
|
|
2492
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
2493
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
2494
|
+
return {
|
|
2495
|
+
entity: {
|
|
2496
|
+
id: makeEntityId5(filePath, kind, name),
|
|
2497
|
+
kind,
|
|
2498
|
+
name,
|
|
2499
|
+
scopePath: name,
|
|
2500
|
+
span: [startOffset, startOffset + rawText.length],
|
|
2501
|
+
rawText,
|
|
2502
|
+
signature: normalizeSignature5(rawText),
|
|
2503
|
+
children: []
|
|
2504
|
+
},
|
|
2505
|
+
endLine
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
function isBlockOpener(trimmed) {
|
|
2509
|
+
if (/^(class|module|def|do|begin|case)\b/.test(trimmed))
|
|
2510
|
+
return true;
|
|
2511
|
+
if (/^(if|unless|while|until|for)\b/.test(trimmed) && !trimmed.includes(" then "))
|
|
2512
|
+
return true;
|
|
2513
|
+
return false;
|
|
2514
|
+
}
|
|
2515
|
+
function extractClassMembers3(lines, startLine, endLine, parentName, filePath) {
|
|
2516
|
+
const children = [];
|
|
2517
|
+
let depth = 0;
|
|
2518
|
+
for (let i = startLine;i <= endLine; i++) {
|
|
2519
|
+
const trimmed = lines[i].trim();
|
|
2520
|
+
const depthBefore = depth;
|
|
2521
|
+
if (isBlockOpener(trimmed))
|
|
2522
|
+
depth++;
|
|
2523
|
+
if (trimmed === "end" || trimmed.startsWith("end ") || trimmed.startsWith("end#"))
|
|
2524
|
+
depth--;
|
|
2525
|
+
if (depthBefore === 1) {
|
|
2526
|
+
const match = trimmed.match(/^def\s+(self\.)?(\w+[?!=]?)/);
|
|
2527
|
+
if (match) {
|
|
2528
|
+
const methodName = match[1] ? `self.${match[2]}` : match[2];
|
|
2529
|
+
const kind = methodName === "initialize" ? "Constructor" : "MethodDef";
|
|
2530
|
+
children.push({
|
|
2531
|
+
id: makeEntityId5(filePath, kind, `${parentName}.${methodName}`),
|
|
2532
|
+
kind,
|
|
2533
|
+
name: methodName,
|
|
2534
|
+
scopePath: `${parentName}.${methodName}`,
|
|
2535
|
+
span: [0, 0],
|
|
2536
|
+
rawText: trimmed,
|
|
2537
|
+
signature: normalizeSignature5(trimmed),
|
|
2538
|
+
children: []
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
const attrMatch = trimmed.match(/^attr_(accessor|reader|writer)\s+(.*)/);
|
|
2542
|
+
if (attrMatch) {
|
|
2543
|
+
const attrs = attrMatch[2].split(",").map((a) => a.trim().replace(/^:/, ""));
|
|
2544
|
+
for (const attr of attrs) {
|
|
2545
|
+
if (attr) {
|
|
2546
|
+
children.push({
|
|
2547
|
+
id: makeEntityId5(filePath, "PropertyDef", `${parentName}.${attr}`),
|
|
2548
|
+
kind: "PropertyDef",
|
|
2549
|
+
name: attr,
|
|
2550
|
+
scopePath: `${parentName}.${attr}`,
|
|
2551
|
+
span: [0, 0],
|
|
2552
|
+
rawText: trimmed,
|
|
2553
|
+
signature: normalizeSignature5(trimmed),
|
|
2554
|
+
children: []
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return children;
|
|
2562
|
+
}
|
|
2563
|
+
function extractImports5(content) {
|
|
2564
|
+
const imports = [];
|
|
2565
|
+
const lines = content.split(`
|
|
2566
|
+
`);
|
|
2567
|
+
for (const line of lines) {
|
|
2568
|
+
const trimmed = line.trim();
|
|
2569
|
+
let match = trimmed.match(/^require\s+['"]([^'"]+)['"]/);
|
|
2570
|
+
if (match) {
|
|
2571
|
+
imports.push({
|
|
2572
|
+
source: match[1],
|
|
2573
|
+
specifiers: [match[1].split("/").pop()],
|
|
2574
|
+
isDefault: true,
|
|
2575
|
+
isNamespace: false,
|
|
2576
|
+
rawText: trimmed,
|
|
2577
|
+
span: [0, trimmed.length]
|
|
2578
|
+
});
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
match = trimmed.match(/^require_relative\s+['"]([^'"]+)['"]/);
|
|
2582
|
+
if (match) {
|
|
2583
|
+
imports.push({
|
|
2584
|
+
source: match[1],
|
|
2585
|
+
specifiers: [match[1].split("/").pop()],
|
|
2586
|
+
isDefault: true,
|
|
2587
|
+
isNamespace: false,
|
|
2588
|
+
rawText: trimmed,
|
|
2589
|
+
span: [0, trimmed.length]
|
|
2590
|
+
});
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
match = trimmed.match(/^(include|extend|prepend)\s+([A-Z]\w*(?:::\w+)*)/);
|
|
2594
|
+
if (match) {
|
|
2595
|
+
imports.push({
|
|
2596
|
+
source: match[2],
|
|
2597
|
+
specifiers: [match[2]],
|
|
2598
|
+
isDefault: false,
|
|
2599
|
+
isNamespace: true,
|
|
2600
|
+
rawText: trimmed,
|
|
2601
|
+
span: [0, trimmed.length]
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return imports;
|
|
2606
|
+
}
|
|
2607
|
+
function extractExports5(content) {
|
|
2608
|
+
const exports = [];
|
|
2609
|
+
const lines = content.split(`
|
|
2610
|
+
`);
|
|
2611
|
+
for (const line of lines) {
|
|
2612
|
+
const trimmed = line.trim();
|
|
2613
|
+
const match = trimmed.match(/^module_function\s+:(\w+)/);
|
|
2614
|
+
if (match) {
|
|
2615
|
+
exports.push({
|
|
2616
|
+
name: match[1],
|
|
2617
|
+
isDefault: false,
|
|
2618
|
+
rawText: trimmed,
|
|
2619
|
+
span: [0, 0]
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
const pubMatch = trimmed.match(/^public\s+:(\w+)/);
|
|
2623
|
+
if (pubMatch) {
|
|
2624
|
+
exports.push({
|
|
2625
|
+
name: pubMatch[1],
|
|
2626
|
+
isDefault: false,
|
|
2627
|
+
rawText: trimmed,
|
|
2628
|
+
span: [0, 0]
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return exports;
|
|
2633
|
+
}
|
|
2634
|
+
function computeSemanticDiff5(oldResult, newResult) {
|
|
2635
|
+
const patches = [];
|
|
2636
|
+
const fileId = newResult.fileEntityId;
|
|
2637
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
2638
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
2639
|
+
for (const [id, entity] of newDecls) {
|
|
2640
|
+
if (!oldDecls.has(id)) {
|
|
2641
|
+
const oldEntity = findRenamedEntity5(entity, oldResult.declarations, newDecls);
|
|
2642
|
+
if (oldEntity) {
|
|
2643
|
+
patches.push({ kind: "symbolRename", entityId: oldEntity.id, oldName: oldEntity.name, newName: entity.name });
|
|
2644
|
+
} else {
|
|
2645
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
for (const [id, entity] of oldDecls) {
|
|
2650
|
+
if (!newDecls.has(id)) {
|
|
2651
|
+
const wasRenamed = findRenamedEntity5(entity, newResult.declarations, oldDecls);
|
|
2652
|
+
if (!wasRenamed) {
|
|
2653
|
+
patches.push({ kind: "symbolRemove", entityId: id, entityName: entity.name });
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
for (const [id, newEntity] of newDecls) {
|
|
2658
|
+
const oldEntity = oldDecls.get(id);
|
|
2659
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
2660
|
+
patches.push({
|
|
2661
|
+
kind: "symbolModify",
|
|
2662
|
+
entityId: id,
|
|
2663
|
+
entityName: newEntity.name,
|
|
2664
|
+
oldSignature: oldEntity.signature,
|
|
2665
|
+
newSignature: newEntity.signature,
|
|
2666
|
+
oldRawText: oldEntity.rawText,
|
|
2667
|
+
newRawText: newEntity.rawText
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
2672
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
2673
|
+
for (const [source, imp] of newImports) {
|
|
2674
|
+
const oldImp = oldImports.get(source);
|
|
2675
|
+
if (!oldImp) {
|
|
2676
|
+
patches.push({ kind: "importAdd", fileId, source, specifiers: imp.specifiers, rawText: imp.rawText });
|
|
2677
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
2678
|
+
patches.push({ kind: "importModify", fileId, source, oldSpecifiers: oldImp.specifiers, newSpecifiers: imp.specifiers });
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
for (const [source] of oldImports) {
|
|
2682
|
+
if (!newImports.has(source)) {
|
|
2683
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
2687
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
2688
|
+
for (const [name, exp] of newExports) {
|
|
2689
|
+
if (!oldExports.has(name)) {
|
|
2690
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
for (const [name] of oldExports) {
|
|
2694
|
+
if (!newExports.has(name)) {
|
|
2695
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return patches;
|
|
2699
|
+
}
|
|
2700
|
+
function findRenamedEntity5(entity, candidates, existingIds) {
|
|
2701
|
+
for (const candidate of candidates) {
|
|
2702
|
+
if (candidate.kind !== entity.kind)
|
|
2703
|
+
continue;
|
|
2704
|
+
if (candidate.name === entity.name)
|
|
2705
|
+
continue;
|
|
2706
|
+
if (existingIds.has(candidate.id))
|
|
2707
|
+
continue;
|
|
2708
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
2709
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
2710
|
+
if (normalizedOld === normalizedNew)
|
|
2711
|
+
return candidate;
|
|
2712
|
+
}
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
function makeEntityId5(filePath, kind, name) {
|
|
2716
|
+
return `${kind}:${filePath}:${name}`;
|
|
2717
|
+
}
|
|
2718
|
+
function normalizeSignature5(text) {
|
|
2719
|
+
return text.replace(/#[^\n]*/g, "").replace(/\s+/g, " ").trim();
|
|
2720
|
+
}
|
|
2721
|
+
// src/semantic/java-parser.ts
|
|
2722
|
+
var javaParser = {
|
|
2723
|
+
languages: ["java"],
|
|
2724
|
+
parse(content, filePath) {
|
|
2725
|
+
const fileEntityId = `file:${filePath}`;
|
|
2726
|
+
return {
|
|
2727
|
+
fileEntityId,
|
|
2728
|
+
filePath,
|
|
2729
|
+
language: "java",
|
|
2730
|
+
declarations: extractDeclarations6(content, filePath),
|
|
2731
|
+
imports: extractImports6(content),
|
|
2732
|
+
exports: extractExports6(content, filePath)
|
|
2733
|
+
};
|
|
2734
|
+
},
|
|
2735
|
+
diff(oldResult, newResult) {
|
|
2736
|
+
return computeSemanticDiff6(oldResult, newResult);
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
var MODIFIERS = /(?:(?:public|private|protected|static|final|abstract|synchronized|native|transient|volatile|strictfp|sealed|non-sealed|default)\s+)*/;
|
|
2740
|
+
var CLASS_RE = new RegExp(`^${MODIFIERS.source}(class|interface|enum|record|@interface)\\s+(\\w+)`);
|
|
2741
|
+
var METHOD_RE = new RegExp(`^${MODIFIERS.source}(?:<[^>]+>\\s+)?(?:\\w[\\w<>\\[\\],\\s?]*?)\\s+(\\w+)\\s*\\(`);
|
|
2742
|
+
var FIELD_RE = new RegExp(`^${MODIFIERS.source}(?:\\w[\\w<>\\[\\],\\s?]*?)\\s+(\\w+)\\s*[;=]`);
|
|
2743
|
+
function extractDeclarations6(content, filePath) {
|
|
2744
|
+
const declarations = [];
|
|
2745
|
+
const lines = content.split(`
|
|
2746
|
+
`);
|
|
2747
|
+
let i = 0;
|
|
2748
|
+
while (i < lines.length) {
|
|
2749
|
+
const trimmed = lines[i].trim();
|
|
2750
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("package ") || trimmed.startsWith("import ")) {
|
|
2751
|
+
i++;
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
const annotations = [];
|
|
2755
|
+
const annoStart = i;
|
|
2756
|
+
while (i < lines.length && lines[i].trim().startsWith("@")) {
|
|
2757
|
+
annotations.push(lines[i].trim());
|
|
2758
|
+
i++;
|
|
2759
|
+
}
|
|
2760
|
+
if (i >= lines.length)
|
|
2761
|
+
break;
|
|
2762
|
+
const declLine = lines[i].trim();
|
|
2763
|
+
const stripped = declLine;
|
|
2764
|
+
const classMatch = stripped.match(CLASS_RE);
|
|
2765
|
+
if (classMatch) {
|
|
2766
|
+
const typeKind = classMatch[1];
|
|
2767
|
+
const name = classMatch[2];
|
|
2768
|
+
const kind = typeKind === "interface" || typeKind === "@interface" ? "InterfaceDef" : typeKind === "enum" ? "EnumDef" : "ClassDef";
|
|
2769
|
+
const result = extractBraceBlock3(name, kind, lines, annotations.length > 0 ? annoStart : i, i, filePath);
|
|
2770
|
+
result.entity.children = extractClassMembers4(lines, i, result.endLine, name, filePath);
|
|
2771
|
+
declarations.push(result.entity);
|
|
2772
|
+
i = result.endLine + 1;
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
const methodMatch = stripped.match(METHOD_RE);
|
|
2776
|
+
if (methodMatch && !stripped.includes(" class ") && !stripped.includes(" interface ") && !stripped.includes(" new ")) {
|
|
2777
|
+
const name = methodMatch[1];
|
|
2778
|
+
if (name !== "if" && name !== "for" && name !== "while" && name !== "switch" && name !== "catch") {
|
|
2779
|
+
if (stripped.includes("{") || i + 1 < lines.length && lines[i + 1].trim() === "{") {
|
|
2780
|
+
const result = extractBraceBlock3(name, "FunctionDef", lines, annotations.length > 0 ? annoStart : i, i, filePath);
|
|
2781
|
+
declarations.push(result.entity);
|
|
2782
|
+
i = result.endLine + 1;
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
if (annotations.length > 0) {
|
|
2788
|
+
i++;
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
i++;
|
|
2792
|
+
}
|
|
2793
|
+
return declarations;
|
|
2794
|
+
}
|
|
2795
|
+
function extractBraceBlock3(name, kind, lines, startLine, defLine, filePath) {
|
|
2796
|
+
let depth = 0;
|
|
2797
|
+
let foundOpen = false;
|
|
2798
|
+
let endLine = defLine;
|
|
2799
|
+
for (let i = defLine;i < lines.length; i++) {
|
|
2800
|
+
for (const ch of lines[i]) {
|
|
2801
|
+
if (ch === "{") {
|
|
2802
|
+
depth++;
|
|
2803
|
+
foundOpen = true;
|
|
2804
|
+
} else if (ch === "}") {
|
|
2805
|
+
depth--;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
if (foundOpen && depth <= 0) {
|
|
2809
|
+
endLine = i;
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
if (i > defLine + 500) {
|
|
2813
|
+
endLine = i;
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
endLine = i;
|
|
2817
|
+
}
|
|
2818
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
2819
|
+
`);
|
|
2820
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
2821
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
2822
|
+
return {
|
|
2823
|
+
entity: {
|
|
2824
|
+
id: makeEntityId6(filePath, kind, name),
|
|
2825
|
+
kind,
|
|
2826
|
+
name,
|
|
2827
|
+
scopePath: name,
|
|
2828
|
+
span: [startOffset, startOffset + rawText.length],
|
|
2829
|
+
rawText,
|
|
2830
|
+
signature: normalizeSignature6(rawText),
|
|
2831
|
+
children: []
|
|
2832
|
+
},
|
|
2833
|
+
endLine
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
function extractClassMembers4(lines, startLine, endLine, parentName, filePath) {
|
|
2837
|
+
const children = [];
|
|
2838
|
+
let depth = 0;
|
|
2839
|
+
for (let i = startLine;i <= endLine; i++) {
|
|
2840
|
+
const line = lines[i];
|
|
2841
|
+
const depthBefore = depth;
|
|
2842
|
+
for (const ch of line) {
|
|
2843
|
+
if (ch === "{")
|
|
2844
|
+
depth++;
|
|
2845
|
+
else if (ch === "}")
|
|
2846
|
+
depth--;
|
|
2847
|
+
}
|
|
2848
|
+
if (depthBefore !== 1)
|
|
2849
|
+
continue;
|
|
2850
|
+
const trimmed = line.trim();
|
|
2851
|
+
if (!trimmed || trimmed.startsWith("@") || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
|
|
2852
|
+
continue;
|
|
2853
|
+
const classMatch = trimmed.match(CLASS_RE);
|
|
2854
|
+
if (classMatch) {
|
|
2855
|
+
const typeKind = classMatch[1];
|
|
2856
|
+
const name = classMatch[2];
|
|
2857
|
+
const kind = typeKind === "interface" ? "InterfaceDef" : typeKind === "enum" ? "EnumDef" : "ClassDef";
|
|
2858
|
+
children.push({
|
|
2859
|
+
id: makeEntityId6(filePath, kind, `${parentName}.${name}`),
|
|
2860
|
+
kind,
|
|
2861
|
+
name,
|
|
2862
|
+
scopePath: `${parentName}.${name}`,
|
|
2863
|
+
span: [0, 0],
|
|
2864
|
+
rawText: trimmed,
|
|
2865
|
+
signature: normalizeSignature6(trimmed),
|
|
2866
|
+
children: []
|
|
2867
|
+
});
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
const ctorMatch = trimmed.match(new RegExp(`^${MODIFIERS.source}${parentName}\\s*\\(`));
|
|
2871
|
+
if (ctorMatch) {
|
|
2872
|
+
children.push({
|
|
2873
|
+
id: makeEntityId6(filePath, "Constructor", `${parentName}.${parentName}`),
|
|
2874
|
+
kind: "Constructor",
|
|
2875
|
+
name: parentName,
|
|
2876
|
+
scopePath: `${parentName}.${parentName}`,
|
|
2877
|
+
span: [0, 0],
|
|
2878
|
+
rawText: trimmed,
|
|
2879
|
+
signature: normalizeSignature6(trimmed),
|
|
2880
|
+
children: []
|
|
2881
|
+
});
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const methodMatch = trimmed.match(METHOD_RE);
|
|
2885
|
+
if (methodMatch) {
|
|
2886
|
+
const name = methodMatch[1];
|
|
2887
|
+
if (name !== "if" && name !== "for" && name !== "while" && name !== "switch" && name !== "catch" && name !== "return") {
|
|
2888
|
+
children.push({
|
|
2889
|
+
id: makeEntityId6(filePath, "MethodDef", `${parentName}.${name}`),
|
|
2890
|
+
kind: "MethodDef",
|
|
2891
|
+
name,
|
|
2892
|
+
scopePath: `${parentName}.${name}`,
|
|
2893
|
+
span: [0, 0],
|
|
2894
|
+
rawText: trimmed,
|
|
2895
|
+
signature: normalizeSignature6(trimmed),
|
|
2896
|
+
children: []
|
|
2897
|
+
});
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const fieldMatch = trimmed.match(FIELD_RE);
|
|
2902
|
+
if (fieldMatch) {
|
|
2903
|
+
const name = fieldMatch[1];
|
|
2904
|
+
if (name !== "return" && name !== "throw" && name !== "new") {
|
|
2905
|
+
children.push({
|
|
2906
|
+
id: makeEntityId6(filePath, "PropertyDef", `${parentName}.${name}`),
|
|
2907
|
+
kind: "PropertyDef",
|
|
2908
|
+
name,
|
|
2909
|
+
scopePath: `${parentName}.${name}`,
|
|
2910
|
+
span: [0, 0],
|
|
2911
|
+
rawText: trimmed,
|
|
2912
|
+
signature: normalizeSignature6(trimmed),
|
|
2913
|
+
children: []
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return children;
|
|
2919
|
+
}
|
|
2920
|
+
function extractImports6(content) {
|
|
2921
|
+
const imports = [];
|
|
2922
|
+
const lines = content.split(`
|
|
2923
|
+
`);
|
|
2924
|
+
for (const line of lines) {
|
|
2925
|
+
const trimmed = line.trim();
|
|
2926
|
+
const match = trimmed.match(/^import\s+(static\s+)?([^;]+);/);
|
|
2927
|
+
if (match) {
|
|
2928
|
+
const isStatic = !!match[1];
|
|
2929
|
+
const path = match[2].trim();
|
|
2930
|
+
const isWildcard = path.endsWith(".*");
|
|
2931
|
+
const source = isWildcard ? path.slice(0, -2) : path;
|
|
2932
|
+
const lastPart = path.split(".").pop();
|
|
2933
|
+
imports.push({
|
|
2934
|
+
source,
|
|
2935
|
+
specifiers: isWildcard ? ["*"] : [lastPart],
|
|
2936
|
+
isDefault: !isWildcard && !isStatic,
|
|
2937
|
+
isNamespace: isWildcard,
|
|
2938
|
+
rawText: trimmed,
|
|
2939
|
+
span: [0, trimmed.length]
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return imports;
|
|
2944
|
+
}
|
|
2945
|
+
function extractExports6(content, filePath) {
|
|
2946
|
+
const exports = [];
|
|
2947
|
+
const lines = content.split(`
|
|
2948
|
+
`);
|
|
2949
|
+
for (const line of lines) {
|
|
2950
|
+
const trimmed = line.trim();
|
|
2951
|
+
const match = trimmed.match(/^public\s+(?:(?:abstract|final|sealed|static)\s+)*(class|interface|enum|record)\s+(\w+)/);
|
|
2952
|
+
if (match) {
|
|
2953
|
+
exports.push({
|
|
2954
|
+
name: match[2],
|
|
2955
|
+
isDefault: false,
|
|
2956
|
+
rawText: trimmed.split("{")[0].trim(),
|
|
2957
|
+
span: [0, 0]
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
return exports;
|
|
2962
|
+
}
|
|
2963
|
+
function computeSemanticDiff6(oldResult, newResult) {
|
|
2964
|
+
const patches = [];
|
|
2965
|
+
const fileId = newResult.fileEntityId;
|
|
2966
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
2967
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
2968
|
+
for (const [id, entity] of newDecls) {
|
|
2969
|
+
if (!oldDecls.has(id)) {
|
|
2970
|
+
const oldEntity = findRenamedEntity6(entity, oldResult.declarations, newDecls);
|
|
2971
|
+
if (oldEntity) {
|
|
2972
|
+
patches.push({ kind: "symbolRename", entityId: oldEntity.id, oldName: oldEntity.name, newName: entity.name });
|
|
2973
|
+
} else {
|
|
2974
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
for (const [id, entity] of oldDecls) {
|
|
2979
|
+
if (!newDecls.has(id)) {
|
|
2980
|
+
const wasRenamed = findRenamedEntity6(entity, newResult.declarations, oldDecls);
|
|
2981
|
+
if (!wasRenamed) {
|
|
2982
|
+
patches.push({ kind: "symbolRemove", entityId: id, entityName: entity.name });
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
for (const [id, newEntity] of newDecls) {
|
|
2987
|
+
const oldEntity = oldDecls.get(id);
|
|
2988
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
2989
|
+
patches.push({
|
|
2990
|
+
kind: "symbolModify",
|
|
2991
|
+
entityId: id,
|
|
2992
|
+
entityName: newEntity.name,
|
|
2993
|
+
oldSignature: oldEntity.signature,
|
|
2994
|
+
newSignature: newEntity.signature,
|
|
2995
|
+
oldRawText: oldEntity.rawText,
|
|
2996
|
+
newRawText: newEntity.rawText
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
3001
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
3002
|
+
for (const [source, imp] of newImports) {
|
|
3003
|
+
const oldImp = oldImports.get(source);
|
|
3004
|
+
if (!oldImp) {
|
|
3005
|
+
patches.push({ kind: "importAdd", fileId, source, specifiers: imp.specifiers, rawText: imp.rawText });
|
|
3006
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
3007
|
+
patches.push({ kind: "importModify", fileId, source, oldSpecifiers: oldImp.specifiers, newSpecifiers: imp.specifiers });
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
for (const [source] of oldImports) {
|
|
3011
|
+
if (!newImports.has(source)) {
|
|
3012
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
3016
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
3017
|
+
for (const [name, exp] of newExports) {
|
|
3018
|
+
if (!oldExports.has(name)) {
|
|
3019
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
for (const [name] of oldExports) {
|
|
3023
|
+
if (!newExports.has(name)) {
|
|
3024
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
return patches;
|
|
3028
|
+
}
|
|
3029
|
+
function findRenamedEntity6(entity, candidates, existingIds) {
|
|
3030
|
+
for (const candidate of candidates) {
|
|
3031
|
+
if (candidate.kind !== entity.kind)
|
|
3032
|
+
continue;
|
|
3033
|
+
if (candidate.name === entity.name)
|
|
3034
|
+
continue;
|
|
3035
|
+
if (existingIds.has(candidate.id))
|
|
3036
|
+
continue;
|
|
3037
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
3038
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
3039
|
+
if (normalizedOld === normalizedNew)
|
|
3040
|
+
return candidate;
|
|
3041
|
+
}
|
|
3042
|
+
return null;
|
|
3043
|
+
}
|
|
3044
|
+
function makeEntityId6(filePath, kind, name) {
|
|
3045
|
+
return `${kind}:${filePath}:${name}`;
|
|
3046
|
+
}
|
|
3047
|
+
function normalizeSignature6(text) {
|
|
3048
|
+
return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
3049
|
+
}
|
|
3050
|
+
// src/semantic/csharp-parser.ts
|
|
3051
|
+
var csharpParser = {
|
|
3052
|
+
languages: ["csharp"],
|
|
3053
|
+
parse(content, filePath) {
|
|
3054
|
+
const fileEntityId = `file:${filePath}`;
|
|
3055
|
+
return {
|
|
3056
|
+
fileEntityId,
|
|
3057
|
+
filePath,
|
|
3058
|
+
language: "csharp",
|
|
3059
|
+
declarations: extractDeclarations7(content, filePath),
|
|
3060
|
+
imports: extractImports7(content),
|
|
3061
|
+
exports: extractExports7(content)
|
|
3062
|
+
};
|
|
3063
|
+
},
|
|
3064
|
+
diff(oldResult, newResult) {
|
|
3065
|
+
return computeSemanticDiff7(oldResult, newResult);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
var MODIFIERS_RE = /(?:(?:public|private|protected|internal|static|abstract|sealed|virtual|override|partial|readonly|async|extern|unsafe|volatile|new)\s+)*/;
|
|
3069
|
+
var TYPE_DEF_RE = new RegExp(`^${MODIFIERS_RE.source}(class|interface|struct|enum|record)\\s+(\\w+)`);
|
|
3070
|
+
var METHOD_RE2 = new RegExp(`^${MODIFIERS_RE.source}(?:\\w[\\w<>\\[\\],\\s?]*?)\\s+(\\w+)\\s*[(<]`);
|
|
3071
|
+
var PROP_RE = new RegExp(`^${MODIFIERS_RE.source}(?:\\w[\\w<>\\[\\],\\s?]*?)\\s+(\\w+)\\s*\\{`);
|
|
3072
|
+
var FIELD_RE2 = new RegExp(`^${MODIFIERS_RE.source}(?:\\w[\\w<>\\[\\],\\s?]*?)\\s+(\\w+)\\s*[;=]`);
|
|
3073
|
+
function extractDeclarations7(content, filePath) {
|
|
3074
|
+
const declarations = [];
|
|
3075
|
+
const lines = content.split(`
|
|
3076
|
+
`);
|
|
3077
|
+
let i = 0;
|
|
3078
|
+
while (i < lines.length) {
|
|
3079
|
+
const trimmed = lines[i].trim();
|
|
3080
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("using ") || trimmed.startsWith("namespace ")) {
|
|
3081
|
+
i++;
|
|
3082
|
+
continue;
|
|
3083
|
+
}
|
|
3084
|
+
const attributes = [];
|
|
3085
|
+
const attrStart = i;
|
|
3086
|
+
while (i < lines.length && lines[i].trim().startsWith("[")) {
|
|
3087
|
+
attributes.push(lines[i].trim());
|
|
3088
|
+
i++;
|
|
3089
|
+
}
|
|
3090
|
+
if (i >= lines.length)
|
|
3091
|
+
break;
|
|
3092
|
+
const declLine = lines[i].trim();
|
|
3093
|
+
const typeMatch = declLine.match(TYPE_DEF_RE);
|
|
3094
|
+
if (typeMatch) {
|
|
3095
|
+
const typeKind = typeMatch[1];
|
|
3096
|
+
const name = typeMatch[2];
|
|
3097
|
+
const kind = typeKind === "interface" ? "InterfaceDef" : typeKind === "enum" ? "EnumDef" : "ClassDef";
|
|
3098
|
+
const result = extractBraceBlock4(name, kind, lines, attributes.length > 0 ? attrStart : i, i, filePath);
|
|
3099
|
+
result.entity.children = extractTypeMembers(lines, i, result.endLine, name, filePath);
|
|
3100
|
+
declarations.push(result.entity);
|
|
3101
|
+
i = result.endLine + 1;
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
if (attributes.length > 0) {
|
|
3105
|
+
i++;
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
i++;
|
|
3109
|
+
}
|
|
3110
|
+
return declarations;
|
|
3111
|
+
}
|
|
3112
|
+
function extractBraceBlock4(name, kind, lines, startLine, defLine, filePath) {
|
|
3113
|
+
let depth = 0;
|
|
3114
|
+
let foundOpen = false;
|
|
3115
|
+
let endLine = defLine;
|
|
3116
|
+
for (let i = defLine;i < lines.length; i++) {
|
|
3117
|
+
for (const ch of lines[i]) {
|
|
3118
|
+
if (ch === "{") {
|
|
3119
|
+
depth++;
|
|
3120
|
+
foundOpen = true;
|
|
3121
|
+
} else if (ch === "}")
|
|
3122
|
+
depth--;
|
|
3123
|
+
}
|
|
3124
|
+
if (foundOpen && depth <= 0) {
|
|
3125
|
+
endLine = i;
|
|
3126
|
+
break;
|
|
3127
|
+
}
|
|
3128
|
+
if (i > defLine + 500) {
|
|
3129
|
+
endLine = i;
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3132
|
+
endLine = i;
|
|
3133
|
+
}
|
|
3134
|
+
const rawText = lines.slice(startLine, endLine + 1).join(`
|
|
3135
|
+
`);
|
|
3136
|
+
const startOffset = lines.slice(0, startLine).join(`
|
|
3137
|
+
`).length + (startLine > 0 ? 1 : 0);
|
|
3138
|
+
return {
|
|
3139
|
+
entity: {
|
|
3140
|
+
id: makeEntityId7(filePath, kind, name),
|
|
3141
|
+
kind,
|
|
3142
|
+
name,
|
|
3143
|
+
scopePath: name,
|
|
3144
|
+
span: [startOffset, startOffset + rawText.length],
|
|
3145
|
+
rawText,
|
|
3146
|
+
signature: normalizeSignature7(rawText),
|
|
3147
|
+
children: []
|
|
3148
|
+
},
|
|
3149
|
+
endLine
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
var CONTROL_FLOW = new Set(["if", "for", "foreach", "while", "switch", "catch", "using", "lock", "try", "else", "return", "throw", "new", "yield", "await"]);
|
|
3153
|
+
function extractTypeMembers(lines, startLine, endLine, parentName, filePath) {
|
|
3154
|
+
const children = [];
|
|
3155
|
+
let depth = 0;
|
|
3156
|
+
for (let i = startLine;i <= endLine; i++) {
|
|
3157
|
+
const line = lines[i];
|
|
3158
|
+
const depthBefore = depth;
|
|
3159
|
+
for (const ch of line) {
|
|
3160
|
+
if (ch === "{")
|
|
3161
|
+
depth++;
|
|
3162
|
+
else if (ch === "}")
|
|
3163
|
+
depth--;
|
|
3164
|
+
}
|
|
3165
|
+
if (depthBefore !== 1)
|
|
3166
|
+
continue;
|
|
3167
|
+
const trimmed = line.trim();
|
|
3168
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("["))
|
|
3169
|
+
continue;
|
|
3170
|
+
const nestedMatch = trimmed.match(TYPE_DEF_RE);
|
|
3171
|
+
if (nestedMatch) {
|
|
3172
|
+
const typeKind = nestedMatch[1];
|
|
3173
|
+
const name = nestedMatch[2];
|
|
3174
|
+
const kind = typeKind === "interface" ? "InterfaceDef" : typeKind === "enum" ? "EnumDef" : "ClassDef";
|
|
3175
|
+
children.push({
|
|
3176
|
+
id: makeEntityId7(filePath, kind, `${parentName}.${name}`),
|
|
3177
|
+
kind,
|
|
3178
|
+
name,
|
|
3179
|
+
scopePath: `${parentName}.${name}`,
|
|
3180
|
+
span: [0, 0],
|
|
3181
|
+
rawText: trimmed,
|
|
3182
|
+
signature: normalizeSignature7(trimmed),
|
|
3183
|
+
children: []
|
|
3184
|
+
});
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
const ctorMatch = trimmed.match(new RegExp(`^${MODIFIERS_RE.source}${parentName}\\s*\\(`));
|
|
3188
|
+
if (ctorMatch) {
|
|
3189
|
+
children.push({
|
|
3190
|
+
id: makeEntityId7(filePath, "Constructor", `${parentName}.${parentName}`),
|
|
3191
|
+
kind: "Constructor",
|
|
3192
|
+
name: parentName,
|
|
3193
|
+
scopePath: `${parentName}.${parentName}`,
|
|
3194
|
+
span: [0, 0],
|
|
3195
|
+
rawText: trimmed,
|
|
3196
|
+
signature: normalizeSignature7(trimmed),
|
|
3197
|
+
children: []
|
|
3198
|
+
});
|
|
3199
|
+
continue;
|
|
3200
|
+
}
|
|
3201
|
+
const propMatch = trimmed.match(PROP_RE);
|
|
3202
|
+
if (propMatch) {
|
|
3203
|
+
const name = propMatch[1];
|
|
3204
|
+
if (!CONTROL_FLOW.has(name)) {
|
|
3205
|
+
const restOfLine = trimmed.slice(trimmed.indexOf(name) + name.length).trim();
|
|
3206
|
+
if (restOfLine.startsWith("{") && (restOfLine.includes("get") || restOfLine.includes("set") || restOfLine.includes("=>") || !restOfLine.includes("("))) {
|
|
3207
|
+
children.push({
|
|
3208
|
+
id: makeEntityId7(filePath, "PropertyDef", `${parentName}.${name}`),
|
|
3209
|
+
kind: "PropertyDef",
|
|
3210
|
+
name,
|
|
3211
|
+
scopePath: `${parentName}.${name}`,
|
|
3212
|
+
span: [0, 0],
|
|
3213
|
+
rawText: trimmed,
|
|
3214
|
+
signature: normalizeSignature7(trimmed),
|
|
3215
|
+
children: []
|
|
3216
|
+
});
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
const methodMatch = trimmed.match(METHOD_RE2);
|
|
3222
|
+
if (methodMatch) {
|
|
3223
|
+
const name = methodMatch[1];
|
|
3224
|
+
if (!CONTROL_FLOW.has(name) && name !== parentName) {
|
|
3225
|
+
children.push({
|
|
3226
|
+
id: makeEntityId7(filePath, "MethodDef", `${parentName}.${name}`),
|
|
3227
|
+
kind: "MethodDef",
|
|
3228
|
+
name,
|
|
3229
|
+
scopePath: `${parentName}.${name}`,
|
|
3230
|
+
span: [0, 0],
|
|
3231
|
+
rawText: trimmed,
|
|
3232
|
+
signature: normalizeSignature7(trimmed),
|
|
3233
|
+
children: []
|
|
3234
|
+
});
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
const fieldMatch = trimmed.match(FIELD_RE2);
|
|
3239
|
+
if (fieldMatch) {
|
|
3240
|
+
const name = fieldMatch[1];
|
|
3241
|
+
if (!CONTROL_FLOW.has(name) && name !== "var") {
|
|
3242
|
+
children.push({
|
|
3243
|
+
id: makeEntityId7(filePath, "PropertyDef", `${parentName}.${name}`),
|
|
3244
|
+
kind: "PropertyDef",
|
|
3245
|
+
name,
|
|
3246
|
+
scopePath: `${parentName}.${name}`,
|
|
3247
|
+
span: [0, 0],
|
|
3248
|
+
rawText: trimmed,
|
|
3249
|
+
signature: normalizeSignature7(trimmed),
|
|
3250
|
+
children: []
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
return children;
|
|
3256
|
+
}
|
|
3257
|
+
function extractImports7(content) {
|
|
3258
|
+
const imports = [];
|
|
3259
|
+
const lines = content.split(`
|
|
3260
|
+
`);
|
|
3261
|
+
for (const line of lines) {
|
|
3262
|
+
const trimmed = line.trim();
|
|
3263
|
+
let match = trimmed.match(/^using\s+([A-Z][\w.]+)\s*;/);
|
|
3264
|
+
if (match) {
|
|
3265
|
+
imports.push({
|
|
3266
|
+
source: match[1],
|
|
3267
|
+
specifiers: [match[1].split(".").pop()],
|
|
3268
|
+
isDefault: false,
|
|
3269
|
+
isNamespace: true,
|
|
3270
|
+
rawText: trimmed,
|
|
3271
|
+
span: [0, trimmed.length]
|
|
3272
|
+
});
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
match = trimmed.match(/^using\s+static\s+([A-Z][\w.]+)\s*;/);
|
|
3276
|
+
if (match) {
|
|
3277
|
+
imports.push({
|
|
3278
|
+
source: match[1],
|
|
3279
|
+
specifiers: [match[1].split(".").pop()],
|
|
3280
|
+
isDefault: false,
|
|
3281
|
+
isNamespace: false,
|
|
3282
|
+
rawText: trimmed,
|
|
3283
|
+
span: [0, trimmed.length]
|
|
3284
|
+
});
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
match = trimmed.match(/^using\s+(\w+)\s*=\s*([^;]+);/);
|
|
3288
|
+
if (match) {
|
|
3289
|
+
imports.push({
|
|
3290
|
+
source: match[2].trim(),
|
|
3291
|
+
specifiers: [match[1]],
|
|
3292
|
+
isDefault: true,
|
|
3293
|
+
isNamespace: false,
|
|
3294
|
+
rawText: trimmed,
|
|
3295
|
+
span: [0, trimmed.length]
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return imports;
|
|
3300
|
+
}
|
|
3301
|
+
function extractExports7(content) {
|
|
3302
|
+
const exports = [];
|
|
3303
|
+
const lines = content.split(`
|
|
3304
|
+
`);
|
|
3305
|
+
for (const line of lines) {
|
|
3306
|
+
const trimmed = line.trim();
|
|
3307
|
+
const match = trimmed.match(/^public\s+(?:(?:abstract|sealed|static|partial)\s+)*(class|interface|struct|enum|record)\s+(\w+)/);
|
|
3308
|
+
if (match) {
|
|
3309
|
+
exports.push({
|
|
3310
|
+
name: match[2],
|
|
3311
|
+
isDefault: false,
|
|
3312
|
+
rawText: trimmed.split("{")[0].trim(),
|
|
3313
|
+
span: [0, 0]
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return exports;
|
|
3318
|
+
}
|
|
3319
|
+
function computeSemanticDiff7(oldResult, newResult) {
|
|
3320
|
+
const patches = [];
|
|
3321
|
+
const fileId = newResult.fileEntityId;
|
|
3322
|
+
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
3323
|
+
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
3324
|
+
for (const [id, entity] of newDecls) {
|
|
3325
|
+
if (!oldDecls.has(id)) {
|
|
3326
|
+
const oldEntity = findRenamedEntity7(entity, oldResult.declarations, newDecls);
|
|
3327
|
+
if (oldEntity) {
|
|
3328
|
+
patches.push({ kind: "symbolRename", entityId: oldEntity.id, oldName: oldEntity.name, newName: entity.name });
|
|
3329
|
+
} else {
|
|
3330
|
+
patches.push({ kind: "symbolAdd", entity });
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
for (const [id, entity] of oldDecls) {
|
|
3335
|
+
if (!newDecls.has(id)) {
|
|
3336
|
+
const wasRenamed = findRenamedEntity7(entity, newResult.declarations, oldDecls);
|
|
3337
|
+
if (!wasRenamed) {
|
|
3338
|
+
patches.push({ kind: "symbolRemove", entityId: id, entityName: entity.name });
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
for (const [id, newEntity] of newDecls) {
|
|
3343
|
+
const oldEntity = oldDecls.get(id);
|
|
3344
|
+
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
3345
|
+
patches.push({
|
|
3346
|
+
kind: "symbolModify",
|
|
3347
|
+
entityId: id,
|
|
3348
|
+
entityName: newEntity.name,
|
|
3349
|
+
oldSignature: oldEntity.signature,
|
|
3350
|
+
newSignature: newEntity.signature,
|
|
3351
|
+
oldRawText: oldEntity.rawText,
|
|
3352
|
+
newRawText: newEntity.rawText
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
3357
|
+
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
3358
|
+
for (const [source, imp] of newImports) {
|
|
3359
|
+
const oldImp = oldImports.get(source);
|
|
3360
|
+
if (!oldImp) {
|
|
3361
|
+
patches.push({ kind: "importAdd", fileId, source, specifiers: imp.specifiers, rawText: imp.rawText });
|
|
3362
|
+
} else if (JSON.stringify(oldImp.specifiers.sort()) !== JSON.stringify(imp.specifiers.sort())) {
|
|
3363
|
+
patches.push({ kind: "importModify", fileId, source, oldSpecifiers: oldImp.specifiers, newSpecifiers: imp.specifiers });
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
for (const [source] of oldImports) {
|
|
3367
|
+
if (!newImports.has(source)) {
|
|
3368
|
+
patches.push({ kind: "importRemove", fileId, source });
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
3372
|
+
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
3373
|
+
for (const [name, exp] of newExports) {
|
|
3374
|
+
if (!oldExports.has(name)) {
|
|
3375
|
+
patches.push({ kind: "exportAdd", fileId, name, rawText: exp.rawText });
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
for (const [name] of oldExports) {
|
|
3379
|
+
if (!newExports.has(name)) {
|
|
3380
|
+
patches.push({ kind: "exportRemove", fileId, name });
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return patches;
|
|
3384
|
+
}
|
|
3385
|
+
function findRenamedEntity7(entity, candidates, existingIds) {
|
|
3386
|
+
for (const candidate of candidates) {
|
|
3387
|
+
if (candidate.kind !== entity.kind)
|
|
3388
|
+
continue;
|
|
3389
|
+
if (candidate.name === entity.name)
|
|
3390
|
+
continue;
|
|
3391
|
+
if (existingIds.has(candidate.id))
|
|
3392
|
+
continue;
|
|
3393
|
+
const normalizedOld = candidate.signature.replace(new RegExp(candidate.name, "g"), "___");
|
|
3394
|
+
const normalizedNew = entity.signature.replace(new RegExp(entity.name, "g"), "___");
|
|
3395
|
+
if (normalizedOld === normalizedNew)
|
|
3396
|
+
return candidate;
|
|
3397
|
+
}
|
|
3398
|
+
return null;
|
|
3399
|
+
}
|
|
3400
|
+
function makeEntityId7(filePath, kind, name) {
|
|
3401
|
+
return `${kind}:${filePath}:${name}`;
|
|
3402
|
+
}
|
|
3403
|
+
function normalizeSignature7(text) {
|
|
3404
|
+
return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
3405
|
+
}
|
|
3406
|
+
// src/engine.ts
|
|
3407
|
+
class JsonOpLog {
|
|
3408
|
+
ops = [];
|
|
3409
|
+
filePath;
|
|
3410
|
+
constructor(filePath) {
|
|
3411
|
+
this.filePath = filePath;
|
|
3412
|
+
}
|
|
3413
|
+
load() {
|
|
3414
|
+
if (existsSync(this.filePath)) {
|
|
3415
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
3416
|
+
try {
|
|
3417
|
+
this.ops = JSON.parse(raw);
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
const backupPath = this.filePath + ".bak";
|
|
3420
|
+
if (existsSync(backupPath)) {
|
|
3421
|
+
const backupRaw = readFileSync(backupPath, "utf-8");
|
|
3422
|
+
this.ops = JSON.parse(backupRaw);
|
|
3423
|
+
writeFileSync(this.filePath, backupRaw);
|
|
3424
|
+
} else {
|
|
3425
|
+
throw new Error(`Corrupted ops.json and no backup found. Run \`trellis repair\` to attempt recovery.`);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
append(op) {
|
|
3431
|
+
this.ops.push(op);
|
|
3432
|
+
this.flush();
|
|
3433
|
+
}
|
|
3434
|
+
readAll() {
|
|
3435
|
+
return [...this.ops];
|
|
3436
|
+
}
|
|
3437
|
+
getLastOp() {
|
|
3438
|
+
return this.ops.length > 0 ? this.ops[this.ops.length - 1] : undefined;
|
|
3439
|
+
}
|
|
3440
|
+
count() {
|
|
3441
|
+
return this.ops.length;
|
|
3442
|
+
}
|
|
3443
|
+
flush() {
|
|
3444
|
+
const dir = dirname(this.filePath);
|
|
3445
|
+
if (!existsSync(dir))
|
|
3446
|
+
mkdirSync(dir, { recursive: true });
|
|
3447
|
+
if (existsSync(this.filePath)) {
|
|
3448
|
+
const backupPath = this.filePath + ".bak";
|
|
3449
|
+
try {
|
|
3450
|
+
copyFileSync(this.filePath, backupPath);
|
|
3451
|
+
} catch {}
|
|
3452
|
+
}
|
|
3453
|
+
writeFileSync(this.filePath, JSON.stringify(this.ops, null, 2));
|
|
3454
|
+
}
|
|
3455
|
+
static repair(filePath) {
|
|
3456
|
+
if (!existsSync(filePath)) {
|
|
3457
|
+
return { recovered: 0, lost: 0 };
|
|
3458
|
+
}
|
|
3459
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
3460
|
+
try {
|
|
3461
|
+
const ops = JSON.parse(raw);
|
|
3462
|
+
return { recovered: ops.length, lost: 0 };
|
|
3463
|
+
} catch {}
|
|
3464
|
+
const lastHash = raw.lastIndexOf('"hash": "trellis:op:');
|
|
3465
|
+
if (lastHash === -1) {
|
|
3466
|
+
const bakPath = filePath + ".bak";
|
|
3467
|
+
if (existsSync(bakPath)) {
|
|
3468
|
+
const bakRaw = readFileSync(bakPath, "utf-8");
|
|
3469
|
+
try {
|
|
3470
|
+
const ops = JSON.parse(bakRaw);
|
|
3471
|
+
writeFileSync(filePath, bakRaw);
|
|
3472
|
+
return { recovered: ops.length, lost: 0 };
|
|
3473
|
+
} catch {}
|
|
3474
|
+
}
|
|
3475
|
+
writeFileSync(filePath, "[]");
|
|
3476
|
+
return { recovered: 0, lost: -1 };
|
|
3477
|
+
}
|
|
3478
|
+
const endOfLine = raw.indexOf(`
|
|
3479
|
+
`, lastHash);
|
|
3480
|
+
const closingBrace = raw.indexOf(" }", endOfLine);
|
|
3481
|
+
if (closingBrace === -1) {
|
|
3482
|
+
writeFileSync(filePath, "[]");
|
|
3483
|
+
return { recovered: 0, lost: -1 };
|
|
3484
|
+
}
|
|
3485
|
+
const fixed = raw.slice(0, closingBrace + 3) + `
|
|
3486
|
+
]`;
|
|
3487
|
+
try {
|
|
3488
|
+
const ops = JSON.parse(fixed);
|
|
3489
|
+
writeFileSync(filePath + ".corrupted", raw);
|
|
3490
|
+
writeFileSync(filePath, fixed);
|
|
3491
|
+
return { recovered: ops.length, lost: 0 };
|
|
3492
|
+
} catch {
|
|
3493
|
+
writeFileSync(filePath + ".corrupted", raw);
|
|
3494
|
+
writeFileSync(filePath, "[]");
|
|
3495
|
+
return { recovered: 0, lost: -1 };
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
function parseIgnoreFile(filePath) {
|
|
3500
|
+
if (!existsSync(filePath))
|
|
3501
|
+
return [];
|
|
3502
|
+
try {
|
|
3503
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3504
|
+
return content.split(`
|
|
3505
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => line.replace(/\/$/, ""));
|
|
3506
|
+
} catch {
|
|
3507
|
+
return [];
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
function readIgnorePatterns(rootPath) {
|
|
3511
|
+
return [
|
|
3512
|
+
...parseIgnoreFile(join2(rootPath, ".gitignore")),
|
|
3513
|
+
...parseIgnoreFile(join2(rootPath, ".trellisignore"))
|
|
3514
|
+
];
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
class TrellisVcsEngine {
|
|
3518
|
+
config;
|
|
3519
|
+
store;
|
|
3520
|
+
opLog;
|
|
3521
|
+
watcher = null;
|
|
3522
|
+
ingestion = null;
|
|
3523
|
+
agentId;
|
|
3524
|
+
currentBranch = "main";
|
|
3525
|
+
checkpointOpCount = 0;
|
|
3526
|
+
checkpointThreshold = 100;
|
|
3527
|
+
_pendingAutoCheckpoint = false;
|
|
3528
|
+
_blobStore = null;
|
|
3529
|
+
constructor(opts) {
|
|
3530
|
+
const gitignorePatterns = readIgnorePatterns(opts.rootPath);
|
|
3531
|
+
const mergedIgnore = [
|
|
3532
|
+
...new Set([
|
|
3533
|
+
...opts.ignorePatterns ?? DEFAULT_CONFIG.ignorePatterns,
|
|
3534
|
+
...gitignorePatterns
|
|
3535
|
+
])
|
|
3536
|
+
];
|
|
3537
|
+
this.config = {
|
|
3538
|
+
rootPath: opts.rootPath,
|
|
3539
|
+
ignorePatterns: mergedIgnore,
|
|
3540
|
+
debounceMs: opts.debounceMs ?? DEFAULT_CONFIG.debounceMs,
|
|
3541
|
+
defaultBranch: opts.defaultBranch ?? DEFAULT_CONFIG.defaultBranch,
|
|
3542
|
+
dbPath: opts.dbPath ?? DEFAULT_CONFIG.dbPath
|
|
3543
|
+
};
|
|
3544
|
+
this.agentId = opts.agentId ?? `agent:${process.env.USER ?? "unknown"}`;
|
|
3545
|
+
this.store = new EAVStore;
|
|
3546
|
+
this.opLog = new JsonOpLog(join2(this.config.rootPath, ".trellis", "ops.json"));
|
|
3547
|
+
}
|
|
3548
|
+
async initRepo() {
|
|
3549
|
+
const trellisDir = join2(this.config.rootPath, ".trellis");
|
|
3550
|
+
if (!existsSync(trellisDir)) {
|
|
3551
|
+
mkdirSync(trellisDir, { recursive: true });
|
|
3552
|
+
}
|
|
3553
|
+
this._blobStore = new BlobStore(trellisDir);
|
|
3554
|
+
const configPath = join2(trellisDir, "config.json");
|
|
3555
|
+
const persistedConfig = {
|
|
3556
|
+
rootPath: this.config.rootPath,
|
|
3557
|
+
ignorePatterns: this.config.ignorePatterns,
|
|
3558
|
+
debounceMs: this.config.debounceMs,
|
|
3559
|
+
defaultBranch: this.config.defaultBranch,
|
|
3560
|
+
agentId: this.agentId,
|
|
3561
|
+
createdAt: new Date().toISOString()
|
|
3562
|
+
};
|
|
3563
|
+
writeFileSync(configPath, JSON.stringify(persistedConfig, null, 2));
|
|
3564
|
+
this.opLog.load();
|
|
3565
|
+
const branchOp = await createVcsOp("vcs:branchCreate", {
|
|
3566
|
+
agentId: this.agentId,
|
|
3567
|
+
previousHash: this.opLog.getLastOp()?.hash,
|
|
3568
|
+
vcs: {
|
|
3569
|
+
branchName: this.config.defaultBranch
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
this.applyOp(branchOp);
|
|
3573
|
+
const scanner = new FileWatcher({
|
|
3574
|
+
rootPath: this.config.rootPath,
|
|
3575
|
+
ignorePatterns: [...this.config.ignorePatterns, ".trellis"],
|
|
3576
|
+
debounceMs: this.config.debounceMs,
|
|
3577
|
+
onEvent: () => {}
|
|
3578
|
+
});
|
|
3579
|
+
const events = await scanner.scan();
|
|
3580
|
+
let opsCreated = 1;
|
|
3581
|
+
for (const event of events) {
|
|
3582
|
+
if (event.contentHash) {
|
|
3583
|
+
try {
|
|
3584
|
+
const absPath = join2(this.config.rootPath, event.path);
|
|
3585
|
+
const content = await readFile2(absPath);
|
|
3586
|
+
await this._blobStore.put(content);
|
|
3587
|
+
} catch {}
|
|
3588
|
+
}
|
|
3589
|
+
const op = await createVcsOp("vcs:fileAdd", {
|
|
3590
|
+
agentId: this.agentId,
|
|
3591
|
+
previousHash: this.opLog.getLastOp()?.hash,
|
|
3592
|
+
vcs: {
|
|
3593
|
+
filePath: event.path,
|
|
3594
|
+
contentHash: event.contentHash,
|
|
3595
|
+
size: event.size
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3598
|
+
this.applyOp(op);
|
|
3599
|
+
opsCreated++;
|
|
3600
|
+
}
|
|
3601
|
+
await this.flushAutoCheckpoint();
|
|
3602
|
+
return { opsCreated };
|
|
3603
|
+
}
|
|
3604
|
+
open() {
|
|
3605
|
+
this.opLog.load();
|
|
3606
|
+
const trellisDir = join2(this.config.rootPath, ".trellis");
|
|
3607
|
+
this._blobStore = new BlobStore(trellisDir);
|
|
3608
|
+
const configPath = join2(this.config.rootPath, ".trellis", "config.json");
|
|
3609
|
+
if (existsSync(configPath)) {
|
|
3610
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
3611
|
+
const persisted = JSON.parse(raw);
|
|
3612
|
+
this.agentId = persisted.agentId;
|
|
3613
|
+
const filePatterns = readIgnorePatterns(this.config.rootPath);
|
|
3614
|
+
this.config.ignorePatterns = [
|
|
3615
|
+
...new Set([...persisted.ignorePatterns, ...filePatterns])
|
|
3616
|
+
];
|
|
3617
|
+
this.config.debounceMs = persisted.debounceMs;
|
|
3618
|
+
this.config.defaultBranch = persisted.defaultBranch;
|
|
3619
|
+
}
|
|
3620
|
+
this.loadCurrentBranch();
|
|
3621
|
+
const ops = this.opLog.readAll();
|
|
3622
|
+
for (const op of ops) {
|
|
3623
|
+
this.replayOp(op);
|
|
3624
|
+
}
|
|
3625
|
+
return { opsReplayed: ops.length };
|
|
3626
|
+
}
|
|
3627
|
+
watch() {
|
|
3628
|
+
this.ingestion = new Ingestion({
|
|
3629
|
+
agentId: this.agentId,
|
|
3630
|
+
lastOpHash: this.opLog.getLastOp()?.hash,
|
|
3631
|
+
onOp: (op) => this.applyOp(op)
|
|
3632
|
+
});
|
|
3633
|
+
this.watcher = new FileWatcher({
|
|
3634
|
+
rootPath: this.config.rootPath,
|
|
3635
|
+
ignorePatterns: [...this.config.ignorePatterns, ".trellis"],
|
|
3636
|
+
debounceMs: this.config.debounceMs,
|
|
3637
|
+
onEvent: async (event) => {
|
|
3638
|
+
if ((event.type === "add" || event.type === "modify") && event.contentHash && this._blobStore) {
|
|
3639
|
+
try {
|
|
3640
|
+
const absPath = join2(this.config.rootPath, event.path);
|
|
3641
|
+
const content = await readFile2(absPath);
|
|
3642
|
+
await this._blobStore.put(content);
|
|
3643
|
+
} catch {}
|
|
3644
|
+
}
|
|
3645
|
+
await this.ingestion.process(event);
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
this.watcher.scan().then(async (scanEvents) => {
|
|
3649
|
+
const trackedPaths = new Set(this.trackedFiles().map((f) => f.path));
|
|
3650
|
+
for (const event of scanEvents) {
|
|
3651
|
+
if (!trackedPaths.has(event.path)) {
|
|
3652
|
+
if (event.contentHash && this._blobStore) {
|
|
3653
|
+
try {
|
|
3654
|
+
const absPath = join2(this.config.rootPath, event.path);
|
|
3655
|
+
const content = await readFile2(absPath);
|
|
3656
|
+
await this._blobStore.put(content);
|
|
3657
|
+
} catch {}
|
|
3658
|
+
}
|
|
3659
|
+
await this.ingestion.process(event);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
this.watcher.start();
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
stop() {
|
|
3666
|
+
this.watcher?.stop();
|
|
3667
|
+
this.watcher = null;
|
|
3668
|
+
this.ingestion = null;
|
|
3669
|
+
}
|
|
3670
|
+
getOps() {
|
|
3671
|
+
return this.opLog.readAll();
|
|
3672
|
+
}
|
|
3673
|
+
getOpCount() {
|
|
3674
|
+
return this.opLog.count();
|
|
3675
|
+
}
|
|
3676
|
+
getStore() {
|
|
3677
|
+
return this.store;
|
|
3678
|
+
}
|
|
3679
|
+
getBlobStore() {
|
|
3680
|
+
return this._blobStore;
|
|
3681
|
+
}
|
|
3682
|
+
status() {
|
|
3683
|
+
const ops = this.opLog.readAll();
|
|
3684
|
+
const fileEntities = this.store.getFactsByAttribute("type").filter((f) => f.v === "FileNode");
|
|
3685
|
+
return {
|
|
3686
|
+
branch: this.currentBranch,
|
|
3687
|
+
totalOps: ops.length,
|
|
3688
|
+
trackedFiles: fileEntities.length,
|
|
3689
|
+
lastOp: ops[ops.length - 1],
|
|
3690
|
+
recentOps: ops.slice(-10)
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
log(opts) {
|
|
3694
|
+
let ops = this.opLog.readAll();
|
|
3695
|
+
if (opts?.filePath) {
|
|
3696
|
+
ops = ops.filter((op) => {
|
|
3697
|
+
const vcs = op.vcs;
|
|
3698
|
+
return vcs?.filePath === opts.filePath || vcs?.oldFilePath === opts.filePath;
|
|
3699
|
+
});
|
|
3700
|
+
}
|
|
3701
|
+
if (opts?.limit) {
|
|
3702
|
+
ops = ops.slice(-opts.limit);
|
|
3703
|
+
}
|
|
3704
|
+
return ops;
|
|
3705
|
+
}
|
|
3706
|
+
trackedFiles() {
|
|
3707
|
+
const fileTypeFacts = this.store.getFactsByAttribute("type").filter((f) => f.v === "FileNode");
|
|
3708
|
+
return fileTypeFacts.map((f) => {
|
|
3709
|
+
const pathFacts = this.store.getFactsByEntity(f.e).filter((ef) => ef.a === "path");
|
|
3710
|
+
const hashFacts = this.store.getFactsByEntity(f.e).filter((ef) => ef.a === "contentHash");
|
|
3711
|
+
return {
|
|
3712
|
+
path: pathFacts[0]?.v ?? f.e,
|
|
3713
|
+
contentHash: hashFacts[0]?.v
|
|
3714
|
+
};
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
getRootPath() {
|
|
3718
|
+
return this.config.rootPath;
|
|
3719
|
+
}
|
|
3720
|
+
static isRepo(rootPath) {
|
|
3721
|
+
return existsSync(join2(rootPath, ".trellis", "config.json"));
|
|
3722
|
+
}
|
|
3723
|
+
static repair(rootPath) {
|
|
3724
|
+
const opsPath = join2(rootPath, ".trellis", "ops.json");
|
|
3725
|
+
return JsonOpLog.repair(opsPath);
|
|
3726
|
+
}
|
|
3727
|
+
async createBranch(name) {
|
|
3728
|
+
const op = await createBranch(this._ctx(), name, this.currentBranch);
|
|
3729
|
+
await this.flushAutoCheckpoint();
|
|
3730
|
+
return op;
|
|
3731
|
+
}
|
|
3732
|
+
switchBranch(name) {
|
|
3733
|
+
switchBranch(this._ctx(), name);
|
|
3734
|
+
this.currentBranch = name;
|
|
3735
|
+
saveBranchState(this.config.rootPath, { currentBranch: name });
|
|
3736
|
+
}
|
|
3737
|
+
listBranches() {
|
|
3738
|
+
return listBranches(this._ctx(), this.currentBranch);
|
|
3739
|
+
}
|
|
3740
|
+
async deleteBranch(name) {
|
|
3741
|
+
const op = await deleteBranch(this._ctx(), name, this.currentBranch);
|
|
3742
|
+
await this.flushAutoCheckpoint();
|
|
3743
|
+
return op;
|
|
3744
|
+
}
|
|
3745
|
+
getCurrentBranch() {
|
|
3746
|
+
return this.currentBranch;
|
|
3747
|
+
}
|
|
3748
|
+
async createMilestone(message, opts) {
|
|
3749
|
+
const op = await createMilestone(this._ctx(), message, opts);
|
|
3750
|
+
await this.flushAutoCheckpoint();
|
|
3751
|
+
return op;
|
|
3752
|
+
}
|
|
3753
|
+
listMilestones() {
|
|
3754
|
+
return listMilestones(this._ctx());
|
|
3755
|
+
}
|
|
3756
|
+
async createCheckpoint(trigger = "manual") {
|
|
3757
|
+
const op = await createCheckpoint(this._ctx(), trigger);
|
|
3758
|
+
this.checkpointOpCount = 0;
|
|
3759
|
+
return op;
|
|
3760
|
+
}
|
|
3761
|
+
listCheckpoints() {
|
|
3762
|
+
return listCheckpoints(this._ctx());
|
|
3763
|
+
}
|
|
3764
|
+
setCheckpointThreshold(threshold) {
|
|
3765
|
+
this.checkpointThreshold = threshold;
|
|
3766
|
+
}
|
|
3767
|
+
diffBranches(branchA, branchB) {
|
|
3768
|
+
const ops = this.opLog.readAll();
|
|
3769
|
+
const stateA = buildFileStateAtOp(ops);
|
|
3770
|
+
const stateB = buildFileStateAtOp(ops);
|
|
3771
|
+
return diffFileStates(stateA, stateB, this._blobStore);
|
|
3772
|
+
}
|
|
3773
|
+
diffOps(fromHash, toHash) {
|
|
3774
|
+
return diffOpRange(this.opLog.readAll(), fromHash, toHash, this._blobStore);
|
|
3775
|
+
}
|
|
3776
|
+
diffFromOp(opHash) {
|
|
3777
|
+
const ops = this.opLog.readAll();
|
|
3778
|
+
const stateA = buildFileStateAtOp(ops, opHash);
|
|
3779
|
+
const stateB = buildFileStateAtOp(ops);
|
|
3780
|
+
return diffFileStates(stateA, stateB, this._blobStore);
|
|
3781
|
+
}
|
|
3782
|
+
mergeBranch(sourceBranch) {
|
|
3783
|
+
const ops = this.opLog.readAll();
|
|
3784
|
+
const branchOp = ops.find((o) => o.kind === "vcs:branchCreate" && o.vcs?.branchName === sourceBranch);
|
|
3785
|
+
const forkHash = branchOp?.vcs?.targetOpHash;
|
|
3786
|
+
const base = forkHash ? buildFileStateAtOp(ops, forkHash) : new Map;
|
|
3787
|
+
const ours = buildFileStateAtOp(ops);
|
|
3788
|
+
const theirs = buildFileStateAtOp(ops);
|
|
3789
|
+
return threeWayMerge(base, ours, theirs, this._blobStore);
|
|
3790
|
+
}
|
|
3791
|
+
_parsers = [
|
|
3792
|
+
typescriptParser,
|
|
3793
|
+
pythonParser,
|
|
3794
|
+
goParser,
|
|
3795
|
+
rustParser,
|
|
3796
|
+
rubyParser,
|
|
3797
|
+
javaParser,
|
|
3798
|
+
csharpParser
|
|
3799
|
+
];
|
|
3800
|
+
parseFile(content, filePath) {
|
|
3801
|
+
const ext = filePath.split(".").pop() ?? "";
|
|
3802
|
+
const parser = this._parsers.find((p) => p.languages.some((lang) => {
|
|
3803
|
+
if (lang === "typescript")
|
|
3804
|
+
return ext === "ts";
|
|
3805
|
+
if (lang === "javascript")
|
|
3806
|
+
return ext === "js" || ext === "mjs" || ext === "cjs";
|
|
3807
|
+
if (lang === "tsx")
|
|
3808
|
+
return ext === "tsx";
|
|
3809
|
+
if (lang === "jsx")
|
|
3810
|
+
return ext === "jsx";
|
|
3811
|
+
if (lang === "python")
|
|
3812
|
+
return ext === "py" || ext === "pyi";
|
|
3813
|
+
if (lang === "go")
|
|
3814
|
+
return ext === "go";
|
|
3815
|
+
if (lang === "rust")
|
|
3816
|
+
return ext === "rs";
|
|
3817
|
+
if (lang === "ruby")
|
|
3818
|
+
return ext === "rb";
|
|
3819
|
+
if (lang === "java")
|
|
3820
|
+
return ext === "java";
|
|
3821
|
+
if (lang === "csharp")
|
|
3822
|
+
return ext === "cs";
|
|
3823
|
+
return false;
|
|
3824
|
+
}));
|
|
3825
|
+
if (!parser)
|
|
3826
|
+
return null;
|
|
3827
|
+
return parser.parse(content, filePath);
|
|
3828
|
+
}
|
|
3829
|
+
semanticDiff(oldContent, newContent, filePath) {
|
|
3830
|
+
const parser = this._parsers.find((p) => p.languages.some((lang) => {
|
|
3831
|
+
const ext = filePath.split(".").pop() ?? "";
|
|
3832
|
+
if (lang === "typescript")
|
|
3833
|
+
return ext === "ts";
|
|
3834
|
+
if (lang === "javascript")
|
|
3835
|
+
return ext === "js" || ext === "mjs" || ext === "cjs";
|
|
3836
|
+
if (lang === "tsx")
|
|
3837
|
+
return ext === "tsx";
|
|
3838
|
+
if (lang === "jsx")
|
|
3839
|
+
return ext === "jsx";
|
|
3840
|
+
if (lang === "python")
|
|
3841
|
+
return ext === "py" || ext === "pyi";
|
|
3842
|
+
if (lang === "go")
|
|
3843
|
+
return ext === "go";
|
|
3844
|
+
if (lang === "rust")
|
|
3845
|
+
return ext === "rs";
|
|
3846
|
+
if (lang === "ruby")
|
|
3847
|
+
return ext === "rb";
|
|
3848
|
+
if (lang === "java")
|
|
3849
|
+
return ext === "java";
|
|
3850
|
+
if (lang === "csharp")
|
|
3851
|
+
return ext === "cs";
|
|
3852
|
+
return false;
|
|
3853
|
+
}));
|
|
3854
|
+
if (!parser)
|
|
3855
|
+
return [];
|
|
3856
|
+
const oldResult = parser.parse(oldContent, filePath);
|
|
3857
|
+
const newResult = parser.parse(newContent, filePath);
|
|
3858
|
+
return parser.diff(oldResult, newResult);
|
|
3859
|
+
}
|
|
3860
|
+
_garden = null;
|
|
3861
|
+
garden() {
|
|
3862
|
+
if (!this._garden) {
|
|
3863
|
+
this._garden = new IdeaGarden({
|
|
3864
|
+
readAllOps: () => this.opLog.readAll(),
|
|
3865
|
+
getMilestonedOpHashes: () => buildMilestonedOpHashes(this.opLog.readAll())
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
return this._garden;
|
|
3869
|
+
}
|
|
3870
|
+
async createIssue(title, opts) {
|
|
3871
|
+
const op = await createIssue(this._ctx(), this.config.rootPath, title, opts);
|
|
3872
|
+
await this.flushAutoCheckpoint();
|
|
3873
|
+
return op;
|
|
3874
|
+
}
|
|
3875
|
+
async updateIssue(id, updates) {
|
|
3876
|
+
const op = await updateIssue(this._ctx(), id, updates);
|
|
3877
|
+
await this.flushAutoCheckpoint();
|
|
3878
|
+
return op;
|
|
3879
|
+
}
|
|
3880
|
+
async startIssue(id) {
|
|
3881
|
+
const issue = getIssue(this._ctx(), id);
|
|
3882
|
+
if (!issue)
|
|
3883
|
+
throw new Error(`Issue ${id} not found.`);
|
|
3884
|
+
const slug = (issue.title ?? id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3885
|
+
const branchName = `issue/${id}-${slug}`;
|
|
3886
|
+
await this.createBranch(branchName);
|
|
3887
|
+
const op = await startIssue(this._ctx(), id, branchName);
|
|
3888
|
+
this.switchBranch(branchName);
|
|
3889
|
+
await this.flushAutoCheckpoint();
|
|
3890
|
+
return op;
|
|
3891
|
+
}
|
|
3892
|
+
async pauseIssue(id, note) {
|
|
3893
|
+
const op = await pauseIssue(this._ctx(), id, note);
|
|
3894
|
+
this.switchBranch(this.config.defaultBranch);
|
|
3895
|
+
await this.flushAutoCheckpoint();
|
|
3896
|
+
return op;
|
|
3897
|
+
}
|
|
3898
|
+
async resumeIssue(id) {
|
|
3899
|
+
const issue = getIssue(this._ctx(), id);
|
|
3900
|
+
if (!issue)
|
|
3901
|
+
throw new Error(`Issue ${id} not found.`);
|
|
3902
|
+
if (!issue.branchName)
|
|
3903
|
+
throw new Error(`Issue ${id} has no tracked branch.`);
|
|
3904
|
+
const op = await resumeIssue(this._ctx(), id);
|
|
3905
|
+
this.switchBranch(issue.branchName);
|
|
3906
|
+
await this.flushAutoCheckpoint();
|
|
3907
|
+
return op;
|
|
3908
|
+
}
|
|
3909
|
+
async closeIssue(id, opts) {
|
|
3910
|
+
const result = await closeIssue(this._ctx(), id, opts);
|
|
3911
|
+
if (result.op) {
|
|
3912
|
+
await this.flushAutoCheckpoint();
|
|
3913
|
+
}
|
|
3914
|
+
return result;
|
|
3915
|
+
}
|
|
3916
|
+
async triageIssue(id) {
|
|
3917
|
+
const op = await triageIssue(this._ctx(), id);
|
|
3918
|
+
await this.flushAutoCheckpoint();
|
|
3919
|
+
return op;
|
|
3920
|
+
}
|
|
3921
|
+
async reopenIssue(id) {
|
|
3922
|
+
const op = await reopenIssue(this._ctx(), id);
|
|
3923
|
+
await this.flushAutoCheckpoint();
|
|
3924
|
+
return op;
|
|
3925
|
+
}
|
|
3926
|
+
checkCompletionReadiness() {
|
|
3927
|
+
return checkCompletionReadiness(this._ctx());
|
|
3928
|
+
}
|
|
3929
|
+
async assignIssue(id, agentId) {
|
|
3930
|
+
const op = await assignIssue(this._ctx(), id, agentId);
|
|
3931
|
+
await this.flushAutoCheckpoint();
|
|
3932
|
+
return op;
|
|
3933
|
+
}
|
|
3934
|
+
async blockIssue(id, blockedById) {
|
|
3935
|
+
const op = await blockIssue(this._ctx(), id, blockedById);
|
|
3936
|
+
await this.flushAutoCheckpoint();
|
|
3937
|
+
return op;
|
|
3938
|
+
}
|
|
3939
|
+
async unblockIssue(id, blockedById) {
|
|
3940
|
+
const op = await unblockIssue(this._ctx(), id, blockedById);
|
|
3941
|
+
await this.flushAutoCheckpoint();
|
|
3942
|
+
return op;
|
|
3943
|
+
}
|
|
3944
|
+
async addCriterion(issueId, description, command) {
|
|
3945
|
+
const op = await addCriterion(this._ctx(), issueId, description, command);
|
|
3946
|
+
await this.flushAutoCheckpoint();
|
|
3947
|
+
return op;
|
|
3948
|
+
}
|
|
3949
|
+
async setCriterionStatus(issueId, criterionIndex, status) {
|
|
3950
|
+
const op = await setCriterionStatus(this._ctx(), issueId, criterionIndex, status);
|
|
3951
|
+
await this.flushAutoCheckpoint();
|
|
3952
|
+
return op;
|
|
3953
|
+
}
|
|
3954
|
+
async runCriteria(issueId) {
|
|
3955
|
+
return runCriteria(this._ctx(), issueId, this.config.rootPath);
|
|
3956
|
+
}
|
|
3957
|
+
listIssues(filters) {
|
|
3958
|
+
return listIssues(this._ctx(), filters);
|
|
3959
|
+
}
|
|
3960
|
+
getIssue(id) {
|
|
3961
|
+
return getIssue(this._ctx(), id);
|
|
3962
|
+
}
|
|
3963
|
+
getActiveIssues() {
|
|
3964
|
+
return getActiveIssues(this._ctx());
|
|
3965
|
+
}
|
|
3966
|
+
async recordDecision(input) {
|
|
3967
|
+
const op = await recordDecision(this._ctx(), this.config.rootPath, input);
|
|
3968
|
+
await this.flushAutoCheckpoint();
|
|
3969
|
+
return op;
|
|
3970
|
+
}
|
|
3971
|
+
queryDecisions(filter) {
|
|
3972
|
+
return queryDecisions(this._ctx(), filter);
|
|
3973
|
+
}
|
|
3974
|
+
getDecisionChain(entityId) {
|
|
3975
|
+
return getDecisionChain(this._ctx(), entityId);
|
|
3976
|
+
}
|
|
3977
|
+
getDecision(id) {
|
|
3978
|
+
return getDecision(this._ctx(), id);
|
|
3979
|
+
}
|
|
3980
|
+
_ctx() {
|
|
3981
|
+
return {
|
|
3982
|
+
store: this.store,
|
|
3983
|
+
agentId: this.agentId,
|
|
3984
|
+
readAllOps: () => this.opLog.readAll(),
|
|
3985
|
+
getLastOp: () => this.opLog.getLastOp(),
|
|
3986
|
+
applyOp: (op) => this.applyOp(op)
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
applyOp(op) {
|
|
3990
|
+
const decomposed = decompose(op);
|
|
3991
|
+
if (decomposed.deleteFacts.length > 0) {
|
|
3992
|
+
this.store.deleteFacts(decomposed.deleteFacts);
|
|
3993
|
+
}
|
|
3994
|
+
if (decomposed.deleteLinks.length > 0) {
|
|
3995
|
+
this.store.deleteLinks(decomposed.deleteLinks);
|
|
3996
|
+
}
|
|
3997
|
+
if (decomposed.addFacts.length > 0) {
|
|
3998
|
+
this.store.addFacts(decomposed.addFacts);
|
|
3999
|
+
}
|
|
4000
|
+
if (decomposed.addLinks.length > 0) {
|
|
4001
|
+
this.store.addLinks(decomposed.addLinks);
|
|
4002
|
+
}
|
|
4003
|
+
this.opLog.append(op);
|
|
4004
|
+
if (op.kind !== "vcs:checkpointCreate" && this.checkpointThreshold > 0) {
|
|
4005
|
+
this.checkpointOpCount++;
|
|
4006
|
+
if (this.checkpointOpCount >= this.checkpointThreshold) {
|
|
4007
|
+
this._pendingAutoCheckpoint = true;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
async flushAutoCheckpoint() {
|
|
4012
|
+
if (this._pendingAutoCheckpoint) {
|
|
4013
|
+
this._pendingAutoCheckpoint = false;
|
|
4014
|
+
await this.createCheckpoint("op-count");
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
loadCurrentBranch() {
|
|
4018
|
+
const state = loadBranchState(this.config.rootPath);
|
|
4019
|
+
this.currentBranch = state.currentBranch;
|
|
4020
|
+
}
|
|
4021
|
+
replayOp(op) {
|
|
4022
|
+
const decomposed = decompose(op);
|
|
4023
|
+
if (decomposed.deleteFacts.length > 0) {
|
|
4024
|
+
this.store.deleteFacts(decomposed.deleteFacts);
|
|
4025
|
+
}
|
|
4026
|
+
if (decomposed.deleteLinks.length > 0) {
|
|
4027
|
+
this.store.deleteLinks(decomposed.deleteLinks);
|
|
4028
|
+
}
|
|
4029
|
+
if (decomposed.addFacts.length > 0) {
|
|
4030
|
+
this.store.addFacts(decomposed.addFacts);
|
|
4031
|
+
}
|
|
4032
|
+
if (decomposed.addLinks.length > 0) {
|
|
4033
|
+
this.store.addLinks(decomposed.addLinks);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
export { FileWatcher, Ingestion, TrellisVcsEngine };
|