@davesheffer/hunch 1.45.2 → 1.47.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/dist/cli/index.js +290 -353
- package/dist/cli/invocation.d.ts +22 -13
- package/dist/cli/invocation.js +49 -29
- package/dist/cli/verify.d.ts +28 -0
- package/dist/cli/verify.js +265 -0
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/experiment.d.ts +1 -1
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/agenthook.js +26 -6
- package/dist/core/boundedSpawn.d.ts +57 -0
- package/dist/core/boundedSpawn.js +321 -0
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +1 -0
- package/dist/core/correction.js +9 -9
- package/dist/core/delivery.d.ts +6 -1
- package/dist/core/delivery.js +71 -5
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/migrate.d.ts +10 -1
- package/dist/core/migrate.js +17 -6
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/projectDna.js +11 -1
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/relativeImports.js +6 -2
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/taskSelection.js +48 -6
- package/dist/core/testSelection.d.ts +122 -0
- package/dist/core/testSelection.js +588 -0
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +8 -23
- package/dist/extractors/git.js +77 -95
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +168 -184
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +37 -37
- package/dist/extractors/languages.js +83 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +27 -13
- package/dist/extractors/parse.js +120 -56
- package/dist/extractors/workspaces.js +3 -1
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/health.d.ts +6 -0
- package/dist/integrations/health.js +33 -12
- package/dist/integrations/hooks.d.ts +13 -16
- package/dist/integrations/hooks.js +31 -30
- package/dist/integrations/mergeDriver.js +18 -17
- package/dist/integrations/optout.d.ts +3 -0
- package/dist/integrations/optout.js +17 -0
- package/dist/integrations/providers.d.ts +6 -2
- package/dist/integrations/providers.js +19 -2
- package/dist/integrations/team.js +4 -2
- package/dist/integrations/uninstall.d.ts +10 -4
- package/dist/integrations/uninstall.js +22 -18
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +107 -81
- package/dist/serve/app.js +9 -1
- package/dist/serve/config.js +5 -0
- package/dist/serve/writelock.d.ts +3 -1
- package/dist/serve/writelock.js +17 -3
- package/dist/store/hunchStore.d.ts +2 -1
- package/dist/store/hunchStore.js +20 -23
- package/dist/store/jsonStore.d.ts +13 -3
- package/dist/store/jsonStore.js +37 -13
- package/dist/store/stateBinding.js +17 -4
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +93 -154
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +33 -50
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -1,43 +1,49 @@
|
|
|
1
1
|
import type { Edge } from "../core/types.js";
|
|
2
2
|
export type ParsedSymbolKind = "function" | "method" | "class" | "interface" | "trait" | "enum" | "type" | "variable" | "file";
|
|
3
3
|
export interface LanguageSpec {
|
|
4
|
-
/** Stable id
|
|
4
|
+
/** Stable language id (`typescript`, `python`, `go`, `php`, `yaml`). */
|
|
5
5
|
id: string;
|
|
6
|
+
/** File suffixes, leading dot included, that this spec handles. */
|
|
6
7
|
extensions: string[];
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
8
|
+
/** Memoization key for the parser+query bundle. One grammar can serve several
|
|
9
|
+
* extensions, and two specs may share an `id` yet differ here (TS vs TSX). */
|
|
9
10
|
grammarKey: string;
|
|
10
|
-
/** Lazily returns the tree-sitter Language object
|
|
11
|
+
/** Lazily returns the tree-sitter Language object. */
|
|
11
12
|
loadGrammar(): unknown;
|
|
12
|
-
/** Tree-sitter query source capturing every construct
|
|
13
|
+
/** Tree-sitter query source capturing every construct of interest. */
|
|
13
14
|
query: string;
|
|
14
|
-
/** Node types
|
|
15
|
+
/** Node types the parser climbs to when attaching a name capture to its definition. */
|
|
15
16
|
defNodeTypes: Set<string>;
|
|
16
|
-
/**
|
|
17
|
+
/** Symbol kind for each capture name ending in `.def`. */
|
|
17
18
|
defKindOf: Record<string, ParsedSymbolKind>;
|
|
18
|
-
/**
|
|
19
|
+
/** Maps each `.name` capture to the `.def` capture it belongs to. */
|
|
19
20
|
nameToDef: Record<string, string>;
|
|
21
|
+
/** Optional query binding a local name to an imported module, run with
|
|
22
|
+
* matches() so captures pair up: @bind.src (module specifier), @bind.local
|
|
23
|
+
* (local name), @bind.name (an imported member, `from X import name`). Feeds
|
|
24
|
+
* qualified calls (`ns.fn()`), resolved only inside the bound module's files. */
|
|
25
|
+
bindingQuery?: string;
|
|
26
|
+
/** Local name an import binds when @bind.local is absent (Go `"a/geo"` -> geo). */
|
|
27
|
+
defaultBindingLocal?(src: string): string | null;
|
|
28
|
+
/** Module path of `from <src> import <name>` when name is itself a submodule. */
|
|
29
|
+
joinSubmodule?(src: string, name: string): string;
|
|
20
30
|
/** Optional semantic relationship captures. The generic parser/indexer emit
|
|
21
31
|
* these through the normal graph path; languages only describe syntax. */
|
|
22
32
|
relationKindOf?: Record<string, {
|
|
23
33
|
edgeType: Edge["type"];
|
|
24
34
|
label: string;
|
|
25
35
|
}>;
|
|
26
|
-
/** Common builtin/stdlib method names.
|
|
27
|
-
*
|
|
28
|
-
* name (DESIGN: keep the graph clean). */
|
|
36
|
+
/** Common builtin/stdlib method names. A member call whose property is in this
|
|
37
|
+
* set never produces a call edge to a same-named repo symbol. */
|
|
29
38
|
builtinMethods: Set<string>;
|
|
30
39
|
/** Direct-call builtins for languages such as PHP. These never become repo
|
|
31
40
|
* call edges merely because a project symbol shares the runtime name. */
|
|
32
41
|
builtinFunctions?: Set<string>;
|
|
33
|
-
/** Edge type emitted for this language's
|
|
34
|
-
* alias
|
|
35
|
-
* when omitted — every language before YAML. */
|
|
42
|
+
/** Edge type emitted for this language's call-list entries; defaults to
|
|
43
|
+
* `"calls"`. YAML sets it because alias-to-anchor links are references. */
|
|
36
44
|
referenceEdgeType?: Edge["type"];
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
* for a language where every ".def" always has a name capture — parse.ts
|
|
40
|
-
* then keeps dropping unnamed defs, the current behavior for TS/Python. */
|
|
45
|
+
/** Name for a `.def` capture that has no paired `.name` capture (a whole-file
|
|
46
|
+
* root symbol, say). When omitted, unnamed defs are dropped. */
|
|
41
47
|
fallbackDefName?: (file: string) => string;
|
|
42
48
|
/** Ancestor shapes in which an ERROR node is a known limitation of THIS grammar
|
|
43
49
|
* rather than a real syntax error. parse.ts forgives an error only when some
|
|
@@ -55,30 +61,24 @@ export interface LanguageSpec {
|
|
|
55
61
|
* every captured value from the original source at the tree's unchanged
|
|
56
62
|
* offsets. Keep replacements syntax-equivalent outside the grammar defect. */
|
|
57
63
|
parseErrorRecovery?: (source: string) => string;
|
|
58
|
-
/** Patterns whose presence anywhere in
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* narrower, per-grammar false positives (issue #33). Keep these narrow: a
|
|
65
|
-
* pattern that also matches ordinary, always-valid {id} syntax (e.g. GitHub
|
|
66
|
-
* Actions' `${{ }}` expressions) would silently disable the fail-closed
|
|
67
|
-
* guarantee for files that were never templated at all. */
|
|
64
|
+
/** Patterns whose presence anywhere in a file's text means the file is a
|
|
65
|
+
* template that renders into this language later (Go/Jinja/Helm delimiters in
|
|
66
|
+
* YAML). The real parse still runs, but the resulting error nodes must not make
|
|
67
|
+
* the file unparseable, so the whole-repo completeness gate does not fail
|
|
68
|
+
* closed. Keep patterns narrow so they never match always-valid syntax such as
|
|
69
|
+
* GitHub Actions `${{ }}` expressions. */
|
|
68
70
|
templatingMarkers?: readonly RegExp[];
|
|
69
|
-
/** Extensions
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* become false for it," matching Helm's `_helpers.tpl` convention where the
|
|
73
|
-
* file may be pure Go-template text with no YAML structure of its own at all. */
|
|
71
|
+
/** Extensions where every file is a template regardless of content, with no
|
|
72
|
+
* sniffing (Helm's `_helpers.tpl` convention). Such files are never reported
|
|
73
|
+
* unparseable. */
|
|
74
74
|
alwaysTemplatedExtensions?: readonly string[];
|
|
75
75
|
}
|
|
76
76
|
export declare const LANGUAGES: LanguageSpec[];
|
|
77
|
+
/** Every registered extension, de-duplicated, in registry order. */
|
|
77
78
|
export declare const CODE_EXTENSIONS: string[];
|
|
79
|
+
/** First spec (registry order) with an extension the file name ends with. */
|
|
78
80
|
export declare function languageFor(file: string): LanguageSpec | null;
|
|
79
|
-
/** Prose formats
|
|
80
|
-
* them — no grammar, no symbols/edges, just eligible input to synthesis (issue #12). */
|
|
81
|
+
/** Prose formats eligible as synthesis input but never parsed. */
|
|
81
82
|
export declare const PROSE_EXTENSIONS: string[];
|
|
82
|
-
/**
|
|
83
|
-
* rather than "can tree-sitter parse this" (symbol/dependency extraction). */
|
|
83
|
+
/** True for parseable code or prose. Deliberately broader than parseability. */
|
|
84
84
|
export declare function isSubstantive(file: string): boolean;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* here
|
|
6
|
-
* four files.
|
|
2
|
+
* The single language registry: one `LanguageSpec` per supported language. The
|
|
3
|
+
* parser reads grammar and query data from here, and the indexer, diff analysis
|
|
4
|
+
* and synthesis ask it "is this a code file?". Adding a language means adding an
|
|
5
|
+
* entry here plus a grammar dependency.
|
|
7
6
|
*/
|
|
8
7
|
import { basename } from "node:path";
|
|
9
8
|
import { loadNativeTreeSitter } from "./nativeTreeSitter.js";
|
|
@@ -17,7 +16,14 @@ const TS_QUERY = `
|
|
|
17
16
|
(variable_declarator
|
|
18
17
|
name: (identifier) @arrow.name
|
|
19
18
|
value: [(arrow_function) (function_expression)]) @arrow.def
|
|
19
|
+
;; Class-field arrows (\`handler = () => {}\`) are methods in every way that matters.
|
|
20
|
+
(public_field_definition
|
|
21
|
+
name: (property_identifier) @method.name
|
|
22
|
+
value: [(arrow_function) (function_expression)]) @method.def
|
|
20
23
|
(import_statement source: (string) @import.src)
|
|
24
|
+
;; require("./x") and dynamic import("./x") are imports too, when the specifier is literal.
|
|
25
|
+
(call_expression function: (identifier) @_require arguments: (arguments . (string) @import.src .) (#eq? @_require "require"))
|
|
26
|
+
(call_expression function: (import) arguments: (arguments . (string) @import.src .))
|
|
21
27
|
(call_expression function: (identifier) @call.id)
|
|
22
28
|
(call_expression function: (member_expression property: (property_identifier) @call.member))
|
|
23
29
|
;; Construction IS a call. Without these, \`new Foo()\` produced no edge at all, so
|
|
@@ -27,24 +33,56 @@ const TS_QUERY = `
|
|
|
27
33
|
(new_expression constructor: (identifier) @call.id)
|
|
28
34
|
(new_expression constructor: (member_expression property: (property_identifier) @call.member))
|
|
29
35
|
`;
|
|
36
|
+
/** Rendering a component IS a call; lowercase tags are intrinsic elements, not symbols. */
|
|
37
|
+
const TSX_QUERY = `${TS_QUERY}
|
|
38
|
+
(jsx_opening_element name: (identifier) @call.id (#match? @call.id "^[A-Z]"))
|
|
39
|
+
(jsx_self_closing_element name: (identifier) @call.id (#match? @call.id "^[A-Z]"))
|
|
40
|
+
(jsx_opening_element name: (member_expression property: (property_identifier) @call.member))
|
|
41
|
+
(jsx_self_closing_element name: (member_expression property: (property_identifier) @call.member))
|
|
42
|
+
`;
|
|
43
|
+
/** Module-object bindings: \`import * as ns\`, \`const m = require()\`, \`const m = await import()\`. */
|
|
44
|
+
const TS_BINDING_QUERY = `
|
|
45
|
+
(import_statement (import_clause (namespace_import (identifier) @bind.local)) source: (string) @bind.src)
|
|
46
|
+
(variable_declarator
|
|
47
|
+
name: (identifier) @bind.local
|
|
48
|
+
value: (call_expression function: (identifier) @_require arguments: (arguments . (string) @bind.src .))
|
|
49
|
+
(#eq? @_require "require"))
|
|
50
|
+
(variable_declarator
|
|
51
|
+
name: (identifier) @bind.local
|
|
52
|
+
value: (await_expression (call_expression function: (import) arguments: (arguments . (string) @bind.src .))))
|
|
53
|
+
`;
|
|
30
54
|
const TS_BUILTIN_METHODS = new Set([
|
|
55
|
+
// arrays
|
|
31
56
|
"map", "filter", "forEach", "reduce", "find", "findIndex", "some", "every", "includes",
|
|
32
|
-
"push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "split", "flat",
|
|
33
|
-
"indexOf", "lastIndexOf", "keys", "values", "entries", "sort", "reverse", "fill", "at",
|
|
57
|
+
"push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "split", "flat",
|
|
58
|
+
"flatMap", "indexOf", "lastIndexOf", "keys", "values", "entries", "sort", "reverse", "fill", "at",
|
|
59
|
+
// Map / Set
|
|
34
60
|
"get", "set", "has", "add", "delete", "clear",
|
|
61
|
+
// Promise
|
|
35
62
|
"then", "catch", "finally", "all", "race", "resolve", "reject",
|
|
63
|
+
// object basics
|
|
36
64
|
"toString", "valueOf", "toJSON", "hasOwnProperty",
|
|
37
|
-
|
|
38
|
-
"
|
|
65
|
+
// strings
|
|
66
|
+
"replace", "replaceAll", "trim", "trimStart", "trimEnd", "padStart", "padEnd", "startsWith",
|
|
67
|
+
"endsWith", "toLowerCase", "toUpperCase", "charAt", "charCodeAt", "substring", "substr",
|
|
68
|
+
"repeat", "match", "matchAll",
|
|
69
|
+
// Function / RegExp / JSON / Date / Array / Object statics
|
|
39
70
|
"call", "apply", "bind", "test", "exec", "now", "parse", "stringify", "from", "of", "isArray", "assign",
|
|
71
|
+
// console
|
|
40
72
|
"log", "error", "warn", "info", "debug",
|
|
41
73
|
]);
|
|
42
74
|
const TS_SHARED = {
|
|
43
75
|
id: "typescript",
|
|
44
76
|
query: TS_QUERY,
|
|
45
77
|
defNodeTypes: new Set([
|
|
46
|
-
"function_declaration",
|
|
47
|
-
"
|
|
78
|
+
"function_declaration",
|
|
79
|
+
"generator_function_declaration",
|
|
80
|
+
"method_definition",
|
|
81
|
+
"class_declaration",
|
|
82
|
+
"interface_declaration",
|
|
83
|
+
"type_alias_declaration",
|
|
84
|
+
"variable_declarator",
|
|
85
|
+
"public_field_definition",
|
|
48
86
|
]),
|
|
49
87
|
defKindOf: {
|
|
50
88
|
"fn.def": "function", "method.def": "method", "class.def": "class",
|
|
@@ -55,6 +93,7 @@ const TS_SHARED = {
|
|
|
55
93
|
"iface.name": "iface.def", "type.name": "type.def", "arrow.name": "arrow.def",
|
|
56
94
|
},
|
|
57
95
|
builtinMethods: TS_BUILTIN_METHODS,
|
|
96
|
+
bindingQuery: TS_BINDING_QUERY,
|
|
58
97
|
// ES2018 relaxed template literals: an INVALID escape (`\x` with no hex, `\u`
|
|
59
98
|
// short, `\u{` unterminated) is legal inside a TAGGED template — the cooked
|
|
60
99
|
// value is undefined and the raw text survives, which is the entire point of
|
|
@@ -72,15 +111,15 @@ const TYPESCRIPT = {
|
|
|
72
111
|
grammarKey: "ts",
|
|
73
112
|
loadGrammar: () => loadNativeTreeSitter().typescript,
|
|
74
113
|
};
|
|
75
|
-
/**
|
|
76
|
-
* the
|
|
77
|
-
*
|
|
78
|
-
* only — not a second `id` (languageFor callers only care about extension match). */
|
|
114
|
+
/** TSX is a second entry that differs from TYPESCRIPT only in grammar and cache
|
|
115
|
+
* key (the `id` is shared). It also overrides the query with TSX_QUERY, widens
|
|
116
|
+
* the tolerated error scopes and adds a recovery pass. */
|
|
79
117
|
const TSX = {
|
|
80
118
|
...TS_SHARED,
|
|
81
119
|
extensions: [".tsx", ".jsx"],
|
|
82
120
|
grammarKey: "tsx",
|
|
83
121
|
loadGrammar: () => loadNativeTreeSitter().tsx,
|
|
122
|
+
query: TSX_QUERY,
|
|
84
123
|
// tree-sitter-javascript rejects a bare ampersand in JSX text even though JSX
|
|
85
124
|
// accepts it (tree-sitter-javascript#366). The ERROR is a direct jsx_element
|
|
86
125
|
// child and its text begins at the ampersand. Require both facts plus text that
|
|
@@ -96,13 +135,15 @@ const TSX = {
|
|
|
96
135
|
parseErrorRecovery: (source) => source.replaceAll("&", "|"),
|
|
97
136
|
};
|
|
98
137
|
const PY_QUERY = `
|
|
138
|
+
;; A class whose body directly holds a def (plain or decorated): the nested def is
|
|
139
|
+
;; a method. parse.ts keeps the first classification per node, so the generic
|
|
140
|
+
;; function pattern below does not reclassify it.
|
|
99
141
|
(class_definition
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
])) @class.def
|
|
142
|
+
name: (identifier) @class.name
|
|
143
|
+
body: (block [
|
|
144
|
+
(function_definition name: (identifier) @method.name) @method.def
|
|
145
|
+
(decorated_definition definition: (function_definition name: (identifier) @method.name) @method.def)
|
|
146
|
+
])) @class.def
|
|
106
147
|
;; Every class, including one with no directly-nested def: dataclasses, Exception
|
|
107
148
|
;; subclasses, Enums, TypedDicts and pydantic models are method-less by design and
|
|
108
149
|
;; were invisible to the entire graph (no symbol, no component, no edges), so
|
|
@@ -113,20 +154,18 @@ const PY_QUERY = `
|
|
|
113
154
|
(function_definition name: (identifier) @fn.name) @fn.def
|
|
114
155
|
(import_statement name: (dotted_name) @import.src)
|
|
115
156
|
(import_statement name: (aliased_import name: (dotted_name) @import.src))
|
|
116
|
-
(import_from_statement module_name: (dotted_name) @import.src)
|
|
117
|
-
(import_from_statement module_name: (relative_import) @import.src)
|
|
157
|
+
(import_from_statement module_name: [(dotted_name) (relative_import)] @import.src)
|
|
118
158
|
(call function: (identifier) @call.id)
|
|
119
159
|
(call function: (attribute attribute: (identifier) @call.member))
|
|
120
160
|
`;
|
|
121
161
|
const PY_BUILTIN_METHODS = new Set([
|
|
122
|
-
"get", "set", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "copy",
|
|
123
|
-
"append", "extend", "insert", "remove", "reverse", "sort", "count", "index",
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"run", "wait", "poll", "communicate",
|
|
162
|
+
"get", "set", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "copy",
|
|
163
|
+
"clear", "append", "extend", "insert", "remove", "reverse", "sort", "count", "index", "add",
|
|
164
|
+
"discard", "union", "intersection", "difference", "format", "join", "split", "rsplit",
|
|
165
|
+
"splitlines", "strip", "lstrip", "rstrip", "startswith", "endswith", "replace", "find",
|
|
166
|
+
"rfind", "lower", "upper", "title", "capitalize", "encode", "decode", "isdigit", "isalpha",
|
|
167
|
+
"isalnum", "isspace", "read", "write", "close", "open", "readline", "readlines", "run",
|
|
168
|
+
"wait", "poll", "communicate",
|
|
130
169
|
]);
|
|
131
170
|
const PYTHON = {
|
|
132
171
|
id: "python",
|
|
@@ -254,7 +293,9 @@ const PHP = {
|
|
|
254
293
|
const YAML_QUERY = `
|
|
255
294
|
(block_node (anchor (anchor_name) @anchor.name)) @anchor.def
|
|
256
295
|
(flow_node (anchor (anchor_name) @anchor.name)) @anchor.def
|
|
296
|
+
;; An alias is a reference to its anchor.
|
|
257
297
|
(alias (alias_name) @call.id)
|
|
298
|
+
;; The whole document gets a root symbol (named via fallbackDefName).
|
|
258
299
|
(stream) @doc.def
|
|
259
300
|
`;
|
|
260
301
|
const YAML = {
|
|
@@ -263,42 +304,30 @@ const YAML = {
|
|
|
263
304
|
grammarKey: "yaml",
|
|
264
305
|
loadGrammar: () => loadNativeTreeSitter().yaml,
|
|
265
306
|
query: YAML_QUERY,
|
|
266
|
-
// Both block-style (`key: &x\n ...`) and flow-style (`key: &x {...}`) anchors
|
|
267
|
-
// wrap only the sigil+name; ascendToDef climbs from anchor_name -> anchor ->
|
|
268
|
-
// this enclosing node to find the def whose byte range covers the full value.
|
|
269
307
|
defNodeTypes: new Set(["block_node", "flow_node"]),
|
|
270
308
|
defKindOf: { "anchor.def": "variable", "doc.def": "file" },
|
|
271
309
|
nameToDef: { "anchor.name": "anchor.def" },
|
|
272
310
|
builtinMethods: new Set(),
|
|
273
|
-
// Alias references aren't calls; label the edge accordingly (Task 5).
|
|
274
311
|
referenceEdgeType: "references",
|
|
275
|
-
// Most aliases
|
|
276
|
-
//
|
|
277
|
-
// attributeCalls's containment check would silently drop them. See Task 4.
|
|
312
|
+
// Most aliases sit outside their anchor's byte range, so without a root symbol
|
|
313
|
+
// call attribution would drop them. The file's own name is the root.
|
|
278
314
|
fallbackDefName: (file) => basename(file),
|
|
279
|
-
// Helm
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
// treating it as a templating marker would blanket-disable the fail-closed
|
|
284
|
-
// gate for .github/workflows/**.
|
|
285
|
-
templatingMarkers: [/(?<!\$)\{\{/, /\{%/],
|
|
315
|
+
// Go/Jinja/Helm delimiters make a file invalid YAML until rendered. A `{{` not
|
|
316
|
+
// preceded by `$` (GitHub Actions `${{ }}` is plain valid YAML) and a Jinja `{%`.
|
|
317
|
+
templatingMarkers: [/(?<!\$)\{\{/u, /\{%/u],
|
|
318
|
+
// `.tpl` files (Helm `_helpers.tpl`) are templates by convention, whatever they hold.
|
|
286
319
|
alwaysTemplatedExtensions: [".tpl"],
|
|
287
320
|
};
|
|
288
321
|
export const LANGUAGES = [TYPESCRIPT, TSX, PYTHON, GO, PHP, YAML];
|
|
322
|
+
/** Every registered extension, de-duplicated, in registry order. */
|
|
289
323
|
export const CODE_EXTENSIONS = [...new Set(LANGUAGES.flatMap((l) => l.extensions))];
|
|
324
|
+
/** First spec (registry order) with an extension the file name ends with. */
|
|
290
325
|
export function languageFor(file) {
|
|
291
|
-
|
|
292
|
-
if (lang.extensions.some((ext) => file.endsWith(ext)))
|
|
293
|
-
return lang;
|
|
294
|
-
}
|
|
295
|
-
return null;
|
|
326
|
+
return LANGUAGES.find((l) => l.extensions.some((ext) => file.endsWith(ext))) ?? null;
|
|
296
327
|
}
|
|
297
|
-
/** Prose formats
|
|
298
|
-
* them — no grammar, no symbols/edges, just eligible input to synthesis (issue #12). */
|
|
328
|
+
/** Prose formats eligible as synthesis input but never parsed. */
|
|
299
329
|
export const PROSE_EXTENSIONS = [".md"];
|
|
300
|
-
/**
|
|
301
|
-
* rather than "can tree-sitter parse this" (symbol/dependency extraction). */
|
|
330
|
+
/** True for parseable code or prose. Deliberately broader than parseability. */
|
|
302
331
|
export function isSubstantive(file) {
|
|
303
332
|
return languageFor(file) !== null || PROSE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
304
333
|
}
|
|
@@ -32,10 +32,10 @@ export declare class NativeTreeSitterLoadError extends Error {
|
|
|
32
32
|
* a worker/IPC boundary keeps only its plain properties. The name check
|
|
33
33
|
* survives both. */
|
|
34
34
|
export declare function isParserLoadError(e: unknown): boolean;
|
|
35
|
-
/**
|
|
35
|
+
/** Loads every native tree-sitter addon (the parser runtime and each grammar) from
|
|
36
36
|
* content-addressed copies in a per-user temp cache — never from the installed
|
|
37
37
|
* package. Windows keeps loaded `.node` files locked for the
|
|
38
|
-
* process
|
|
39
|
-
* installed package during
|
|
40
|
-
*
|
|
38
|
+
* lifetime of the process, so pointing the upstream loaders at copies lets npm
|
|
39
|
+
* replace the installed package during a live MCP session without killing it and
|
|
40
|
+
* without ever loading a stale binary. */
|
|
41
41
|
export declare function loadNativeTreeSitter(): NativeTreeSitterRuntime;
|
|
@@ -10,13 +10,14 @@ const COPY_PREFIX = "hunch-tree-sitter-";
|
|
|
10
10
|
const CACHE_PREFIX = "hunch-tree-sitter-cache-";
|
|
11
11
|
/** A hash dir no process has used for this long is pruned. */
|
|
12
12
|
const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13
|
+
/** npm packages whose native addons are loaded from isolated copies. */
|
|
13
14
|
const NATIVE_PACKAGES = [
|
|
14
15
|
"tree-sitter",
|
|
15
16
|
"tree-sitter-typescript",
|
|
16
17
|
"tree-sitter-python",
|
|
17
18
|
"tree-sitter-go",
|
|
18
19
|
"tree-sitter-php",
|
|
19
|
-
"
|
|
20
|
+
"tree-sitter-yaml",
|
|
20
21
|
];
|
|
21
22
|
let runtime = null;
|
|
22
23
|
/** The parser runtime itself is unavailable — the addons could not be copied or
|
|
@@ -87,8 +88,11 @@ function removeStaleCopies() {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
/** node-gyp-build keys the redirect on the package's own `name`, not the
|
|
92
|
+
* specifier it was required by — the two differ for an npm alias. */
|
|
90
93
|
function environmentKey(packageName) {
|
|
91
|
-
|
|
94
|
+
const { name } = runtimeRequire(`${packageName}/package.json`);
|
|
95
|
+
return `${name.toUpperCase().replaceAll("-", "_")}_PREBUILD`;
|
|
92
96
|
}
|
|
93
97
|
/** The per-user copy cache. A binary copied to a fresh path costs a first-load
|
|
94
98
|
* assessment (macOS checks every never-seen dylib: ~2s per addon, six addons)
|
|
@@ -197,12 +201,12 @@ function pruneCache(root, keep) {
|
|
|
197
201
|
catch { /* in use or already gone */ }
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
|
-
/**
|
|
204
|
+
/** Loads every native tree-sitter addon (the parser runtime and each grammar) from
|
|
201
205
|
* content-addressed copies in a per-user temp cache — never from the installed
|
|
202
206
|
* package. Windows keeps loaded `.node` files locked for the
|
|
203
|
-
* process
|
|
204
|
-
* installed package during
|
|
205
|
-
*
|
|
207
|
+
* lifetime of the process, so pointing the upstream loaders at copies lets npm
|
|
208
|
+
* replace the installed package during a live MCP session without killing it and
|
|
209
|
+
* without ever loading a stale binary. */
|
|
206
210
|
export function loadNativeTreeSitter() {
|
|
207
211
|
if (runtime)
|
|
208
212
|
return runtime;
|
|
@@ -219,13 +223,12 @@ export function loadNativeTreeSitter() {
|
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
function loadRuntime() {
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// with underscores
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// file-lock isolation entirely (issue #52).
|
|
226
|
+
// Recognised addon file names: the prebuilt names of the runtime and of each
|
|
227
|
+
// grammar package, the `@scope+name` spelling that scoped packages use inside
|
|
228
|
+
// their prebuilds, and from-source builds, which are named after the
|
|
229
|
+
// binding.gyp target with underscores and end in `_binding`. Missing the
|
|
230
|
+
// underscore form once let an already-loaded source build slip past isolation
|
|
231
|
+
// (issue #52).
|
|
229
232
|
const preloaded = Object.keys(runtimeRequire.cache).filter((path) => /(?:tree-sitter(?:-typescript|-python|-go|-php|-yaml)?|tree_sitter(?:_[a-z]+)*_binding)\.node$/.test(path)
|
|
230
233
|
&& !new RegExp(`(?:^|[\\\\/])(?:${COPY_PREFIX}\\d+-|${CACHE_PREFIX})`).test(path));
|
|
231
234
|
if (preloaded.length) {
|
|
@@ -260,7 +263,7 @@ function loadRuntime() {
|
|
|
260
263
|
const python = runtimeRequire("tree-sitter-python");
|
|
261
264
|
const go = runtimeRequire("tree-sitter-go");
|
|
262
265
|
const php = runtimeRequire("tree-sitter-php");
|
|
263
|
-
const yaml = runtimeRequire("
|
|
266
|
+
const yaml = runtimeRequire("tree-sitter-yaml");
|
|
264
267
|
runtime = { Parser, typescript: languages.typescript, tsx: languages.tsx, python, go, php: php.php, yaml };
|
|
265
268
|
}
|
|
266
269
|
finally {
|
|
@@ -15,6 +15,20 @@ export interface ParsedCall {
|
|
|
15
15
|
endByte: number;
|
|
16
16
|
/** true for `x.foo()` (property access), false for a direct `foo()` call. */
|
|
17
17
|
member: boolean;
|
|
18
|
+
/** For a member call on a plain identifier (`x.foo()`), that identifier. */
|
|
19
|
+
qualifier?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A local name bound by an import: a module object (`imported` null — `import *
|
|
22
|
+
* as ns`, `require()`, Go package alias) or one aliased member of a module. */
|
|
23
|
+
export interface ParsedBinding {
|
|
24
|
+
local: string;
|
|
25
|
+
module: string;
|
|
26
|
+
imported: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** A call whose target is pinned to one imported module (`ns.fn()`, aliased `g()`). */
|
|
29
|
+
export interface QualifiedCall {
|
|
30
|
+
callee: string;
|
|
31
|
+
module: string;
|
|
18
32
|
}
|
|
19
33
|
export interface ParsedRelation {
|
|
20
34
|
target: string;
|
|
@@ -28,28 +42,28 @@ export interface ParsedFile {
|
|
|
28
42
|
imports: string[];
|
|
29
43
|
calls: ParsedCall[];
|
|
30
44
|
relations: ParsedRelation[];
|
|
45
|
+
bindings: ParsedBinding[];
|
|
31
46
|
namespace: string | null;
|
|
32
47
|
parseable: boolean;
|
|
33
48
|
}
|
|
34
|
-
/** Cap on
|
|
35
|
-
* enough that a huge function/file doesn't bloat every JSON symbol record. */
|
|
49
|
+
/** Cap on stored symbol body text: enough context for review without bloating JSON records. */
|
|
36
50
|
export declare const MAX_BODY_TEXT_CHARS = 4000;
|
|
37
51
|
export declare function parseSource(file: string, source: string, opts?: {
|
|
38
52
|
throwOnParseError?: boolean;
|
|
39
53
|
}): ParsedFile | null;
|
|
40
54
|
/** Map each call site to the innermost symbol whose byte-range contains it.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* regardless of byte overlap — the caller must consume the exact same
|
|
48
|
-
* `parsed.symbols` array (or an equivalently-ordered copy) to look up a
|
|
49
|
-
* symbol by the index this function returns. The value maps callee name ->
|
|
50
|
-
* `memberOnly` (true iff every occurrence was a `x.foo()` member call, never
|
|
51
|
-
* a direct `foo()`), so the indexer can resolve member calls conservatively. */
|
|
55
|
+
* The result is keyed by the symbol's INDEX in `parsed.symbols`, not its start
|
|
56
|
+
* byte: two distinct symbols can share a start byte (a synthetic whole-file root
|
|
57
|
+
* beside others starting at byte 0), so callers must look symbols up in the same
|
|
58
|
+
* array order. Each value maps callee name -> memberOnly, true iff every
|
|
59
|
+
* occurrence of that callee inside the symbol was a member call, so the indexer
|
|
60
|
+
* can resolve member calls conservatively. */
|
|
52
61
|
export declare function attributeCalls(parsed: ParsedFile): Map<number, Map<string, boolean>>;
|
|
62
|
+
/** Map each call bound to an imported module — `ns.fn()` through a module-object
|
|
63
|
+
* binding, or `g()` through an aliased member import — to the innermost symbol
|
|
64
|
+
* containing it (same index identity as attributeCalls). A local bound to two
|
|
65
|
+
* different modules is ambiguous and dropped: never guess a binding. */
|
|
66
|
+
export declare function attributeQualifiedCalls(parsed: ParsedFile): Map<number, QualifiedCall[]>;
|
|
53
67
|
/** Map static semantic relationships (extends/implements/trait use) to the
|
|
54
68
|
* innermost enclosing declaration using the same stable symbol-index identity
|
|
55
69
|
* as call attribution. */
|