trellis 2.0.13 → 2.1.2
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/dist/cli/index.js +1 -1
- package/dist/embeddings/index.js +1 -1
- package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
- package/package.json +2 -10
- package/dist/transformers.node-bx3q9d7k.js +0 -33130
- package/src/cli/index.ts +0 -3356
- package/src/core/agents/harness.ts +0 -380
- package/src/core/agents/index.ts +0 -18
- package/src/core/agents/types.ts +0 -90
- package/src/core/index.ts +0 -118
- package/src/core/kernel/middleware.ts +0 -44
- package/src/core/kernel/trellis-kernel.ts +0 -593
- package/src/core/ontology/builtins.ts +0 -248
- package/src/core/ontology/index.ts +0 -34
- package/src/core/ontology/registry.ts +0 -209
- package/src/core/ontology/types.ts +0 -124
- package/src/core/ontology/validator.ts +0 -382
- package/src/core/persist/backend.ts +0 -74
- package/src/core/persist/sqlite-backend.ts +0 -298
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/registry.ts +0 -322
- package/src/core/plugins/types.ts +0 -126
- package/src/core/query/datalog.ts +0 -188
- package/src/core/query/engine.ts +0 -370
- package/src/core/query/index.ts +0 -34
- package/src/core/query/parser.ts +0 -481
- package/src/core/query/types.ts +0 -200
- package/src/core/store/eav-store.ts +0 -467
- package/src/decisions/auto-capture.ts +0 -136
- package/src/decisions/hooks.ts +0 -163
- package/src/decisions/index.ts +0 -261
- package/src/decisions/types.ts +0 -103
- package/src/embeddings/auto-embed.ts +0 -248
- package/src/embeddings/chunker.ts +0 -327
- package/src/embeddings/index.ts +0 -48
- package/src/embeddings/model.ts +0 -112
- package/src/embeddings/search.ts +0 -305
- package/src/embeddings/store.ts +0 -313
- package/src/embeddings/types.ts +0 -92
- package/src/engine.ts +0 -1125
- package/src/garden/cluster.ts +0 -330
- package/src/garden/garden.ts +0 -306
- package/src/garden/index.ts +0 -29
- package/src/git/git-exporter.ts +0 -286
- package/src/git/git-importer.ts +0 -329
- package/src/git/git-reader.ts +0 -189
- package/src/git/index.ts +0 -22
- package/src/identity/governance.ts +0 -211
- package/src/identity/identity.ts +0 -224
- package/src/identity/index.ts +0 -30
- package/src/identity/signing-middleware.ts +0 -97
- package/src/index.ts +0 -29
- package/src/links/index.ts +0 -49
- package/src/links/lifecycle.ts +0 -400
- package/src/links/parser.ts +0 -484
- package/src/links/ref-index.ts +0 -186
- package/src/links/resolver.ts +0 -314
- package/src/links/types.ts +0 -108
- package/src/mcp/index.ts +0 -22
- package/src/mcp/server.ts +0 -1278
- package/src/semantic/csharp-parser.ts +0 -493
- package/src/semantic/go-parser.ts +0 -585
- package/src/semantic/index.ts +0 -34
- package/src/semantic/java-parser.ts +0 -456
- package/src/semantic/python-parser.ts +0 -659
- package/src/semantic/ruby-parser.ts +0 -446
- package/src/semantic/rust-parser.ts +0 -784
- package/src/semantic/semantic-merge.ts +0 -210
- package/src/semantic/ts-parser.ts +0 -681
- package/src/semantic/types.ts +0 -175
- package/src/sync/http-transport.ts +0 -144
- package/src/sync/index.ts +0 -43
- package/src/sync/memory-transport.ts +0 -66
- package/src/sync/multi-repo.ts +0 -200
- package/src/sync/reconciler.ts +0 -237
- package/src/sync/sync-engine.ts +0 -258
- package/src/sync/types.ts +0 -104
- package/src/sync/ws-transport.ts +0 -145
- package/src/ui/client.html +0 -695
- package/src/ui/server.ts +0 -419
- package/src/vcs/blob-store.ts +0 -124
- package/src/vcs/branch.ts +0 -150
- package/src/vcs/checkpoint.ts +0 -64
- package/src/vcs/decompose.ts +0 -469
- package/src/vcs/diff.ts +0 -409
- package/src/vcs/engine-context.ts +0 -26
- package/src/vcs/index.ts +0 -23
- package/src/vcs/issue.ts +0 -800
- package/src/vcs/merge.ts +0 -425
- package/src/vcs/milestone.ts +0 -124
- package/src/vcs/ops.ts +0 -59
- package/src/vcs/types.ts +0 -213
- package/src/vcs/vcs-middleware.ts +0 -81
- package/src/watcher/fs-watcher.ts +0 -255
- package/src/watcher/index.ts +0 -9
- package/src/watcher/ingestion.ts +0 -116
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python Parser Adapter
|
|
3
|
-
*
|
|
4
|
-
* Tier 1 regex-based parser for Python source files.
|
|
5
|
-
* Extracts classes, functions, decorators, async functions,
|
|
6
|
-
* type hints, imports, and module-level variables.
|
|
7
|
-
*
|
|
8
|
-
* @see TRL-5
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type {
|
|
12
|
-
ParserAdapter,
|
|
13
|
-
ParseResult,
|
|
14
|
-
ASTEntity,
|
|
15
|
-
ASTEntityKind,
|
|
16
|
-
ImportRelation,
|
|
17
|
-
ExportRelation,
|
|
18
|
-
SemanticPatch,
|
|
19
|
-
} from './types.js';
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Parser Adapter
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export const pythonParser: ParserAdapter = {
|
|
26
|
-
languages: ['python'],
|
|
27
|
-
|
|
28
|
-
parse(content: string, filePath: string): ParseResult {
|
|
29
|
-
const fileEntityId = `file:${filePath}`;
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
fileEntityId,
|
|
33
|
-
filePath,
|
|
34
|
-
language: 'python',
|
|
35
|
-
declarations: extractDeclarations(content, filePath),
|
|
36
|
-
imports: extractImports(content),
|
|
37
|
-
exports: extractExports(content),
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
diff(oldResult: ParseResult, newResult: ParseResult): SemanticPatch[] {
|
|
42
|
-
return computeSemanticDiff(oldResult, newResult);
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Declaration extraction
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Extract top-level declarations from Python source.
|
|
52
|
-
* Handles: class, def, async def, and module-level assignments.
|
|
53
|
-
* Respects indentation to determine block boundaries.
|
|
54
|
-
*/
|
|
55
|
-
function extractDeclarations(content: string, filePath: string): ASTEntity[] {
|
|
56
|
-
const declarations: ASTEntity[] = [];
|
|
57
|
-
const lines = content.split('\n');
|
|
58
|
-
|
|
59
|
-
let i = 0;
|
|
60
|
-
while (i < lines.length) {
|
|
61
|
-
const line = lines[i];
|
|
62
|
-
const trimmed = line.trim();
|
|
63
|
-
|
|
64
|
-
// Skip empty lines, comments, string literals used as docstrings
|
|
65
|
-
if (
|
|
66
|
-
!trimmed ||
|
|
67
|
-
trimmed.startsWith('#') ||
|
|
68
|
-
trimmed.startsWith('"""') ||
|
|
69
|
-
trimmed.startsWith("'''")
|
|
70
|
-
) {
|
|
71
|
-
i++;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Only consider top-level (zero indentation)
|
|
76
|
-
if ((line.length > 0 && line[0] === ' ') || line[0] === '\t') {
|
|
77
|
-
i++;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Collect decorators
|
|
82
|
-
const decorators: string[] = [];
|
|
83
|
-
const decoratorStart = i;
|
|
84
|
-
while (i < lines.length && lines[i].trim().startsWith('@')) {
|
|
85
|
-
decorators.push(lines[i].trim());
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (i >= lines.length) break;
|
|
90
|
-
const declLine = lines[i].trim();
|
|
91
|
-
|
|
92
|
-
// Class definition
|
|
93
|
-
let match = declLine.match(/^class\s+(\w+)/);
|
|
94
|
-
if (match) {
|
|
95
|
-
const result = extractIndentedBlock(
|
|
96
|
-
match[1],
|
|
97
|
-
'ClassDef',
|
|
98
|
-
lines,
|
|
99
|
-
decorators.length > 0 ? decoratorStart : i,
|
|
100
|
-
i,
|
|
101
|
-
filePath,
|
|
102
|
-
decorators,
|
|
103
|
-
);
|
|
104
|
-
declarations.push(result.entity);
|
|
105
|
-
i = result.endLine + 1;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Async function
|
|
110
|
-
match = declLine.match(/^async\s+def\s+(\w+)/);
|
|
111
|
-
if (match) {
|
|
112
|
-
const result = extractIndentedBlock(
|
|
113
|
-
match[1],
|
|
114
|
-
'FunctionDef',
|
|
115
|
-
lines,
|
|
116
|
-
decorators.length > 0 ? decoratorStart : i,
|
|
117
|
-
i,
|
|
118
|
-
filePath,
|
|
119
|
-
decorators,
|
|
120
|
-
);
|
|
121
|
-
declarations.push(result.entity);
|
|
122
|
-
i = result.endLine + 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Function definition
|
|
127
|
-
match = declLine.match(/^def\s+(\w+)/);
|
|
128
|
-
if (match) {
|
|
129
|
-
const result = extractIndentedBlock(
|
|
130
|
-
match[1],
|
|
131
|
-
'FunctionDef',
|
|
132
|
-
lines,
|
|
133
|
-
decorators.length > 0 ? decoratorStart : i,
|
|
134
|
-
i,
|
|
135
|
-
filePath,
|
|
136
|
-
decorators,
|
|
137
|
-
);
|
|
138
|
-
declarations.push(result.entity);
|
|
139
|
-
i = result.endLine + 1;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Module-level variable assignment (e.g. `MY_VAR = ...` or `MY_VAR: int = ...`)
|
|
144
|
-
match = declLine.match(/^([A-Za-z_]\w*)\s*(?::\s*\w[^=]*)?\s*=/);
|
|
145
|
-
if (
|
|
146
|
-
match &&
|
|
147
|
-
match[1] !== '__all__' &&
|
|
148
|
-
!declLine.startsWith('import ') &&
|
|
149
|
-
!declLine.startsWith('from ')
|
|
150
|
-
) {
|
|
151
|
-
const result = extractAssignment(match[1], lines, i, filePath);
|
|
152
|
-
if (decorators.length === 0) {
|
|
153
|
-
declarations.push(result.entity);
|
|
154
|
-
}
|
|
155
|
-
i = result.endLine + 1;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// If we collected decorators but no matching def/class, skip
|
|
160
|
-
if (decorators.length > 0) {
|
|
161
|
-
i++;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
i++;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return declarations;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// Block extraction (indentation-based)
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
interface ExtractionResult {
|
|
176
|
-
entity: ASTEntity;
|
|
177
|
-
endLine: number;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Extract a Python indentation-delimited block (class or function).
|
|
182
|
-
*/
|
|
183
|
-
function extractIndentedBlock(
|
|
184
|
-
name: string,
|
|
185
|
-
kind: ASTEntityKind,
|
|
186
|
-
lines: string[],
|
|
187
|
-
startLine: number,
|
|
188
|
-
defLine: number,
|
|
189
|
-
filePath: string,
|
|
190
|
-
decorators: string[],
|
|
191
|
-
): ExtractionResult {
|
|
192
|
-
// Find the colon at end of def/class line (may span multiple lines for long signatures)
|
|
193
|
-
let headerEnd = defLine;
|
|
194
|
-
while (headerEnd < lines.length && !lines[headerEnd].includes(':')) {
|
|
195
|
-
headerEnd++;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Determine body indentation from the first non-empty line after header
|
|
199
|
-
let bodyIndent = -1;
|
|
200
|
-
let endLine = headerEnd;
|
|
201
|
-
|
|
202
|
-
for (let i = headerEnd + 1; i < lines.length; i++) {
|
|
203
|
-
const line = lines[i];
|
|
204
|
-
const trimmed = line.trim();
|
|
205
|
-
|
|
206
|
-
// Skip empty lines and comment-only lines
|
|
207
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
208
|
-
endLine = i;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const indent = line.length - line.trimStart().length;
|
|
213
|
-
|
|
214
|
-
if (bodyIndent < 0) {
|
|
215
|
-
// First non-empty line in body sets the expected indent
|
|
216
|
-
bodyIndent = indent;
|
|
217
|
-
endLine = i;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (indent >= bodyIndent) {
|
|
222
|
-
endLine = i;
|
|
223
|
-
} else {
|
|
224
|
-
// Dedented — block is over
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Trim trailing empty lines from the block
|
|
230
|
-
while (endLine > headerEnd && lines[endLine].trim() === '') {
|
|
231
|
-
endLine--;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const rawText = lines.slice(startLine, endLine + 1).join('\n');
|
|
235
|
-
const startOffset =
|
|
236
|
-
lines.slice(0, startLine).join('\n').length + (startLine > 0 ? 1 : 0);
|
|
237
|
-
|
|
238
|
-
const children =
|
|
239
|
-
kind === 'ClassDef'
|
|
240
|
-
? extractClassMembers(
|
|
241
|
-
lines,
|
|
242
|
-
headerEnd,
|
|
243
|
-
endLine,
|
|
244
|
-
bodyIndent,
|
|
245
|
-
name,
|
|
246
|
-
filePath,
|
|
247
|
-
)
|
|
248
|
-
: [];
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
entity: {
|
|
252
|
-
id: makeEntityId(filePath, kind, name),
|
|
253
|
-
kind,
|
|
254
|
-
name,
|
|
255
|
-
scopePath: name,
|
|
256
|
-
span: [startOffset, startOffset + rawText.length],
|
|
257
|
-
rawText,
|
|
258
|
-
signature: normalizeSignature(rawText),
|
|
259
|
-
children,
|
|
260
|
-
},
|
|
261
|
-
endLine,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Extract a module-level variable assignment (may span multiple lines).
|
|
267
|
-
*/
|
|
268
|
-
function extractAssignment(
|
|
269
|
-
name: string,
|
|
270
|
-
lines: string[],
|
|
271
|
-
startLine: number,
|
|
272
|
-
filePath: string,
|
|
273
|
-
): ExtractionResult {
|
|
274
|
-
let endLine = startLine;
|
|
275
|
-
let depth = 0;
|
|
276
|
-
|
|
277
|
-
for (let i = startLine; i < lines.length; i++) {
|
|
278
|
-
const line = lines[i];
|
|
279
|
-
for (const ch of line) {
|
|
280
|
-
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
281
|
-
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
endLine = i;
|
|
285
|
-
|
|
286
|
-
// Assignment ends when depth returns to 0 and we're past the first line,
|
|
287
|
-
// or the next line is a new top-level statement
|
|
288
|
-
if (depth <= 0 && i > startLine) break;
|
|
289
|
-
if (depth <= 0 && i === startLine) {
|
|
290
|
-
// Check if next line is indented (continuation) or a new statement
|
|
291
|
-
if (i + 1 < lines.length) {
|
|
292
|
-
const next = lines[i + 1];
|
|
293
|
-
if (!next.trim() || (!next.startsWith(' ') && !next.startsWith('\t'))) {
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
} else {
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const rawText = lines.slice(startLine, endLine + 1).join('\n');
|
|
303
|
-
const startOffset =
|
|
304
|
-
lines.slice(0, startLine).join('\n').length + (startLine > 0 ? 1 : 0);
|
|
305
|
-
|
|
306
|
-
return {
|
|
307
|
-
entity: {
|
|
308
|
-
id: makeEntityId(filePath, 'VariableDecl', name),
|
|
309
|
-
kind: 'VariableDecl',
|
|
310
|
-
name,
|
|
311
|
-
scopePath: name,
|
|
312
|
-
span: [startOffset, startOffset + rawText.length],
|
|
313
|
-
rawText,
|
|
314
|
-
signature: normalizeSignature(rawText),
|
|
315
|
-
children: [],
|
|
316
|
-
},
|
|
317
|
-
endLine,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
// Class member extraction
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
|
|
325
|
-
function extractClassMembers(
|
|
326
|
-
lines: string[],
|
|
327
|
-
headerEnd: number,
|
|
328
|
-
blockEnd: number,
|
|
329
|
-
bodyIndent: number,
|
|
330
|
-
className: string,
|
|
331
|
-
filePath: string,
|
|
332
|
-
): ASTEntity[] {
|
|
333
|
-
const children: ASTEntity[] = [];
|
|
334
|
-
if (bodyIndent < 0) return children;
|
|
335
|
-
|
|
336
|
-
for (let i = headerEnd + 1; i <= blockEnd; i++) {
|
|
337
|
-
const line = lines[i];
|
|
338
|
-
const trimmed = line.trim();
|
|
339
|
-
if (
|
|
340
|
-
!trimmed ||
|
|
341
|
-
trimmed.startsWith('#') ||
|
|
342
|
-
trimmed.startsWith('"""') ||
|
|
343
|
-
trimmed.startsWith("'''")
|
|
344
|
-
)
|
|
345
|
-
continue;
|
|
346
|
-
|
|
347
|
-
const indent = line.length - line.trimStart().length;
|
|
348
|
-
if (indent !== bodyIndent) continue;
|
|
349
|
-
|
|
350
|
-
// Skip decorators (they precede a method)
|
|
351
|
-
if (trimmed.startsWith('@')) continue;
|
|
352
|
-
|
|
353
|
-
// Method (def / async def)
|
|
354
|
-
let match = trimmed.match(/^(?:async\s+)?def\s+(\w+)/);
|
|
355
|
-
if (match) {
|
|
356
|
-
const methodName = match[1];
|
|
357
|
-
const kind: ASTEntityKind =
|
|
358
|
-
methodName === '__init__' ? 'Constructor' : 'MethodDef';
|
|
359
|
-
children.push({
|
|
360
|
-
id: makeEntityId(filePath, kind, `${className}.${methodName}`),
|
|
361
|
-
kind,
|
|
362
|
-
name: methodName,
|
|
363
|
-
scopePath: `${className}.${methodName}`,
|
|
364
|
-
span: [0, 0],
|
|
365
|
-
rawText: trimmed,
|
|
366
|
-
signature: normalizeSignature(trimmed),
|
|
367
|
-
children: [],
|
|
368
|
-
});
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Class-level variable / property (e.g. `name: str = "default"`)
|
|
373
|
-
match = trimmed.match(/^(\w+)\s*(?::\s*\S[^=]*)?\s*=/);
|
|
374
|
-
if (match) {
|
|
375
|
-
children.push({
|
|
376
|
-
id: makeEntityId(filePath, 'PropertyDef', `${className}.${match[1]}`),
|
|
377
|
-
kind: 'PropertyDef',
|
|
378
|
-
name: match[1],
|
|
379
|
-
scopePath: `${className}.${match[1]}`,
|
|
380
|
-
span: [0, 0],
|
|
381
|
-
rawText: trimmed,
|
|
382
|
-
signature: normalizeSignature(trimmed),
|
|
383
|
-
children: [],
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return children;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ---------------------------------------------------------------------------
|
|
392
|
-
// Import extraction
|
|
393
|
-
// ---------------------------------------------------------------------------
|
|
394
|
-
|
|
395
|
-
function extractImports(content: string): ImportRelation[] {
|
|
396
|
-
const imports: ImportRelation[] = [];
|
|
397
|
-
const lines = content.split('\n');
|
|
398
|
-
|
|
399
|
-
for (let i = 0; i < lines.length; i++) {
|
|
400
|
-
const trimmed = lines[i].trim();
|
|
401
|
-
|
|
402
|
-
// from module import ...
|
|
403
|
-
let match = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
404
|
-
if (match) {
|
|
405
|
-
let specText = match[2];
|
|
406
|
-
|
|
407
|
-
// Handle multi-line imports: from mod import (a, b, ...)
|
|
408
|
-
if (specText.includes('(') && !specText.includes(')')) {
|
|
409
|
-
while (i + 1 < lines.length && !specText.includes(')')) {
|
|
410
|
-
i++;
|
|
411
|
-
specText += ' ' + lines[i].trim();
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Strip parens and trailing comments
|
|
416
|
-
specText = specText.replace(/[()]/g, '').replace(/#.*$/, '');
|
|
417
|
-
const specifiers = specText
|
|
418
|
-
.split(',')
|
|
419
|
-
.map(
|
|
420
|
-
(s) =>
|
|
421
|
-
s
|
|
422
|
-
.trim()
|
|
423
|
-
.split(/\s+as\s+/)
|
|
424
|
-
.pop()!,
|
|
425
|
-
)
|
|
426
|
-
.filter(Boolean);
|
|
427
|
-
|
|
428
|
-
const isWildcard = specifiers.includes('*');
|
|
429
|
-
|
|
430
|
-
imports.push({
|
|
431
|
-
source: match[1],
|
|
432
|
-
specifiers: isWildcard ? ['*'] : specifiers,
|
|
433
|
-
isDefault: false,
|
|
434
|
-
isNamespace: isWildcard,
|
|
435
|
-
rawText: trimmed,
|
|
436
|
-
span: [0, trimmed.length],
|
|
437
|
-
});
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// import module [as alias]
|
|
442
|
-
match = trimmed.match(/^import\s+([\w.,\s]+)/);
|
|
443
|
-
if (match) {
|
|
444
|
-
const modules = match[1].split(',').map((m) => m.trim());
|
|
445
|
-
for (const mod of modules) {
|
|
446
|
-
const parts = mod.split(/\s+as\s+/);
|
|
447
|
-
const source = parts[0].trim();
|
|
448
|
-
const alias = parts.length > 1 ? parts[1].trim() : source;
|
|
449
|
-
imports.push({
|
|
450
|
-
source,
|
|
451
|
-
specifiers: [alias],
|
|
452
|
-
isDefault: true,
|
|
453
|
-
isNamespace: false,
|
|
454
|
-
rawText: trimmed,
|
|
455
|
-
span: [0, trimmed.length],
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return imports;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// ---------------------------------------------------------------------------
|
|
465
|
-
// Export extraction
|
|
466
|
-
// ---------------------------------------------------------------------------
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Python uses __all__ to declare public API. We also treat
|
|
470
|
-
* top-level public names (not starting with _) as exports.
|
|
471
|
-
*/
|
|
472
|
-
function extractExports(content: string): ExportRelation[] {
|
|
473
|
-
const exports: ExportRelation[] = [];
|
|
474
|
-
|
|
475
|
-
// Look for __all__ = [...]
|
|
476
|
-
const allMatch = content.match(/__all__\s*=\s*\[([^\]]*)\]/s);
|
|
477
|
-
if (allMatch) {
|
|
478
|
-
const names = allMatch[1]
|
|
479
|
-
.replace(/['"]/g, '')
|
|
480
|
-
.split(',')
|
|
481
|
-
.map((s) => s.trim())
|
|
482
|
-
.filter(Boolean);
|
|
483
|
-
|
|
484
|
-
for (const name of names) {
|
|
485
|
-
exports.push({
|
|
486
|
-
name,
|
|
487
|
-
isDefault: false,
|
|
488
|
-
rawText: `__all__: ${name}`,
|
|
489
|
-
span: [0, 0],
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return exports;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// ---------------------------------------------------------------------------
|
|
498
|
-
// Semantic diff (reuses generic algorithm from ts-parser pattern)
|
|
499
|
-
// ---------------------------------------------------------------------------
|
|
500
|
-
|
|
501
|
-
function computeSemanticDiff(
|
|
502
|
-
oldResult: ParseResult,
|
|
503
|
-
newResult: ParseResult,
|
|
504
|
-
): SemanticPatch[] {
|
|
505
|
-
const patches: SemanticPatch[] = [];
|
|
506
|
-
const fileId = newResult.fileEntityId;
|
|
507
|
-
|
|
508
|
-
const oldDecls = new Map(oldResult.declarations.map((d) => [d.id, d]));
|
|
509
|
-
const newDecls = new Map(newResult.declarations.map((d) => [d.id, d]));
|
|
510
|
-
|
|
511
|
-
// Detect additions (+ rename detection)
|
|
512
|
-
for (const [id, entity] of newDecls) {
|
|
513
|
-
if (!oldDecls.has(id)) {
|
|
514
|
-
const oldEntity = findRenamedEntity(
|
|
515
|
-
entity,
|
|
516
|
-
oldResult.declarations,
|
|
517
|
-
newDecls,
|
|
518
|
-
);
|
|
519
|
-
if (oldEntity) {
|
|
520
|
-
patches.push({
|
|
521
|
-
kind: 'symbolRename',
|
|
522
|
-
entityId: oldEntity.id,
|
|
523
|
-
oldName: oldEntity.name,
|
|
524
|
-
newName: entity.name,
|
|
525
|
-
});
|
|
526
|
-
} else {
|
|
527
|
-
patches.push({ kind: 'symbolAdd', entity });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Detect removals
|
|
533
|
-
for (const [id, entity] of oldDecls) {
|
|
534
|
-
if (!newDecls.has(id)) {
|
|
535
|
-
const wasRenamed = findRenamedEntity(
|
|
536
|
-
entity,
|
|
537
|
-
newResult.declarations,
|
|
538
|
-
oldDecls,
|
|
539
|
-
);
|
|
540
|
-
if (!wasRenamed) {
|
|
541
|
-
patches.push({
|
|
542
|
-
kind: 'symbolRemove',
|
|
543
|
-
entityId: id,
|
|
544
|
-
entityName: entity.name,
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Detect modifications
|
|
551
|
-
for (const [id, newEntity] of newDecls) {
|
|
552
|
-
const oldEntity = oldDecls.get(id);
|
|
553
|
-
if (oldEntity && oldEntity.signature !== newEntity.signature) {
|
|
554
|
-
patches.push({
|
|
555
|
-
kind: 'symbolModify',
|
|
556
|
-
entityId: id,
|
|
557
|
-
entityName: newEntity.name,
|
|
558
|
-
oldSignature: oldEntity.signature,
|
|
559
|
-
newSignature: newEntity.signature,
|
|
560
|
-
oldRawText: oldEntity.rawText,
|
|
561
|
-
newRawText: newEntity.rawText,
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Diff imports
|
|
567
|
-
const oldImports = new Map(oldResult.imports.map((imp) => [imp.source, imp]));
|
|
568
|
-
const newImports = new Map(newResult.imports.map((imp) => [imp.source, imp]));
|
|
569
|
-
|
|
570
|
-
for (const [source, imp] of newImports) {
|
|
571
|
-
const oldImp = oldImports.get(source);
|
|
572
|
-
if (!oldImp) {
|
|
573
|
-
patches.push({
|
|
574
|
-
kind: 'importAdd',
|
|
575
|
-
fileId,
|
|
576
|
-
source,
|
|
577
|
-
specifiers: imp.specifiers,
|
|
578
|
-
rawText: imp.rawText,
|
|
579
|
-
});
|
|
580
|
-
} else if (
|
|
581
|
-
JSON.stringify(oldImp.specifiers.sort()) !==
|
|
582
|
-
JSON.stringify(imp.specifiers.sort())
|
|
583
|
-
) {
|
|
584
|
-
patches.push({
|
|
585
|
-
kind: 'importModify',
|
|
586
|
-
fileId,
|
|
587
|
-
source,
|
|
588
|
-
oldSpecifiers: oldImp.specifiers,
|
|
589
|
-
newSpecifiers: imp.specifiers,
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
for (const [source] of oldImports) {
|
|
595
|
-
if (!newImports.has(source)) {
|
|
596
|
-
patches.push({ kind: 'importRemove', fileId, source });
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Diff exports
|
|
601
|
-
const oldExports = new Map(oldResult.exports.map((exp) => [exp.name, exp]));
|
|
602
|
-
const newExports = new Map(newResult.exports.map((exp) => [exp.name, exp]));
|
|
603
|
-
|
|
604
|
-
for (const [name, exp] of newExports) {
|
|
605
|
-
if (!oldExports.has(name)) {
|
|
606
|
-
patches.push({ kind: 'exportAdd', fileId, name, rawText: exp.rawText });
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
for (const [name] of oldExports) {
|
|
611
|
-
if (!newExports.has(name)) {
|
|
612
|
-
patches.push({ kind: 'exportRemove', fileId, name });
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return patches;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// ---------------------------------------------------------------------------
|
|
620
|
-
// Helpers
|
|
621
|
-
// ---------------------------------------------------------------------------
|
|
622
|
-
|
|
623
|
-
function findRenamedEntity(
|
|
624
|
-
entity: ASTEntity,
|
|
625
|
-
candidates: ASTEntity[],
|
|
626
|
-
existingIds: Map<string, ASTEntity>,
|
|
627
|
-
): ASTEntity | null {
|
|
628
|
-
for (const candidate of candidates) {
|
|
629
|
-
if (candidate.kind !== entity.kind) continue;
|
|
630
|
-
if (candidate.name === entity.name) continue;
|
|
631
|
-
if (existingIds.has(candidate.id)) continue;
|
|
632
|
-
|
|
633
|
-
const normalizedOld = candidate.signature.replace(
|
|
634
|
-
new RegExp(candidate.name, 'g'),
|
|
635
|
-
'___',
|
|
636
|
-
);
|
|
637
|
-
const normalizedNew = entity.signature.replace(
|
|
638
|
-
new RegExp(entity.name, 'g'),
|
|
639
|
-
'___',
|
|
640
|
-
);
|
|
641
|
-
if (normalizedOld === normalizedNew) {
|
|
642
|
-
return candidate;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
return null;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function makeEntityId(filePath: string, kind: string, name: string): string {
|
|
649
|
-
return `${kind}:${filePath}:${name}`;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function normalizeSignature(text: string): string {
|
|
653
|
-
return text
|
|
654
|
-
.replace(/#[^\n]*/g, '') // line comments
|
|
655
|
-
.replace(/"""[\s\S]*?"""/g, '') // docstrings (triple double)
|
|
656
|
-
.replace(/'''[\s\S]*?'''/g, '') // docstrings (triple single)
|
|
657
|
-
.replace(/\s+/g, ' ') // collapse whitespace
|
|
658
|
-
.trim();
|
|
659
|
-
}
|