@shanepadgett/tau-agent 0.27.0 → 0.28.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/extensions/context/sync.ts +1 -9
- package/extensions/context-pruning/prune.ts +2 -2
- package/extensions/explore/README.md +13 -21
- package/extensions/explore/ast/adapter.ts +97 -0
- package/extensions/explore/ast/engine.ts +470 -0
- package/extensions/explore/ast/format/ast-search.ts +97 -0
- package/extensions/explore/ast/format/composite.ts +26 -0
- package/extensions/explore/ast/format/context.ts +66 -0
- package/extensions/explore/ast/format/deps.ts +86 -0
- package/extensions/explore/ast/format/discover.ts +85 -0
- package/extensions/explore/ast/format/impact.ts +105 -0
- package/extensions/explore/ast/format/outline.ts +66 -0
- package/extensions/explore/ast/format/relationships.ts +81 -0
- package/extensions/explore/ast/format/show.ts +38 -0
- package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
- package/extensions/explore/ast/grammars/manifest.json +41 -0
- package/extensions/explore/ast/grammars/manifest.ts +99 -0
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/grammars/swift.wasm +0 -0
- package/extensions/explore/ast/graph/file-graph.ts +360 -0
- package/extensions/explore/ast/graph/relationships.ts +597 -0
- package/extensions/explore/ast/identity.ts +119 -0
- package/extensions/explore/ast/ir.ts +130 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
- package/extensions/explore/ast/languages/csharp.ts +493 -0
- package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
- package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
- package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
- package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
- package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
- package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
- package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
- package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
- package/extensions/explore/ast/languages/go.ts +545 -0
- package/extensions/explore/ast/languages/java.ts +412 -0
- package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
- package/extensions/explore/ast/languages/kotlin.ts +611 -0
- package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
- package/extensions/explore/ast/languages/odin.ts +407 -0
- package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
- package/extensions/explore/ast/languages/rust.ts +506 -0
- package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
- package/extensions/explore/ast/languages/swift.ts +466 -0
- package/extensions/explore/ast/languages/tree.ts +220 -0
- package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
- package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
- package/extensions/explore/ast/languages/typescript.ts +716 -0
- package/extensions/explore/ast/markdown.ts +145 -0
- package/extensions/explore/ast/package-surface.ts +42 -0
- package/extensions/explore/ast/queries/ast-search.ts +306 -0
- package/extensions/explore/ast/queries/composite-target.ts +77 -0
- package/extensions/explore/ast/queries/context.ts +406 -0
- package/extensions/explore/ast/queries/discover.ts +427 -0
- package/extensions/explore/ast/queries/impact.ts +250 -0
- package/extensions/explore/ast/queries/outline.ts +203 -0
- package/extensions/explore/ast/queries/show.ts +262 -0
- package/extensions/explore/ast/query.ts +48 -0
- package/extensions/explore/ast/read/hook.ts +125 -0
- package/extensions/explore/ast/read/policy.ts +55 -0
- package/extensions/explore/ast/registry.ts +120 -0
- package/extensions/explore/ast/scan.ts +76 -0
- package/extensions/explore/ast/slice.ts +31 -0
- package/extensions/explore/ast/tools/ast-search.ts +138 -0
- package/extensions/explore/ast/tools/context.ts +126 -0
- package/extensions/explore/ast/tools/deps.ts +72 -0
- package/extensions/explore/ast/tools/discover.ts +208 -0
- package/extensions/explore/ast/tools/impact.ts +135 -0
- package/extensions/explore/ast/tools/outline.ts +200 -0
- package/extensions/explore/ast/tools/relationships.ts +160 -0
- package/extensions/explore/ast/tools/render.ts +121 -0
- package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
- package/extensions/explore/ast/tools/show.ts +116 -0
- package/extensions/explore/guidance.ts +52 -0
- package/extensions/explore/index.ts +82 -92
- package/extensions/explore/read/autoread.ts +118 -0
- package/extensions/explore/settings.ts +49 -31
- package/extensions/explore/traverse.ts +254 -91
- package/extensions/handoff/index.ts +1 -1
- package/extensions/publish/index.ts +18 -85
- package/extensions/publish/publish-ui.ts +0 -4
- package/extensions/soul/README.md +7 -4
- package/extensions/soul/index.ts +11 -5
- package/extensions/soul/prompt.ts +31 -13
- package/extensions/soul/settings.ts +7 -2
- package/extensions/subagent/agents/context-sync.md +6 -10
- package/extensions/subagent/agents/review.md +24 -55
- package/extensions/subagent/agents/scout.md +38 -115
- package/extensions/subagent/run.ts +1 -1
- package/extensions/tau-help/help.md +2 -4
- package/package.json +6 -4
- package/schemas/tau.schema.json +35 -30
- package/{extensions/explore → shared}/autoread.ts +21 -7
- package/shared/events.ts +1 -1
- package/shared/full-file-knowledge.ts +49 -0
- package/extensions/explore/ast-guidance.ts +0 -67
- package/extensions/explore/ast-languages.ts +0 -61
- package/extensions/explore/ast-tools.ts +0 -1994
- package/extensions/explore/ast-worker.ts +0 -1025
- package/extensions/explore/find.ts +0 -147
- package/extensions/explore/full-file-knowledge.ts +0 -234
- package/extensions/explore/grep.ts +0 -749
- package/extensions/explore/limits.ts +0 -22
- package/extensions/explore/ls.ts +0 -113
- package/extensions/explore/orientation-state.ts +0 -280
- package/extensions/explore/path-display.ts +0 -41
- package/extensions/explore/path-tree.ts +0 -150
- package/extensions/explore/read-cache.ts +0 -273
- package/extensions/explore/read-snapshots.ts +0 -57
- package/extensions/explore/read-stats-panel.ts +0 -176
- package/extensions/explore/read-stats.ts +0 -138
- package/extensions/explore/read.ts +0 -372
- package/extensions/explore/result.ts +0 -29
- package/native-bin/darwin-arm64/tau-ast +0 -0
|
@@ -223,12 +223,4 @@ function changedCatalogPaths(before: Map<string, string>, after: Map<string, str
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
// Ensure evidence tool name stays aligned with agent definition tooling.
|
|
226
|
-
export const CONTEXT_SYNC_REQUIRED_TOOLS = [
|
|
227
|
-
"read",
|
|
228
|
-
"ls",
|
|
229
|
-
"find",
|
|
230
|
-
"grep",
|
|
231
|
-
"bash",
|
|
232
|
-
"patch",
|
|
233
|
-
CONTEXT_SYNC_EVIDENCE_TOOL,
|
|
234
|
-
] as const;
|
|
226
|
+
export const CONTEXT_SYNC_REQUIRED_TOOLS = ["read", "bash", "patch", CONTEXT_SYNC_EVIDENCE_TOOL] as const;
|
|
@@ -2,8 +2,8 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Static, Type } from "typebox";
|
|
4
4
|
import type { ContextPruneDeferredFileV2, ContextPruneDetailsV2 } from "../../shared/context-pruning-state.ts";
|
|
5
|
-
import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "
|
|
6
|
-
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "
|
|
5
|
+
import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "../../shared/autoread.ts";
|
|
6
|
+
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "../../shared/full-file-knowledge.ts";
|
|
7
7
|
|
|
8
8
|
export const contextPruneParameters = Type.Object(
|
|
9
9
|
{
|
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
# Explore
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Explore gives Tau 12 structural source tools: outlines, declaration slices, discovery, structural search, graph and relationship queries, impact, and context packs. Targets use path + declaration name, with an optional line to disambiguate.
|
|
4
4
|
|
|
5
|
-
Explore
|
|
5
|
+
Pi keeps ordinary filesystem tools (`ls`, `find`, `grep`, `read`). Explore adds structure on top of supported source languages via in-process tree-sitter (WASM) on every platform supported by the Node runtime. Registered languages share the same tools and exploration workflow.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Large `read` / autoread
|
|
8
8
|
|
|
9
|
-
When
|
|
9
|
+
When `explore.read.enabled` is on (default), a full Pi `read` or autoread of a registered source file (including Markdown) above `explore.read.structureThresholdLines` (default 200) returns an **outline** — declarations or headings — not the full body. Use ranged `read` (`offset`/`limit`, capped by `explore.read.maxRangeLines`) or `show` for bodies and sections. Small files and unregistered paths stay ordinary Pi full text. Set `explore.read.enabled` to `false` to turn the overlay off.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Tools
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
3. Use `api_discover` when reuse intent is known but the declaration path or exact name is not. Prefer package surfaces and public exports before private declarations.
|
|
22
|
-
4. Narrow paths, exact names, query work, and result limits once the likely target is known. Enable `includePrivate` only for targeted implementation work.
|
|
23
|
-
5. Stop at a clear outline or API candidate signature. Use `symbol(signature)` for closer contract inspection, `signatureWithDocs` only when attached documentation matters, and declaration views only for exact source.
|
|
24
|
-
6. Use `references`, `callers`, `callees`, `implementations`, or `tests` after selecting a change target and when the result can affect the plan. Inspect tests earlier only when the task begins with test behavior or a failure.
|
|
25
|
-
7. Use `ast_search` for code shapes and `grep` for literal text. Use targeted `read` for exact formatting, comments, parser gaps, unsupported files, or source outside declaration boundaries.
|
|
26
|
-
8. Prefer locator edits when a complete declaration or body is the natural edit boundary. Use textual patching when the change crosses those boundaries or depends on surrounding text.
|
|
27
|
-
|
|
28
|
-
Stop when the current question is answered. Expand exploration only for a specific unresolved question.
|
|
13
|
+
- `outline` — declarations and structure for a file, one-level directory, or recursive subtree (no bodies).
|
|
14
|
+
- `show` — exact signature / docs / declaration / declaration+imports for path+name targets.
|
|
15
|
+
- `discover` — find reusable declarations across a repo/package/subtree by name, kind, or docs (signatures only).
|
|
16
|
+
- `deps` / `reverse_deps` — file import graph forward and reverse.
|
|
17
|
+
- `callers` / `callees` / `references` / `implementations` — symbol relationship sites.
|
|
18
|
+
- `impact` — blast radius: symbol callees/callers plus file imports/importers/transitive dependents.
|
|
19
|
+
- `context` — budgeted pack of bodies/signatures around one symbol.
|
|
20
|
+
- `ast_search` — structural pattern search (ast-grep) over supported source.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Tree } from "web-tree-sitter";
|
|
2
|
+
import type { Decl, ImportRef } from "./ir.ts";
|
|
3
|
+
import type { PackageSurfaceResolver } from "./package-surface.ts";
|
|
4
|
+
|
|
5
|
+
export type LanguageCapabilities = {
|
|
6
|
+
shape: boolean;
|
|
7
|
+
search: boolean;
|
|
8
|
+
fileDeps: boolean;
|
|
9
|
+
callEdges: boolean;
|
|
10
|
+
packageSurface: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Contextual pattern wrap for languages where a bare snippet parses as the wrong kind
|
|
15
|
+
* (e.g. Go `fmt.Println($A)` → type_conversion). Shared search substitutes the user
|
|
16
|
+
* pattern for `$PATTERN` and tries each wrap after the bare pattern.
|
|
17
|
+
*/
|
|
18
|
+
export type AstSearchPatternContext = {
|
|
19
|
+
/** Source template containing the literal placeholder `$PATTERN`. */
|
|
20
|
+
readonly context: string;
|
|
21
|
+
/** tree-sitter kind name selected from the parsed context. */
|
|
22
|
+
readonly selector: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ExtractResult = {
|
|
26
|
+
decls: Decl[];
|
|
27
|
+
imports: ImportRef[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Result of adapter-owned import specifier → file resolution. */
|
|
31
|
+
export type FileDepResolution =
|
|
32
|
+
| { kind: "internal"; paths: string[] }
|
|
33
|
+
| { kind: "external"; id: string }
|
|
34
|
+
| { kind: "unresolved" };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Host services for file-dep resolution.
|
|
38
|
+
* Language-agnostic: resolvers never touch the engine type.
|
|
39
|
+
*/
|
|
40
|
+
export type FileDepHost = {
|
|
41
|
+
readonly cwd: string;
|
|
42
|
+
/** Scope root for this query. Resolvers must not walk above it for internal hits. */
|
|
43
|
+
readonly scopeRoot: string;
|
|
44
|
+
pathExists(path: string): Promise<boolean>;
|
|
45
|
+
isFile(path: string): Promise<boolean>;
|
|
46
|
+
/** Basenames; empty if missing or not a directory. */
|
|
47
|
+
readDir(path: string): Promise<string[]>;
|
|
48
|
+
/** True when path is a registered source file this graph should consider. */
|
|
49
|
+
ownsPath(path: string): boolean;
|
|
50
|
+
/** Session-scoped memo bag; cleared with graph invalidation. */
|
|
51
|
+
readonly memo: Map<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type FileDepResolver = (
|
|
55
|
+
fromPath: string,
|
|
56
|
+
specifier: string,
|
|
57
|
+
host: FileDepHost,
|
|
58
|
+
signal: AbortSignal,
|
|
59
|
+
) => Promise<FileDepResolution>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Grammar-backed language. `id` must match a pin in `ast/grammars/manifest`.
|
|
63
|
+
* Engine owns wasm load/parse; adapter only walks the tree.
|
|
64
|
+
* Language-owned hooks (`importNoiseIdentifiers`, `resolveFileDep`, optional `resolvePackageSurface`, callEdges)
|
|
65
|
+
* stay on the adapter — tools/queries never branch on language id.
|
|
66
|
+
*/
|
|
67
|
+
export type GrammarAdapter = {
|
|
68
|
+
readonly mode: "grammar";
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly extensions: readonly string[];
|
|
71
|
+
readonly capabilities: LanguageCapabilities;
|
|
72
|
+
/** Keywords/types ignored when intersecting import text with a declaration slice (`show`). */
|
|
73
|
+
readonly importNoiseIdentifiers: ReadonlySet<string>;
|
|
74
|
+
extract(tree: Tree, source: string): ExtractResult;
|
|
75
|
+
/** Required when `capabilities.fileDeps` is true. */
|
|
76
|
+
readonly resolveFileDep?: FileDepResolver;
|
|
77
|
+
readonly resolvePackageSurface?: PackageSurfaceResolver;
|
|
78
|
+
/** Optional ast_search contextual wraps; shared matcher stays language-blind. */
|
|
79
|
+
readonly patternContexts?: readonly AstSearchPatternContext[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Source-only language (Markdown). No tree-sitter grammar.
|
|
84
|
+
* Discriminant keeps engine free of `language ===` branches.
|
|
85
|
+
*/
|
|
86
|
+
export type SourceAdapter = {
|
|
87
|
+
readonly mode: "source";
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly extensions: readonly string[];
|
|
90
|
+
readonly capabilities: LanguageCapabilities;
|
|
91
|
+
readonly importNoiseIdentifiers: ReadonlySet<string>;
|
|
92
|
+
extract(source: string): ExtractResult;
|
|
93
|
+
readonly resolveFileDep?: FileDepResolver;
|
|
94
|
+
readonly resolvePackageSurface?: PackageSurfaceResolver;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type LanguageAdapter = GrammarAdapter | SourceAdapter;
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
initializeTreeSitter,
|
|
6
|
+
parse as astGrepParse,
|
|
7
|
+
registerDynamicLanguage,
|
|
8
|
+
type SgNode,
|
|
9
|
+
type SgRoot,
|
|
10
|
+
} from "@ast-grep/wasm";
|
|
11
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
12
|
+
import type { AdapterRegistry } from "./registry.ts";
|
|
13
|
+
import { createDefaultRegistry } from "./registry.ts";
|
|
14
|
+
import type { FileIr } from "./ir.ts";
|
|
15
|
+
import { grammarWasmPath, loadGrammarManifest, runtimeWasmPath, type GrammarPin } from "./grammars/manifest.ts";
|
|
16
|
+
import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../traverse.ts";
|
|
17
|
+
|
|
18
|
+
export type FileSource = {
|
|
19
|
+
ir: FileIr;
|
|
20
|
+
/** Decoded source text this IR's offsets index into (UTF-16 code units). */
|
|
21
|
+
source: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AstSearchBinding = {
|
|
25
|
+
name: string;
|
|
26
|
+
/** Exact matched text (single node or multi-node join of exact pieces). */
|
|
27
|
+
text: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type AstSearchHit = {
|
|
31
|
+
/** 1-indexed inclusive. */
|
|
32
|
+
startLine: number;
|
|
33
|
+
endLine: number;
|
|
34
|
+
/** Exact match preview (edit-grade). */
|
|
35
|
+
text: string;
|
|
36
|
+
bindings: AstSearchBinding[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ExploreEngine = {
|
|
40
|
+
/** Absolute session cwd — path resolution and directory scans use this. */
|
|
41
|
+
readonly cwd: string;
|
|
42
|
+
readonly registry: AdapterRegistry;
|
|
43
|
+
/** Resolve path, read bytes, return cached or freshly extracted FileIr. */
|
|
44
|
+
irForFile(path: string): Promise<FileIr>;
|
|
45
|
+
/** One read: IR (cached when hash matches) plus the decoded source for slicing. */
|
|
46
|
+
sourceForFile(path: string): Promise<FileSource>;
|
|
47
|
+
/**
|
|
48
|
+
* Structural pattern search over one source string.
|
|
49
|
+
* Engine owns @ast-grep/wasm init + grammar registration from pinned wasm paths.
|
|
50
|
+
* Does not use the IR cache; does not cache match results.
|
|
51
|
+
*/
|
|
52
|
+
searchInSource(languageId: string, source: string, pattern: string): Promise<AstSearchHit[]>;
|
|
53
|
+
invalidate(paths: readonly string[]): void;
|
|
54
|
+
clear(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Drop IR cache and delete the retained Parser.
|
|
57
|
+
* Loaded Language modules are dropped from JS maps; web-tree-sitter 0.26 has no Language.delete().
|
|
58
|
+
*/
|
|
59
|
+
shutdown(): void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ExploreEngineOptions = {
|
|
63
|
+
cwd: string;
|
|
64
|
+
registry?: AdapterRegistry;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type CacheEntry = {
|
|
68
|
+
contentHash: string;
|
|
69
|
+
ir: FileIr;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function contentHashOf(bytes: Uint8Array): string {
|
|
73
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function lineCountOf(source: string): number {
|
|
77
|
+
if (source.length === 0) return 0;
|
|
78
|
+
let count = 1;
|
|
79
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
80
|
+
const code = source.charCodeAt(i);
|
|
81
|
+
if (code === 10) count += 1;
|
|
82
|
+
else if (code === 13) {
|
|
83
|
+
count += 1;
|
|
84
|
+
if (source.charCodeAt(i + 1) === 10) i += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return count;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pinById(id: string): GrammarPin {
|
|
91
|
+
const pin = loadGrammarManifest().grammars.find((entry) => entry.id === id);
|
|
92
|
+
if (pin === undefined) {
|
|
93
|
+
throw new Error(`No grammar artifact for language: ${id}`);
|
|
94
|
+
}
|
|
95
|
+
return pin;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** ast-grep metavars: `$NAME` one node, `$$$NAME` sibling sequence. Skip `$_`. */
|
|
99
|
+
function metaVarNames(pattern: string): { singles: string[]; multis: string[] } {
|
|
100
|
+
const singles: string[] = [];
|
|
101
|
+
const multis: string[] = [];
|
|
102
|
+
let i = 0;
|
|
103
|
+
while (i < pattern.length) {
|
|
104
|
+
if (pattern[i] !== "$") {
|
|
105
|
+
i += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let dollars = 0;
|
|
109
|
+
while (i < pattern.length && pattern[i] === "$") {
|
|
110
|
+
dollars += 1;
|
|
111
|
+
i += 1;
|
|
112
|
+
}
|
|
113
|
+
let name = "";
|
|
114
|
+
while (i < pattern.length) {
|
|
115
|
+
const c = pattern[i];
|
|
116
|
+
if (c === undefined) break;
|
|
117
|
+
const isAlpha = (c >= "A" && c <= "Z") || c === "_";
|
|
118
|
+
const isAlnum = isAlpha || (c >= "0" && c <= "9");
|
|
119
|
+
if ((name.length === 0 && isAlpha) || (name.length > 0 && isAlnum)) {
|
|
120
|
+
name += c;
|
|
121
|
+
i += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (name.length === 0 || name === "_") continue;
|
|
127
|
+
if (dollars >= 3) {
|
|
128
|
+
if (!multis.includes(name)) multis.push(name);
|
|
129
|
+
} else if (dollars === 1) {
|
|
130
|
+
if (!singles.includes(name)) singles.push(name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { singles, multis };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hitKey(hit: AstSearchHit): string {
|
|
137
|
+
return `${hit.startLine}:${hit.endLine}:${hit.text}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @ast-grep/wasm Pos.index is Unicode scalar offset, not UTF-16. */
|
|
141
|
+
function sliceByCharOffset(source: string, startChar: number, endChar: number): string {
|
|
142
|
+
let start = 0;
|
|
143
|
+
let end = source.length;
|
|
144
|
+
let charIndex = 0;
|
|
145
|
+
for (let i = 0; i < source.length;) {
|
|
146
|
+
if (charIndex === startChar) start = i;
|
|
147
|
+
const code = source.charCodeAt(i);
|
|
148
|
+
const step = code >= 0xd800 && code <= 0xdbff ? 2 : 1;
|
|
149
|
+
i += step;
|
|
150
|
+
charIndex += 1;
|
|
151
|
+
if (charIndex === endChar) {
|
|
152
|
+
end = i;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (charIndex < endChar) end = source.length;
|
|
157
|
+
return source.slice(start, end);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function bindingsFromNode(node: SgNode, pattern: string, source: string): AstSearchBinding[] {
|
|
161
|
+
const { singles, multis } = metaVarNames(pattern);
|
|
162
|
+
const out: AstSearchBinding[] = [];
|
|
163
|
+
for (const name of singles) {
|
|
164
|
+
const match = node.getMatch(name);
|
|
165
|
+
if (match !== undefined) out.push({ name, text: match.text() });
|
|
166
|
+
}
|
|
167
|
+
for (const name of multis) {
|
|
168
|
+
const parts = node.getMultipleMatches(name);
|
|
169
|
+
if (parts.length === 0) continue;
|
|
170
|
+
const first = parts[0];
|
|
171
|
+
const last = parts[parts.length - 1];
|
|
172
|
+
if (first === undefined || last === undefined) continue;
|
|
173
|
+
const start = first.range().start.index;
|
|
174
|
+
const end = last.range().end.index;
|
|
175
|
+
out.push({ name, text: sliceByCharOffset(source, start, end) });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function nodeToHit(node: SgNode, pattern: string, source: string): AstSearchHit {
|
|
181
|
+
const range = node.range();
|
|
182
|
+
return {
|
|
183
|
+
startLine: range.start.line + 1,
|
|
184
|
+
endLine: range.end.line + 1,
|
|
185
|
+
text: node.text(),
|
|
186
|
+
bindings: bindingsFromNode(node, pattern, source),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectHits(
|
|
191
|
+
root: SgRoot,
|
|
192
|
+
matcher: unknown,
|
|
193
|
+
pattern: string,
|
|
194
|
+
source: string,
|
|
195
|
+
into: Map<string, AstSearchHit>,
|
|
196
|
+
): void {
|
|
197
|
+
const nodes = root.root().findAll(matcher);
|
|
198
|
+
for (const node of nodes) {
|
|
199
|
+
const hit = nodeToHit(node, pattern, source);
|
|
200
|
+
const key = hitKey(hit);
|
|
201
|
+
if (!into.has(key)) into.set(key, hit);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* In-process web-tree-sitter host.
|
|
207
|
+
* Parse → extract plain FileIr → tree.delete() immediately. Cache IR only.
|
|
208
|
+
* All IR offsets are UTF-16 code units into the decoded source string.
|
|
209
|
+
* Only this module resolves wasm paths via the grammar manifest.
|
|
210
|
+
* ast_search uses @ast-grep/wasm on the same pinned grammar paths (decision 11-A).
|
|
211
|
+
*/
|
|
212
|
+
export function createExploreEngine(options: ExploreEngineOptions): ExploreEngine {
|
|
213
|
+
const cwd = resolve(options.cwd);
|
|
214
|
+
const registry = options.registry ?? createDefaultRegistry();
|
|
215
|
+
const irCache = new Map<string, CacheEntry>();
|
|
216
|
+
const languages = new Map<string, Language>();
|
|
217
|
+
const languageLoads = new Map<string, Promise<Language>>();
|
|
218
|
+
const searchLangReady = new Set<string>();
|
|
219
|
+
|
|
220
|
+
let initPromise: Promise<void> | undefined;
|
|
221
|
+
let searchInitPromise: Promise<void> | undefined;
|
|
222
|
+
let parser: Parser | undefined;
|
|
223
|
+
let shutDown = false;
|
|
224
|
+
|
|
225
|
+
const assertLive = (): void => {
|
|
226
|
+
if (shutDown) {
|
|
227
|
+
throw new Error("Explore engine has been shut down");
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const ensureReady = async (): Promise<Parser> => {
|
|
232
|
+
assertLive();
|
|
233
|
+
if (parser !== undefined) return parser;
|
|
234
|
+
if (initPromise === undefined) {
|
|
235
|
+
initPromise = Parser.init({ locateFile: () => runtimeWasmPath() }).then(() => {
|
|
236
|
+
if (shutDown) return;
|
|
237
|
+
parser = new Parser();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
await initPromise;
|
|
241
|
+
assertLive();
|
|
242
|
+
if (parser === undefined) throw new Error("Explore engine failed to initialize parser");
|
|
243
|
+
return parser;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const ensureSearchReady = async (): Promise<void> => {
|
|
247
|
+
assertLive();
|
|
248
|
+
// IR path init first so locateFile pins web-tree-sitter.wasm for the process.
|
|
249
|
+
await ensureReady();
|
|
250
|
+
if (searchInitPromise === undefined) {
|
|
251
|
+
searchInitPromise = initializeTreeSitter();
|
|
252
|
+
}
|
|
253
|
+
await searchInitPromise;
|
|
254
|
+
assertLive();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const ensureSearchLanguage = async (languageId: string): Promise<void> => {
|
|
258
|
+
assertLive();
|
|
259
|
+
if (searchLangReady.has(languageId)) return;
|
|
260
|
+
const adapter = registry.adapterForId(languageId);
|
|
261
|
+
if (adapter === undefined) {
|
|
262
|
+
throw new Error(`Unregistered language: ${languageId}`);
|
|
263
|
+
}
|
|
264
|
+
if (adapter.mode !== "grammar" || !adapter.capabilities.search) {
|
|
265
|
+
throw new Error(`Language does not support structural search: ${languageId}`);
|
|
266
|
+
}
|
|
267
|
+
await ensureSearchReady();
|
|
268
|
+
assertLive();
|
|
269
|
+
await registerDynamicLanguage({
|
|
270
|
+
[languageId]: {
|
|
271
|
+
libraryPath: grammarWasmPath(pinById(languageId)),
|
|
272
|
+
expandoChar: "µ",
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
assertLive();
|
|
276
|
+
searchLangReady.add(languageId);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const loadLanguage = async (languageId: string): Promise<Language> => {
|
|
280
|
+
assertLive();
|
|
281
|
+
const cached = languages.get(languageId);
|
|
282
|
+
if (cached !== undefined) return cached;
|
|
283
|
+
|
|
284
|
+
let pending = languageLoads.get(languageId);
|
|
285
|
+
if (pending === undefined) {
|
|
286
|
+
pending = Language.load(grammarWasmPath(pinById(languageId))).then(
|
|
287
|
+
(language) => {
|
|
288
|
+
languageLoads.delete(languageId);
|
|
289
|
+
if (!shutDown) {
|
|
290
|
+
languages.set(languageId, language);
|
|
291
|
+
}
|
|
292
|
+
return language;
|
|
293
|
+
},
|
|
294
|
+
(error: unknown) => {
|
|
295
|
+
languageLoads.delete(languageId);
|
|
296
|
+
throw error;
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
languageLoads.set(languageId, pending);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const language = await pending;
|
|
303
|
+
assertLive();
|
|
304
|
+
return language;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const buildIr = async (absolutePath: string, source: string, hash: string): Promise<FileIr> => {
|
|
308
|
+
const adapter = registry.adapterForPath(absolutePath);
|
|
309
|
+
if (adapter === undefined) {
|
|
310
|
+
throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, cwd)}`);
|
|
311
|
+
}
|
|
312
|
+
const lineCount = lineCountOf(source);
|
|
313
|
+
|
|
314
|
+
if (adapter.mode === "source") {
|
|
315
|
+
const extracted = adapter.extract(source);
|
|
316
|
+
return {
|
|
317
|
+
path: absolutePath,
|
|
318
|
+
contentHash: hash,
|
|
319
|
+
languageId: adapter.id,
|
|
320
|
+
lineCount,
|
|
321
|
+
decls: extracted.decls,
|
|
322
|
+
imports: extracted.imports,
|
|
323
|
+
parseDegraded: false,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const activeParser = await ensureReady();
|
|
328
|
+
const language = await loadLanguage(adapter.id);
|
|
329
|
+
assertLive();
|
|
330
|
+
activeParser.setLanguage(language);
|
|
331
|
+
activeParser.reset();
|
|
332
|
+
const tree = activeParser.parse(source);
|
|
333
|
+
if (tree === null) {
|
|
334
|
+
throw new Error(`Parse failed for ${formatPathForDisplay(absolutePath, cwd)}`);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const extracted = adapter.extract(tree, source);
|
|
338
|
+
return {
|
|
339
|
+
path: absolutePath,
|
|
340
|
+
contentHash: hash,
|
|
341
|
+
languageId: adapter.id,
|
|
342
|
+
lineCount,
|
|
343
|
+
decls: extracted.decls,
|
|
344
|
+
imports: extracted.imports,
|
|
345
|
+
parseDegraded: tree.rootNode.hasError,
|
|
346
|
+
};
|
|
347
|
+
} finally {
|
|
348
|
+
tree.delete();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const loadSource = async (path: string): Promise<FileSource> => {
|
|
353
|
+
assertLive();
|
|
354
|
+
const absolutePath = resolveExplorePath(cwd, path);
|
|
355
|
+
let bytes: Buffer;
|
|
356
|
+
try {
|
|
357
|
+
bytes = await readFile(absolutePath);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw pathResolutionError(error, path);
|
|
360
|
+
}
|
|
361
|
+
assertLive();
|
|
362
|
+
const hash = contentHashOf(bytes);
|
|
363
|
+
const source = bytes.toString("utf8");
|
|
364
|
+
const cached = irCache.get(absolutePath);
|
|
365
|
+
if (cached !== undefined && cached.contentHash === hash) {
|
|
366
|
+
return { ir: cached.ir, source };
|
|
367
|
+
}
|
|
368
|
+
const ir = await buildIr(absolutePath, source, hash);
|
|
369
|
+
assertLive();
|
|
370
|
+
irCache.set(absolutePath, { contentHash: hash, ir });
|
|
371
|
+
return { ir, source };
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
cwd,
|
|
376
|
+
registry,
|
|
377
|
+
|
|
378
|
+
async irForFile(path: string): Promise<FileIr> {
|
|
379
|
+
const source = await loadSource(path);
|
|
380
|
+
return source.ir;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
async sourceForFile(path: string): Promise<FileSource> {
|
|
384
|
+
return loadSource(path);
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
async searchInSource(languageId: string, source: string, pattern: string): Promise<AstSearchHit[]> {
|
|
388
|
+
assertLive();
|
|
389
|
+
const adapter = registry.adapterForId(languageId);
|
|
390
|
+
if (adapter === undefined) {
|
|
391
|
+
throw new Error(`Unregistered language: ${languageId}`);
|
|
392
|
+
}
|
|
393
|
+
if (adapter.mode !== "grammar" || !adapter.capabilities.search) {
|
|
394
|
+
throw new Error(`Language does not support structural search: ${languageId}`);
|
|
395
|
+
}
|
|
396
|
+
await ensureSearchLanguage(languageId);
|
|
397
|
+
assertLive();
|
|
398
|
+
|
|
399
|
+
let root: SgRoot;
|
|
400
|
+
try {
|
|
401
|
+
root = astGrepParse(languageId, source);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
404
|
+
throw new Error(`Parse failed for structural search (${languageId}): ${message}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const byKey = new Map<string, AstSearchHit>();
|
|
409
|
+
const matchers: unknown[] = [pattern];
|
|
410
|
+
const contexts = adapter.patternContexts ?? [];
|
|
411
|
+
for (const wrap of contexts) {
|
|
412
|
+
if (!wrap.context.includes("$PATTERN")) continue;
|
|
413
|
+
matchers.push({
|
|
414
|
+
rule: {
|
|
415
|
+
pattern: {
|
|
416
|
+
context: wrap.context.split("$PATTERN").join(pattern),
|
|
417
|
+
selector: wrap.selector,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let anyMatcherOk = false;
|
|
424
|
+
let lastError: string | undefined;
|
|
425
|
+
for (const matcher of matchers) {
|
|
426
|
+
try {
|
|
427
|
+
collectHits(root, matcher, pattern, source, byKey);
|
|
428
|
+
anyMatcherOk = true;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!anyMatcherOk) {
|
|
434
|
+
throw new Error(`Invalid pattern: ${lastError ?? "unknown pattern error"}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return [...byKey.values()].sort((a, b) => {
|
|
438
|
+
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
439
|
+
if (a.endLine !== b.endLine) return a.endLine - b.endLine;
|
|
440
|
+
return a.text.localeCompare(b.text);
|
|
441
|
+
});
|
|
442
|
+
} finally {
|
|
443
|
+
root.free();
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
invalidate(paths: readonly string[]): void {
|
|
448
|
+
for (const path of paths) {
|
|
449
|
+
irCache.delete(resolveExplorePath(cwd, path));
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
clear(): void {
|
|
454
|
+
irCache.clear();
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
shutdown(): void {
|
|
458
|
+
if (shutDown) return;
|
|
459
|
+
shutDown = true;
|
|
460
|
+
irCache.clear();
|
|
461
|
+
languages.clear();
|
|
462
|
+
languageLoads.clear();
|
|
463
|
+
searchLangReady.clear();
|
|
464
|
+
parser?.delete();
|
|
465
|
+
parser = undefined;
|
|
466
|
+
initPromise = undefined;
|
|
467
|
+
searchInitPromise = undefined;
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|