@shanepadgett/tau-agent 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/subagents.md +1 -1
- package/extensions/appshot/README.md +1 -1
- package/extensions/appshot/index.ts +5 -11
- package/extensions/context-pruning/README.md +2 -2
- package/extensions/context-pruning/index.ts +58 -57
- package/extensions/context-pruning/render.ts +51 -39
- package/extensions/context-pruning/settings.ts +4 -5
- package/extensions/explore/README.md +18 -11
- package/extensions/explore/ast-guidance.ts +67 -0
- package/extensions/explore/ast-languages.ts +61 -0
- package/extensions/explore/ast-tools.ts +1257 -91
- package/extensions/explore/ast-worker.ts +735 -66
- package/extensions/explore/index.ts +62 -6
- package/extensions/explore/orientation-state.ts +280 -0
- package/extensions/explore/read-stats-panel.ts +23 -1
- package/extensions/explore/read-stats.ts +21 -3
- package/extensions/explore/read.ts +58 -21
- package/extensions/explore/settings.ts +46 -0
- package/extensions/explore/traverse.ts +15 -4
- package/extensions/patch/executor.ts +95 -1
- package/extensions/patch/index.ts +1 -0
- package/extensions/soul/prompt.ts +1 -3
- package/extensions/subagent/agents/review.md +76 -25
- package/extensions/subagent/agents/scout.md +118 -22
- package/extensions/tau-help/help.md +3 -1
- package/native-bin/darwin-arm64/tau-ast +0 -0
- package/package.json +2 -3
- package/schemas/tau.schema.json +44 -4
- package/shared/bounded-text-result.ts +304 -0
- package/shared/events.ts +2 -1
- package/shared/temporary-output-store.ts +275 -0
|
@@ -2,20 +2,32 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { isAstLanguage, type AstLanguage } from "./ast-languages.ts";
|
|
5
6
|
|
|
6
|
-
export type AstLanguage
|
|
7
|
-
| "typeScript"
|
|
8
|
-
| "tsx"
|
|
9
|
-
| "odin"
|
|
10
|
-
| "go"
|
|
11
|
-
| "rust"
|
|
12
|
-
| "cSharp"
|
|
13
|
-
| "java"
|
|
14
|
-
| "kotlin"
|
|
15
|
-
| "swift"
|
|
16
|
-
| "markdown";
|
|
7
|
+
export type { AstLanguage } from "./ast-languages.ts";
|
|
17
8
|
|
|
18
|
-
export type OutlineTarget =
|
|
9
|
+
export type OutlineTarget =
|
|
10
|
+
| { kind: "file"; path: string; language: AstLanguage }
|
|
11
|
+
| { kind: "directory"; path: string }
|
|
12
|
+
| {
|
|
13
|
+
kind: "recursiveDirectory";
|
|
14
|
+
path: string;
|
|
15
|
+
budgets: RecursiveOutlineBudgets;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface RecursiveOutlineBudgets {
|
|
19
|
+
maxFiles: number;
|
|
20
|
+
maxSourceBytes: number;
|
|
21
|
+
maxDepth: number;
|
|
22
|
+
maxElapsedMs: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const RECURSIVE_OUTLINE_BUDGETS: RecursiveOutlineBudgets = {
|
|
26
|
+
maxFiles: 2000,
|
|
27
|
+
maxSourceBytes: 64 * 1024 * 1024,
|
|
28
|
+
maxDepth: 32,
|
|
29
|
+
maxElapsedMs: 20_000,
|
|
30
|
+
};
|
|
19
31
|
|
|
20
32
|
export interface SourcePosition {
|
|
21
33
|
line: number;
|
|
@@ -45,7 +57,7 @@ export interface OutlineEntry {
|
|
|
45
57
|
locator?: string;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
export type SymbolView = "signature" | "declaration" | "declarationWithImports";
|
|
60
|
+
export type SymbolView = "signature" | "signatureWithDocs" | "declaration" | "declarationWithImports";
|
|
49
61
|
|
|
50
62
|
export interface OutlineItem extends OutlineEntry {
|
|
51
63
|
rowKind: "package" | "import" | "declaration" | "export" | "sideEffect";
|
|
@@ -71,12 +83,43 @@ export interface OutlineTargetResult {
|
|
|
71
83
|
totalLineCount: number;
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
export interface RecursiveOutlineDiagnostic {
|
|
87
|
+
relativePath: string;
|
|
88
|
+
language: AstLanguage | undefined;
|
|
89
|
+
code: string;
|
|
90
|
+
message: string;
|
|
91
|
+
sourceFingerprint: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RecursiveOutlineSummary {
|
|
95
|
+
discoveredFiles: number;
|
|
96
|
+
supportedFiles: number;
|
|
97
|
+
unsupportedFiles: number;
|
|
98
|
+
emittedFiles: number;
|
|
99
|
+
unreadableFiles: number;
|
|
100
|
+
oversizedFiles: number;
|
|
101
|
+
failedFiles: number;
|
|
102
|
+
parserDegradedFiles: number;
|
|
103
|
+
totalByteLength: number;
|
|
104
|
+
totalLineCount: number;
|
|
105
|
+
fileLimitReached: boolean;
|
|
106
|
+
sourceByteLimitReached: boolean;
|
|
107
|
+
depthLimitReached: boolean;
|
|
108
|
+
elapsedLimitReached: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface RecursiveOutlineCallbacks {
|
|
112
|
+
onFile(relativePath: string, file: OutlineFileResult): Promise<void>;
|
|
113
|
+
onDiagnostic(diagnostic: RecursiveOutlineDiagnostic): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
export interface SymbolDeclaration {
|
|
75
117
|
locator: string;
|
|
76
118
|
path: string;
|
|
77
119
|
language: AstLanguage;
|
|
78
120
|
sourceFingerprint: string;
|
|
79
121
|
declarationRange: SourceRange;
|
|
122
|
+
diagnostics: string[];
|
|
80
123
|
}
|
|
81
124
|
|
|
82
125
|
export interface SymbolBlock {
|
|
@@ -91,6 +134,266 @@ export interface SymbolBatchResult {
|
|
|
91
134
|
blocks: SymbolBlock[];
|
|
92
135
|
}
|
|
93
136
|
|
|
137
|
+
export type ApiDeclarationKind =
|
|
138
|
+
| "module"
|
|
139
|
+
| "namespace"
|
|
140
|
+
| "package"
|
|
141
|
+
| "class"
|
|
142
|
+
| "method"
|
|
143
|
+
| "property"
|
|
144
|
+
| "field"
|
|
145
|
+
| "constructor"
|
|
146
|
+
| "enum"
|
|
147
|
+
| "interface"
|
|
148
|
+
| "function"
|
|
149
|
+
| "variable"
|
|
150
|
+
| "constant"
|
|
151
|
+
| "object"
|
|
152
|
+
| "enumMember"
|
|
153
|
+
| "struct"
|
|
154
|
+
| "event"
|
|
155
|
+
| "operator"
|
|
156
|
+
| "typeParameter"
|
|
157
|
+
| "heading";
|
|
158
|
+
|
|
159
|
+
export type ApiQuery =
|
|
160
|
+
| { kind: "exactName"; name: string }
|
|
161
|
+
| { kind: "prefixName"; name: string }
|
|
162
|
+
| { kind: "substringName"; name: string }
|
|
163
|
+
| { kind: "fuzzyName"; name: string; maxCandidates: number; maxWork: number }
|
|
164
|
+
| { kind: "declarationKind"; declarationKind: ApiDeclarationKind }
|
|
165
|
+
| { kind: "documentation"; terms: string[]; maxCandidates: number; maxWork: number };
|
|
166
|
+
|
|
167
|
+
export type ApiSurfaceFilter = "all" | "public" | "private" | "sourceExport" | "packageSurface";
|
|
168
|
+
|
|
169
|
+
export interface ApiCandidate {
|
|
170
|
+
locator: string;
|
|
171
|
+
language: AstLanguage;
|
|
172
|
+
sourceFingerprint: string;
|
|
173
|
+
name: string;
|
|
174
|
+
qualifiedName: string;
|
|
175
|
+
symbolType: ApiDeclarationKind;
|
|
176
|
+
signature: string;
|
|
177
|
+
definingFile: string;
|
|
178
|
+
range: SourceRange;
|
|
179
|
+
visibility: "public" | "protected" | "internal" | "packagePrivate" | "filePrivate" | "private" | "unknown";
|
|
180
|
+
sourceExport: "yes" | "no" | "unknown";
|
|
181
|
+
packageSurface: "yes" | "no" | "unknown";
|
|
182
|
+
internalOnly: "yes" | "no" | "unknown";
|
|
183
|
+
reExportChain: string[];
|
|
184
|
+
callerAccess: {
|
|
185
|
+
modulePath: string;
|
|
186
|
+
importStatement: string;
|
|
187
|
+
accessExpression: string;
|
|
188
|
+
form: "direct" | "qualified";
|
|
189
|
+
} | null;
|
|
190
|
+
provenance: "exact" | "inferred" | "ambiguous" | "unsupported";
|
|
191
|
+
certainty: "certain" | "recovered" | "nearRecovery";
|
|
192
|
+
certaintyReason: string | null;
|
|
193
|
+
uncertainty: string | null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface ApiDiscoverySummary {
|
|
197
|
+
filesScanned: number;
|
|
198
|
+
declarationsConsidered: number;
|
|
199
|
+
resultsReturned: number;
|
|
200
|
+
resultLimit: number;
|
|
201
|
+
omittedCandidates: number;
|
|
202
|
+
candidateLimitReached: boolean;
|
|
203
|
+
workLimitReached: boolean;
|
|
204
|
+
resolutionDiagnostics: number;
|
|
205
|
+
totalSourceBytes: number;
|
|
206
|
+
fileLimitReached: boolean;
|
|
207
|
+
sourceByteLimitReached: boolean;
|
|
208
|
+
depthLimitReached: boolean;
|
|
209
|
+
elapsedLimitReached: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ApiDiscoveryResult {
|
|
213
|
+
path: string;
|
|
214
|
+
candidates: ApiCandidate[];
|
|
215
|
+
summary: ApiDiscoverySummary;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface AstSearchBindingValue {
|
|
219
|
+
range: SourceRange;
|
|
220
|
+
preview: string;
|
|
221
|
+
previewTruncated: boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface AstSearchBinding {
|
|
225
|
+
name: string;
|
|
226
|
+
values: AstSearchBindingValue[];
|
|
227
|
+
valuesTruncated: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface AstSearchScope {
|
|
231
|
+
astKind: string;
|
|
232
|
+
range: SourceRange;
|
|
233
|
+
preview: string;
|
|
234
|
+
previewTruncated: boolean;
|
|
235
|
+
locator: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface AstSearchMatch {
|
|
239
|
+
relativePath: string;
|
|
240
|
+
language: AstLanguage;
|
|
241
|
+
sourceFingerprint: string;
|
|
242
|
+
range: SourceRange;
|
|
243
|
+
preview: string;
|
|
244
|
+
previewTruncated: boolean;
|
|
245
|
+
bindings: AstSearchBinding[];
|
|
246
|
+
bindingsTruncated: boolean;
|
|
247
|
+
certainty: "certain" | "recovered" | "nearRecovery";
|
|
248
|
+
certaintyReason?: string;
|
|
249
|
+
locator: string;
|
|
250
|
+
enclosingScope?: AstSearchScope;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface AstSearchSummary {
|
|
254
|
+
filesDiscovered: number;
|
|
255
|
+
filesFiltered: number;
|
|
256
|
+
languageFilteredFiles: number;
|
|
257
|
+
literalFilteredFiles: number;
|
|
258
|
+
filesRead: number;
|
|
259
|
+
filesParsed: number;
|
|
260
|
+
filesSearched: number;
|
|
261
|
+
unreadableFiles: number;
|
|
262
|
+
oversizedFiles: number;
|
|
263
|
+
failedFiles: number;
|
|
264
|
+
parserDegradedFiles: number;
|
|
265
|
+
sourceBytes: number;
|
|
266
|
+
matchesFound: number;
|
|
267
|
+
matchesReturned: number;
|
|
268
|
+
resultLimit: number;
|
|
269
|
+
resultLimitReached: boolean;
|
|
270
|
+
literalPrefilterApplied: boolean;
|
|
271
|
+
potentialKindPrefilterApplied: boolean;
|
|
272
|
+
diagnosticsOmitted: number;
|
|
273
|
+
fileLimitReached: boolean;
|
|
274
|
+
sourceByteLimitReached: boolean;
|
|
275
|
+
depthLimitReached: boolean;
|
|
276
|
+
elapsedLimitReached: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface AstSearchResult {
|
|
280
|
+
path: string;
|
|
281
|
+
language: AstLanguage;
|
|
282
|
+
pattern: string;
|
|
283
|
+
targetSourceFingerprint: string | null;
|
|
284
|
+
matches: AstSearchMatch[];
|
|
285
|
+
diagnostics: Array<{ relativePath: string; code: string; message: string }>;
|
|
286
|
+
summary: AstSearchSummary;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export type RelationshipOperation = "references" | "callers" | "callees" | "implementations" | "tests";
|
|
290
|
+
|
|
291
|
+
export interface EditableScope {
|
|
292
|
+
locator: string;
|
|
293
|
+
language: AstLanguage;
|
|
294
|
+
kind: string;
|
|
295
|
+
qualifiedIdentity: string;
|
|
296
|
+
range: SourceRange;
|
|
297
|
+
bodyRange: SourceRange | null;
|
|
298
|
+
sourceFingerprint: string;
|
|
299
|
+
certainty: "certain" | "recovered" | "nearRecovery";
|
|
300
|
+
certaintyReason: string | null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface RelationshipLocation {
|
|
304
|
+
relativePath: string;
|
|
305
|
+
language: AstLanguage;
|
|
306
|
+
sourceFingerprint: string;
|
|
307
|
+
range: SourceRange;
|
|
308
|
+
relationshipKind:
|
|
309
|
+
| "reference"
|
|
310
|
+
| "typeUsage"
|
|
311
|
+
| "caller"
|
|
312
|
+
| "callee"
|
|
313
|
+
| "implementation"
|
|
314
|
+
| "override"
|
|
315
|
+
| "reExport"
|
|
316
|
+
| "test";
|
|
317
|
+
certainty: "exact" | "inferred" | "ambiguous";
|
|
318
|
+
parseCertainty: "certain" | "recovered" | "nearRecovery";
|
|
319
|
+
certaintyReason: string | null;
|
|
320
|
+
classification: "production" | "test" | "generated" | "reExport";
|
|
321
|
+
targetLocator: string;
|
|
322
|
+
targetPath: string;
|
|
323
|
+
targetSourceFingerprint: string;
|
|
324
|
+
candidateLocators: string[];
|
|
325
|
+
candidatePaths: string[];
|
|
326
|
+
candidateSourceFingerprints: string[];
|
|
327
|
+
competingCandidatesOmitted: number;
|
|
328
|
+
actionable: boolean;
|
|
329
|
+
sitePreview: string;
|
|
330
|
+
sitePreviewTruncated: boolean;
|
|
331
|
+
enclosingScope: EditableScope;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface RelationshipResult {
|
|
335
|
+
path: string;
|
|
336
|
+
operation: RelationshipOperation;
|
|
337
|
+
targetName: string;
|
|
338
|
+
targetLocator: string;
|
|
339
|
+
relationships: RelationshipLocation[];
|
|
340
|
+
summary: {
|
|
341
|
+
filesScanned: number;
|
|
342
|
+
sourceBytes: number;
|
|
343
|
+
parserDegradedFiles: number;
|
|
344
|
+
relationshipsFound: number;
|
|
345
|
+
relationshipsReturned: number;
|
|
346
|
+
resultLimit: number;
|
|
347
|
+
resultLimitReached: boolean;
|
|
348
|
+
ambiguousRelationships: number;
|
|
349
|
+
diagnostics: number;
|
|
350
|
+
fileLimitReached: boolean;
|
|
351
|
+
sourceByteLimitReached: boolean;
|
|
352
|
+
depthLimitReached: boolean;
|
|
353
|
+
elapsedLimitReached: boolean;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export type EditOperation =
|
|
358
|
+
| { kind: "replaceDeclaration"; source: string }
|
|
359
|
+
| { kind: "replaceBody"; body: string }
|
|
360
|
+
| { kind: "insertDeclaration"; position: "before" | "after"; source: string }
|
|
361
|
+
| {
|
|
362
|
+
kind: "renameDeclaration";
|
|
363
|
+
newName: string;
|
|
364
|
+
scope: { kind: "file" } | { kind: "repository"; path: string };
|
|
365
|
+
includeInferred: boolean;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
export interface PlannedEdit {
|
|
369
|
+
range: SourceRange;
|
|
370
|
+
replacement: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export interface EditFilePlan {
|
|
374
|
+
path: string;
|
|
375
|
+
expectedFingerprint: string;
|
|
376
|
+
source: string;
|
|
377
|
+
edits: PlannedEdit[];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface EditPlanResult {
|
|
381
|
+
files: EditFilePlan[];
|
|
382
|
+
skippedImpacts: Array<{
|
|
383
|
+
path: string;
|
|
384
|
+
range: SourceRange;
|
|
385
|
+
reason: "ambiguous" | "uncertainParse" | "inferredNotApproved";
|
|
386
|
+
candidateLocators: string[];
|
|
387
|
+
candidatePaths: string[];
|
|
388
|
+
}>;
|
|
389
|
+
freshLocators: Array<{
|
|
390
|
+
locator: string;
|
|
391
|
+
path: string;
|
|
392
|
+
name: string;
|
|
393
|
+
sourceFingerprint: string;
|
|
394
|
+
}>;
|
|
395
|
+
}
|
|
396
|
+
|
|
94
397
|
export interface AstClient {
|
|
95
398
|
getGeneration(): number;
|
|
96
399
|
outline(
|
|
@@ -100,37 +403,133 @@ export interface AstClient {
|
|
|
100
403
|
names: string[],
|
|
101
404
|
signal: AbortSignal | undefined,
|
|
102
405
|
): Promise<OutlineTargetResult>;
|
|
406
|
+
outlineRecursive(
|
|
407
|
+
path: string,
|
|
408
|
+
includePrivate: boolean,
|
|
409
|
+
includeDocs: boolean,
|
|
410
|
+
names: string[],
|
|
411
|
+
callbacks: RecursiveOutlineCallbacks,
|
|
412
|
+
signal: AbortSignal | undefined,
|
|
413
|
+
): Promise<RecursiveOutlineSummary>;
|
|
103
414
|
symbol(
|
|
104
415
|
locators: string[],
|
|
105
416
|
view: SymbolView,
|
|
106
417
|
contextLines: number,
|
|
107
418
|
signal: AbortSignal | undefined,
|
|
108
419
|
): Promise<SymbolBatchResult>;
|
|
420
|
+
discoverApi(
|
|
421
|
+
path: string,
|
|
422
|
+
query: ApiQuery,
|
|
423
|
+
surface: ApiSurfaceFilter,
|
|
424
|
+
resultLimit: number,
|
|
425
|
+
signal: AbortSignal | undefined,
|
|
426
|
+
): Promise<ApiDiscoveryResult>;
|
|
427
|
+
search(
|
|
428
|
+
path: string,
|
|
429
|
+
language: AstLanguage,
|
|
430
|
+
pattern: string,
|
|
431
|
+
resultLimit: number,
|
|
432
|
+
signal: AbortSignal | undefined,
|
|
433
|
+
): Promise<AstSearchResult>;
|
|
434
|
+
relationships(
|
|
435
|
+
path: string,
|
|
436
|
+
locator: string,
|
|
437
|
+
relationship: RelationshipOperation,
|
|
438
|
+
resultLimit: number,
|
|
439
|
+
signal: AbortSignal | undefined,
|
|
440
|
+
): Promise<RelationshipResult>;
|
|
441
|
+
planEdit(locator: string, edit: EditOperation, signal: AbortSignal | undefined): Promise<EditPlanResult>;
|
|
109
442
|
shutdown(): Promise<void>;
|
|
110
443
|
}
|
|
111
444
|
|
|
112
445
|
type WorkerRequestPayload =
|
|
113
446
|
| { operation: "handshake" }
|
|
114
|
-
| {
|
|
115
|
-
|
|
447
|
+
| {
|
|
448
|
+
operation: "outline";
|
|
449
|
+
target: OutlineTarget;
|
|
450
|
+
includePrivate: boolean;
|
|
451
|
+
includeDocs: boolean;
|
|
452
|
+
names: string[];
|
|
453
|
+
}
|
|
454
|
+
| {
|
|
455
|
+
operation: "symbol";
|
|
456
|
+
locators: string[];
|
|
457
|
+
view: SymbolView;
|
|
458
|
+
contextLines: number;
|
|
459
|
+
}
|
|
460
|
+
| {
|
|
461
|
+
operation: "apiDiscover";
|
|
462
|
+
path: string;
|
|
463
|
+
budgets: RecursiveOutlineBudgets;
|
|
464
|
+
query: ApiQuery;
|
|
465
|
+
surface: ApiSurfaceFilter;
|
|
466
|
+
resultLimit: number;
|
|
467
|
+
}
|
|
468
|
+
| {
|
|
469
|
+
operation: "astSearch";
|
|
470
|
+
path: string;
|
|
471
|
+
language: AstLanguage;
|
|
472
|
+
budgets: RecursiveOutlineBudgets;
|
|
473
|
+
pattern: string;
|
|
474
|
+
resultLimit: number;
|
|
475
|
+
}
|
|
476
|
+
| {
|
|
477
|
+
operation: "relationships";
|
|
478
|
+
path: string;
|
|
479
|
+
budgets: RecursiveOutlineBudgets;
|
|
480
|
+
locator: string;
|
|
481
|
+
relationship: RelationshipOperation;
|
|
482
|
+
resultLimit: number;
|
|
483
|
+
}
|
|
484
|
+
| {
|
|
485
|
+
operation: "planEdit";
|
|
486
|
+
locator: string;
|
|
487
|
+
edit: EditOperation;
|
|
488
|
+
budgets: RecursiveOutlineBudgets;
|
|
489
|
+
};
|
|
116
490
|
|
|
117
491
|
interface WorkerResponse {
|
|
118
492
|
requestId: number;
|
|
119
493
|
protocolVersion: number;
|
|
120
494
|
success: boolean;
|
|
121
495
|
result?: Record<string, unknown>;
|
|
122
|
-
error?: { code?: string; message?: string };
|
|
496
|
+
error?: { code?: string; message?: string; sourceFingerprint?: string };
|
|
123
497
|
}
|
|
124
498
|
|
|
125
|
-
|
|
499
|
+
export class AstWorkerError extends Error {
|
|
500
|
+
readonly code: string;
|
|
501
|
+
readonly sourceFingerprint: string | undefined;
|
|
502
|
+
|
|
503
|
+
constructor(code: string, message: string, sourceFingerprint: string | undefined = undefined) {
|
|
504
|
+
super(message);
|
|
505
|
+
this.name = "AstWorkerError";
|
|
506
|
+
this.code = code;
|
|
507
|
+
this.sourceFingerprint = sourceFingerprint;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
interface PendingUnaryRequest {
|
|
512
|
+
kind: "unary";
|
|
126
513
|
resolve(value: Record<string, unknown>): void;
|
|
127
514
|
reject(error: Error): void;
|
|
128
515
|
removeAbortListener(): void;
|
|
129
516
|
}
|
|
130
517
|
|
|
131
|
-
|
|
518
|
+
interface PendingStreamRequest {
|
|
519
|
+
kind: "recursiveOutline";
|
|
520
|
+
started: boolean;
|
|
521
|
+
callbacks: RecursiveOutlineCallbacks;
|
|
522
|
+
resolve(value: RecursiveOutlineSummary): void;
|
|
523
|
+
reject(error: Error): void;
|
|
524
|
+
removeAbortListener(): void;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
type PendingRequest = PendingUnaryRequest | PendingStreamRequest;
|
|
528
|
+
|
|
529
|
+
const PROTOCOL_VERSION = 13;
|
|
132
530
|
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
133
531
|
const STDERR_BYTES = 16 * 1024;
|
|
532
|
+
const HANDSHAKE_TIMEOUT_MS = 2000;
|
|
134
533
|
|
|
135
534
|
export type AstWorkerResolution = { command: string } | { error: Error };
|
|
136
535
|
|
|
@@ -144,12 +543,14 @@ export function resolveAstWorkerCommand(
|
|
|
144
543
|
|
|
145
544
|
const sourceRoot = join(packageRoot, "native", "tau-ast");
|
|
146
545
|
if (existsSync(join(sourceRoot, "Cargo.toml")))
|
|
147
|
-
return {
|
|
546
|
+
return {
|
|
547
|
+
command: join(sourceRoot, "target", "release", `tau-ast${platform === "win32" ? ".exe" : ""}`),
|
|
548
|
+
};
|
|
148
549
|
|
|
149
550
|
if (platform === "darwin" && arch === "arm64")
|
|
150
551
|
return {
|
|
151
552
|
error: new Error(
|
|
152
|
-
"tau-ast is missing from this @shanepadgett/tau-agent installation. Reinstall the package before using outline or symbol.",
|
|
553
|
+
"tau-ast is missing from this @shanepadgett/tau-agent installation. Reinstall the package before using api_discover, ast_search, outline, or symbol.",
|
|
153
554
|
),
|
|
154
555
|
};
|
|
155
556
|
return {
|
|
@@ -159,6 +560,10 @@ export function resolveAstWorkerCommand(
|
|
|
159
560
|
};
|
|
160
561
|
}
|
|
161
562
|
|
|
563
|
+
function resolveDefaultAstWorkerCommand(): AstWorkerResolution {
|
|
564
|
+
return resolveAstWorkerCommand(fileURLToPath(new URL("../../", import.meta.url)), process.platform, process.arch);
|
|
565
|
+
}
|
|
566
|
+
|
|
162
567
|
export class AstWorkerClient implements AstClient {
|
|
163
568
|
private readonly command: string | undefined;
|
|
164
569
|
private readonly args: readonly string[];
|
|
@@ -167,8 +572,10 @@ export class AstWorkerClient implements AstClient {
|
|
|
167
572
|
private pending = new Map<number, PendingRequest>();
|
|
168
573
|
private nextRequestId = 1;
|
|
169
574
|
private incoming = Buffer.alloc(0);
|
|
575
|
+
private readonly drainingChildren = new Set<ChildProcessWithoutNullStreams>();
|
|
170
576
|
private stderr = "";
|
|
171
577
|
private generation = 0;
|
|
578
|
+
private capabilities: readonly AstLanguage[] | undefined;
|
|
172
579
|
|
|
173
580
|
constructor(command: string | undefined = undefined, args: readonly string[] = []) {
|
|
174
581
|
this.command = command;
|
|
@@ -179,6 +586,16 @@ export class AstWorkerClient implements AstClient {
|
|
|
179
586
|
return this.generation;
|
|
180
587
|
}
|
|
181
588
|
|
|
589
|
+
private resolveCommand(): AstWorkerResolution {
|
|
590
|
+
return this.command ? { command: this.command } : resolveDefaultAstWorkerCommand();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async supportedLanguages(): Promise<readonly AstLanguage[]> {
|
|
594
|
+
await this.ensureStarted();
|
|
595
|
+
if (!this.capabilities) throw new Error("tau-ast handshake omitted supported languages");
|
|
596
|
+
return this.capabilities;
|
|
597
|
+
}
|
|
598
|
+
|
|
182
599
|
async outline(
|
|
183
600
|
target: OutlineTarget,
|
|
184
601
|
includePrivate: boolean,
|
|
@@ -191,6 +608,27 @@ export class AstWorkerClient implements AstClient {
|
|
|
191
608
|
return result as unknown as OutlineTargetResult;
|
|
192
609
|
}
|
|
193
610
|
|
|
611
|
+
async outlineRecursive(
|
|
612
|
+
path: string,
|
|
613
|
+
includePrivate: boolean,
|
|
614
|
+
includeDocs: boolean,
|
|
615
|
+
names: string[],
|
|
616
|
+
callbacks: RecursiveOutlineCallbacks,
|
|
617
|
+
signal: AbortSignal | undefined,
|
|
618
|
+
): Promise<RecursiveOutlineSummary> {
|
|
619
|
+
await this.ensureStarted();
|
|
620
|
+
const target: OutlineTarget = {
|
|
621
|
+
kind: "recursiveDirectory",
|
|
622
|
+
path,
|
|
623
|
+
budgets: RECURSIVE_OUTLINE_BUDGETS,
|
|
624
|
+
};
|
|
625
|
+
return this.sendRecursive(
|
|
626
|
+
{ operation: "outline", target, includePrivate, includeDocs, names },
|
|
627
|
+
callbacks,
|
|
628
|
+
signal,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
194
632
|
async symbol(
|
|
195
633
|
locators: string[],
|
|
196
634
|
view: SymbolView,
|
|
@@ -202,11 +640,66 @@ export class AstWorkerClient implements AstClient {
|
|
|
202
640
|
return result as unknown as SymbolBatchResult;
|
|
203
641
|
}
|
|
204
642
|
|
|
643
|
+
async discoverApi(
|
|
644
|
+
path: string,
|
|
645
|
+
query: ApiQuery,
|
|
646
|
+
surface: ApiSurfaceFilter,
|
|
647
|
+
resultLimit: number,
|
|
648
|
+
signal: AbortSignal | undefined,
|
|
649
|
+
): Promise<ApiDiscoveryResult> {
|
|
650
|
+
const result = await this.request(
|
|
651
|
+
{ operation: "apiDiscover", path, budgets: RECURSIVE_OUTLINE_BUDGETS, query, surface, resultLimit },
|
|
652
|
+
signal,
|
|
653
|
+
);
|
|
654
|
+
if (result.kind !== "apiDiscovery") throw new Error("tau-ast returned the wrong result for API discovery");
|
|
655
|
+
return result as unknown as ApiDiscoveryResult;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async search(
|
|
659
|
+
path: string,
|
|
660
|
+
language: AstLanguage,
|
|
661
|
+
pattern: string,
|
|
662
|
+
resultLimit: number,
|
|
663
|
+
signal: AbortSignal | undefined,
|
|
664
|
+
): Promise<AstSearchResult> {
|
|
665
|
+
const result = await this.request(
|
|
666
|
+
{ operation: "astSearch", path, language, budgets: RECURSIVE_OUTLINE_BUDGETS, pattern, resultLimit },
|
|
667
|
+
signal,
|
|
668
|
+
);
|
|
669
|
+
if (result.kind !== "astSearch") throw new Error("tau-ast returned the wrong result for ast_search");
|
|
670
|
+
return result as unknown as AstSearchResult;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async relationships(
|
|
674
|
+
path: string,
|
|
675
|
+
locator: string,
|
|
676
|
+
relationship: RelationshipOperation,
|
|
677
|
+
resultLimit: number,
|
|
678
|
+
signal: AbortSignal | undefined,
|
|
679
|
+
): Promise<RelationshipResult> {
|
|
680
|
+
const result = await this.request(
|
|
681
|
+
{ operation: "relationships", path, budgets: RECURSIVE_OUTLINE_BUDGETS, locator, relationship, resultLimit },
|
|
682
|
+
signal,
|
|
683
|
+
);
|
|
684
|
+
if (result.kind !== "relationships") throw new Error("tau-ast returned the wrong result for relationships");
|
|
685
|
+
return result as unknown as RelationshipResult;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async planEdit(locator: string, edit: EditOperation, signal: AbortSignal | undefined): Promise<EditPlanResult> {
|
|
689
|
+
const result = await this.request(
|
|
690
|
+
{ operation: "planEdit", locator, edit, budgets: RECURSIVE_OUTLINE_BUDGETS },
|
|
691
|
+
signal,
|
|
692
|
+
);
|
|
693
|
+
if (result.kind !== "editPlan") throw new Error("tau-ast returned the wrong result for edit planning");
|
|
694
|
+
return result as unknown as EditPlanResult;
|
|
695
|
+
}
|
|
696
|
+
|
|
205
697
|
async shutdown(): Promise<void> {
|
|
206
698
|
const child = this.child;
|
|
207
699
|
if (!child) return;
|
|
208
700
|
this.child = undefined;
|
|
209
701
|
this.startPromise = undefined;
|
|
702
|
+
this.capabilities = undefined;
|
|
210
703
|
this.rejectPending(new Error("tau-ast worker shut down"));
|
|
211
704
|
child.stdin.end();
|
|
212
705
|
if (child.exitCode !== null) return;
|
|
@@ -238,11 +731,11 @@ export class AstWorkerClient implements AstClient {
|
|
|
238
731
|
}
|
|
239
732
|
|
|
240
733
|
private async start(): Promise<void> {
|
|
241
|
-
const resolution = this.
|
|
242
|
-
? { command: this.command }
|
|
243
|
-
: resolveAstWorkerCommand(fileURLToPath(new URL("../../", import.meta.url)), process.platform, process.arch);
|
|
734
|
+
const resolution = this.resolveCommand();
|
|
244
735
|
if ("error" in resolution) throw resolution.error;
|
|
245
|
-
const child = spawn(resolution.command, this.args, {
|
|
736
|
+
const child = spawn(resolution.command, this.args, {
|
|
737
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
738
|
+
});
|
|
246
739
|
this.generation += 1;
|
|
247
740
|
this.child = child;
|
|
248
741
|
this.incoming = Buffer.alloc(0);
|
|
@@ -259,17 +752,59 @@ export class AstWorkerClient implements AstClient {
|
|
|
259
752
|
const suffix = this.stderr.trim() ? `: ${this.stderr.trim()}` : "";
|
|
260
753
|
this.fail(child, new Error(`tau-ast exited (${signal ?? code ?? "unknown"})${suffix}`), false);
|
|
261
754
|
});
|
|
755
|
+
const handshakeTimeout = setTimeout(
|
|
756
|
+
() => this.fail(child, new Error(`tau-ast handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms`)),
|
|
757
|
+
HANDSHAKE_TIMEOUT_MS,
|
|
758
|
+
);
|
|
262
759
|
try {
|
|
263
760
|
const result = await this.send({ operation: "handshake" }, undefined);
|
|
264
761
|
if (result.kind !== "handshake") throw new Error("tau-ast handshake returned the wrong result");
|
|
762
|
+
if (!Array.isArray(result.supportedLanguages)) {
|
|
763
|
+
throw new Error("tau-ast handshake omitted supported languages");
|
|
764
|
+
}
|
|
765
|
+
this.capabilities = result.supportedLanguages.filter(isAstLanguage);
|
|
265
766
|
} catch (error) {
|
|
266
767
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
267
768
|
this.fail(child, failure);
|
|
268
769
|
throw failure;
|
|
770
|
+
} finally {
|
|
771
|
+
clearTimeout(handshakeTimeout);
|
|
269
772
|
}
|
|
270
773
|
}
|
|
271
774
|
|
|
272
775
|
private send(request: WorkerRequestPayload, signal: AbortSignal | undefined): Promise<Record<string, unknown>> {
|
|
776
|
+
return this.sendPending(request, signal, (resolve, reject, removeAbortListener) => ({
|
|
777
|
+
kind: "unary",
|
|
778
|
+
resolve,
|
|
779
|
+
reject,
|
|
780
|
+
removeAbortListener,
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private sendRecursive(
|
|
785
|
+
request: WorkerRequestPayload,
|
|
786
|
+
callbacks: RecursiveOutlineCallbacks,
|
|
787
|
+
signal: AbortSignal | undefined,
|
|
788
|
+
): Promise<RecursiveOutlineSummary> {
|
|
789
|
+
return this.sendPending(request, signal, (resolve, reject, removeAbortListener) => ({
|
|
790
|
+
kind: "recursiveOutline",
|
|
791
|
+
started: false,
|
|
792
|
+
callbacks,
|
|
793
|
+
resolve,
|
|
794
|
+
reject,
|
|
795
|
+
removeAbortListener,
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private sendPending<T>(
|
|
800
|
+
request: WorkerRequestPayload,
|
|
801
|
+
signal: AbortSignal | undefined,
|
|
802
|
+
createPending: (
|
|
803
|
+
resolve: (value: T) => void,
|
|
804
|
+
reject: (error: Error) => void,
|
|
805
|
+
removeAbortListener: () => void,
|
|
806
|
+
) => PendingRequest,
|
|
807
|
+
): Promise<T> {
|
|
273
808
|
const child = this.child;
|
|
274
809
|
if (!child) return Promise.reject(new Error("tau-ast worker is not running"));
|
|
275
810
|
if (signal?.aborted) return Promise.reject(new Error("tau-ast request cancelled"));
|
|
@@ -279,14 +814,13 @@ export class AstWorkerClient implements AstClient {
|
|
|
279
814
|
frame.writeUInt32BE(payload.length, 0);
|
|
280
815
|
payload.copy(frame, 4);
|
|
281
816
|
|
|
282
|
-
return new Promise<
|
|
817
|
+
return new Promise<T>((resolve, reject) => {
|
|
283
818
|
const onAbort = (): void => this.fail(child, new Error("tau-ast request cancelled"));
|
|
284
819
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
285
|
-
this.pending.set(
|
|
286
|
-
|
|
287
|
-
reject,
|
|
288
|
-
|
|
289
|
-
});
|
|
820
|
+
this.pending.set(
|
|
821
|
+
requestId,
|
|
822
|
+
createPending(resolve, reject, () => signal?.removeEventListener("abort", onAbort)),
|
|
823
|
+
);
|
|
290
824
|
child.stdin.write(frame, (error) => {
|
|
291
825
|
if (error) this.fail(child, new Error(`Failed to write tau-ast request: ${error.message}`));
|
|
292
826
|
});
|
|
@@ -296,44 +830,112 @@ export class AstWorkerClient implements AstClient {
|
|
|
296
830
|
private receive(child: ChildProcessWithoutNullStreams, chunk: Buffer): void {
|
|
297
831
|
if (this.child !== child) return;
|
|
298
832
|
this.incoming = Buffer.concat([this.incoming, chunk]);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
833
|
+
void this.drain(child);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private async drain(child: ChildProcessWithoutNullStreams): Promise<void> {
|
|
837
|
+
if (this.drainingChildren.has(child) || this.child !== child) return;
|
|
838
|
+
this.drainingChildren.add(child);
|
|
839
|
+
try {
|
|
840
|
+
while (this.child === child && this.incoming.length >= 4) {
|
|
841
|
+
const length = this.incoming.readUInt32BE(0);
|
|
842
|
+
if (length > MAX_FRAME_BYTES) {
|
|
843
|
+
this.fail(child, new Error(`tau-ast response frame exceeds ${MAX_FRAME_BYTES} bytes`));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (this.incoming.length < length + 4) return;
|
|
847
|
+
const payload = this.incoming.subarray(4, length + 4);
|
|
848
|
+
this.incoming = this.incoming.subarray(length + 4);
|
|
849
|
+
let response: WorkerResponse;
|
|
850
|
+
try {
|
|
851
|
+
response = JSON.parse(payload.toString("utf8")) as WorkerResponse;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
this.fail(child, new Error(`tau-ast returned malformed JSON: ${String(error)}`));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const pending = this.pending.get(response.requestId);
|
|
857
|
+
if (!pending) {
|
|
858
|
+
this.fail(child, new Error(`tau-ast returned unknown request id ${String(response.requestId)}`));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (response.protocolVersion !== PROTOCOL_VERSION) {
|
|
862
|
+
const error = new Error(`tau-ast response used protocol ${response.protocolVersion}`);
|
|
863
|
+
this.fail(child, error);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (!response.success) {
|
|
867
|
+
this.pending.delete(response.requestId);
|
|
868
|
+
pending.removeAbortListener();
|
|
869
|
+
pending.reject(
|
|
870
|
+
new AstWorkerError(
|
|
871
|
+
response.error?.code ?? "worker_error",
|
|
872
|
+
response.error?.message ?? response.error?.code ?? "tau-ast request failed",
|
|
873
|
+
response.error?.sourceFingerprint,
|
|
874
|
+
),
|
|
875
|
+
);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (!response.result || typeof response.result !== "object") {
|
|
879
|
+
this.fail(child, new Error("tau-ast response omitted its result"));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (pending.kind === "unary") {
|
|
883
|
+
this.pending.delete(response.requestId);
|
|
884
|
+
pending.removeAbortListener();
|
|
885
|
+
pending.resolve(response.result);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const kind = response.result.kind;
|
|
889
|
+
if (kind === "recursiveStart") {
|
|
890
|
+
if (pending.started || typeof response.result.path !== "string" || !isRecord(response.result.budgets)) {
|
|
891
|
+
this.fail(child, new Error("tau-ast recursive outline returned more than one start frame"));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
pending.started = true;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (!pending.started) {
|
|
898
|
+
this.fail(child, new Error("tau-ast recursive outline omitted its start frame"));
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (kind === "recursiveFile") {
|
|
902
|
+
const file = parseRecursiveFile(response.result);
|
|
903
|
+
if (!file) {
|
|
904
|
+
this.fail(child, new Error("tau-ast recursive file frame is malformed"));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
await pending.callbacks.onFile(response.result.relativePath as string, file);
|
|
908
|
+
if (this.child !== child) return;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (kind === "recursiveDiagnostic") {
|
|
912
|
+
const diagnostic = parseRecursiveDiagnostic(response.result);
|
|
913
|
+
if (!diagnostic) {
|
|
914
|
+
this.fail(child, new Error("tau-ast recursive diagnostic frame is malformed"));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
await pending.callbacks.onDiagnostic(diagnostic);
|
|
918
|
+
if (this.child !== child) return;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (kind === "recursiveComplete") {
|
|
922
|
+
const summary = parseRecursiveSummary(response.result);
|
|
923
|
+
if (!summary) {
|
|
924
|
+
this.fail(child, new Error("tau-ast recursive completion frame is malformed"));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.pending.delete(response.requestId);
|
|
928
|
+
pending.removeAbortListener();
|
|
929
|
+
pending.resolve(summary);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
this.fail(child, new Error(`tau-ast recursive outline returned unexpected frame ${String(kind)}`));
|
|
326
933
|
return;
|
|
327
934
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!response.result || typeof response.result !== "object") {
|
|
333
|
-
pending.reject(new Error("tau-ast response omitted its result"));
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
pending.resolve(response.result);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
this.fail(child, error instanceof Error ? error : new Error(String(error)));
|
|
937
|
+
} finally {
|
|
938
|
+
this.drainingChildren.delete(child);
|
|
337
939
|
}
|
|
338
940
|
}
|
|
339
941
|
|
|
@@ -342,6 +944,7 @@ export class AstWorkerClient implements AstClient {
|
|
|
342
944
|
this.child = undefined;
|
|
343
945
|
this.generation += 1;
|
|
344
946
|
this.incoming = Buffer.alloc(0);
|
|
947
|
+
this.capabilities = undefined;
|
|
345
948
|
this.rejectPending(error);
|
|
346
949
|
if (kill && child.exitCode === null) child.kill();
|
|
347
950
|
}
|
|
@@ -354,3 +957,69 @@ export class AstWorkerClient implements AstClient {
|
|
|
354
957
|
this.pending.clear();
|
|
355
958
|
}
|
|
356
959
|
}
|
|
960
|
+
|
|
961
|
+
function parseRecursiveFile(result: Record<string, unknown>): OutlineFileResult | undefined {
|
|
962
|
+
if (typeof result.relativePath !== "string" || !isRecord(result.file)) return undefined;
|
|
963
|
+
const file = result.file;
|
|
964
|
+
if (
|
|
965
|
+
typeof file.path !== "string" ||
|
|
966
|
+
!isAstLanguage(file.language) ||
|
|
967
|
+
typeof file.sourceFingerprint !== "string" ||
|
|
968
|
+
!isCount(file.byteLength) ||
|
|
969
|
+
!isCount(file.lineCount) ||
|
|
970
|
+
!isRecord(file.diagnostics) ||
|
|
971
|
+
!isCount(file.diagnostics.errorNodes) ||
|
|
972
|
+
!isCount(file.diagnostics.missingNodes) ||
|
|
973
|
+
!Array.isArray(file.items)
|
|
974
|
+
) {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
return file as unknown as OutlineFileResult;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function parseRecursiveDiagnostic(result: Record<string, unknown>): RecursiveOutlineDiagnostic | undefined {
|
|
981
|
+
if (
|
|
982
|
+
typeof result.relativePath !== "string" ||
|
|
983
|
+
typeof result.code !== "string" ||
|
|
984
|
+
typeof result.message !== "string" ||
|
|
985
|
+
(result.language !== undefined && !isAstLanguage(result.language)) ||
|
|
986
|
+
(result.sourceFingerprint !== undefined && typeof result.sourceFingerprint !== "string")
|
|
987
|
+
) {
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
relativePath: result.relativePath,
|
|
992
|
+
language: result.language as AstLanguage | undefined,
|
|
993
|
+
code: result.code,
|
|
994
|
+
message: result.message,
|
|
995
|
+
sourceFingerprint: result.sourceFingerprint as string | undefined,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function parseRecursiveSummary(result: Record<string, unknown>): RecursiveOutlineSummary | undefined {
|
|
1000
|
+
const counts = [
|
|
1001
|
+
"discoveredFiles",
|
|
1002
|
+
"supportedFiles",
|
|
1003
|
+
"unsupportedFiles",
|
|
1004
|
+
"emittedFiles",
|
|
1005
|
+
"unreadableFiles",
|
|
1006
|
+
"oversizedFiles",
|
|
1007
|
+
"failedFiles",
|
|
1008
|
+
"parserDegradedFiles",
|
|
1009
|
+
"totalByteLength",
|
|
1010
|
+
"totalLineCount",
|
|
1011
|
+
] as const;
|
|
1012
|
+
const limits = ["fileLimitReached", "sourceByteLimitReached", "depthLimitReached", "elapsedLimitReached"] as const;
|
|
1013
|
+
if (counts.some((name) => !isCount(result[name])) || limits.some((name) => typeof result[name] !== "boolean")) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
return result as unknown as RecursiveOutlineSummary;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1020
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function isCount(value: unknown): value is number {
|
|
1024
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1025
|
+
}
|