@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.
Files changed (91) hide show
  1. package/dist/cli/index.js +290 -353
  2. package/dist/cli/invocation.d.ts +22 -13
  3. package/dist/cli/invocation.js +49 -29
  4. package/dist/cli/verify.d.ts +28 -0
  5. package/dist/cli/verify.js +265 -0
  6. package/dist/constitution/delta.js +2 -2
  7. package/dist/constitution/experiment.d.ts +1 -1
  8. package/dist/constitution/sourceMutation.js +28 -18
  9. package/dist/core/agenthook.js +26 -6
  10. package/dist/core/boundedSpawn.d.ts +57 -0
  11. package/dist/core/boundedSpawn.js +321 -0
  12. package/dist/core/commitrepair.d.ts +43 -138
  13. package/dist/core/commitrepair.js +76 -167
  14. package/dist/core/config.d.ts +6 -0
  15. package/dist/core/config.js +1 -0
  16. package/dist/core/correction.js +9 -9
  17. package/dist/core/delivery.d.ts +6 -1
  18. package/dist/core/delivery.js +71 -5
  19. package/dist/core/docanchors.js +9 -9
  20. package/dist/core/drift.js +22 -27
  21. package/dist/core/escalations.d.ts +16 -58
  22. package/dist/core/escalations.js +39 -121
  23. package/dist/core/glob.d.ts +7 -7
  24. package/dist/core/glob.js +12 -11
  25. package/dist/core/groundingLag.d.ts +2 -9
  26. package/dist/core/groundingLag.js +5 -11
  27. package/dist/core/groundingMerge.d.ts +2 -7
  28. package/dist/core/groundingMerge.js +35 -54
  29. package/dist/core/migrate.d.ts +10 -1
  30. package/dist/core/migrate.js +17 -6
  31. package/dist/core/paths.d.ts +17 -24
  32. package/dist/core/paths.js +37 -43
  33. package/dist/core/projectDna.js +11 -1
  34. package/dist/core/refrepair.d.ts +2 -1
  35. package/dist/core/refrepair.js +4 -4
  36. package/dist/core/relativeImports.js +6 -2
  37. package/dist/core/repairqueue.d.ts +17 -34
  38. package/dist/core/repairqueue.js +48 -98
  39. package/dist/core/taskSelection.js +48 -6
  40. package/dist/core/testSelection.d.ts +122 -0
  41. package/dist/core/testSelection.js +588 -0
  42. package/dist/core/types.d.ts +3 -3
  43. package/dist/core/types.js +1 -1
  44. package/dist/extractors/diff.d.ts +4 -4
  45. package/dist/extractors/diff.js +25 -29
  46. package/dist/extractors/git.d.ts +8 -23
  47. package/dist/extractors/git.js +77 -95
  48. package/dist/extractors/helm.d.ts +10 -0
  49. package/dist/extractors/helm.js +43 -59
  50. package/dist/extractors/indexer.d.ts +6 -0
  51. package/dist/extractors/indexer.js +168 -184
  52. package/dist/extractors/k8sManifest.d.ts +23 -80
  53. package/dist/extractors/k8sManifest.js +319 -690
  54. package/dist/extractors/languages.d.ts +37 -37
  55. package/dist/extractors/languages.js +83 -54
  56. package/dist/extractors/nativeTreeSitter.d.ts +4 -4
  57. package/dist/extractors/nativeTreeSitter.js +17 -14
  58. package/dist/extractors/parse.d.ts +27 -13
  59. package/dist/extractors/parse.js +120 -56
  60. package/dist/extractors/workspaces.js +3 -1
  61. package/dist/integrations/claudemd.js +1 -0
  62. package/dist/integrations/gitignore.js +3 -5
  63. package/dist/integrations/health.d.ts +6 -0
  64. package/dist/integrations/health.js +33 -12
  65. package/dist/integrations/hooks.d.ts +13 -16
  66. package/dist/integrations/hooks.js +31 -30
  67. package/dist/integrations/mergeDriver.js +18 -17
  68. package/dist/integrations/optout.d.ts +3 -0
  69. package/dist/integrations/optout.js +17 -0
  70. package/dist/integrations/providers.d.ts +6 -2
  71. package/dist/integrations/providers.js +19 -2
  72. package/dist/integrations/team.js +4 -2
  73. package/dist/integrations/uninstall.d.ts +10 -4
  74. package/dist/integrations/uninstall.js +22 -18
  75. package/dist/mcp/server.d.ts +4 -5
  76. package/dist/mcp/server.js +107 -81
  77. package/dist/serve/app.js +9 -1
  78. package/dist/serve/config.js +5 -0
  79. package/dist/serve/writelock.d.ts +3 -1
  80. package/dist/serve/writelock.js +17 -3
  81. package/dist/store/hunchStore.d.ts +2 -1
  82. package/dist/store/hunchStore.js +20 -23
  83. package/dist/store/jsonStore.d.ts +13 -3
  84. package/dist/store/jsonStore.js +37 -13
  85. package/dist/store/stateBinding.js +17 -4
  86. package/dist/synthesis/provider.d.ts +19 -21
  87. package/dist/synthesis/provider.js +93 -154
  88. package/dist/synthesis/synthesize.d.ts +8 -8
  89. package/dist/synthesis/synthesize.js +33 -50
  90. package/package.json +3 -3
  91. 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, also used as the grammar-bundle cache key by parse.ts. */
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
- /** Cache key parse.ts's bundleFor() uses (one grammar+query pair may serve
8
- * several extensions, e.g. tsx serves both .tsx and .jsx). */
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 for this spec. */
11
+ /** Lazily returns the tree-sitter Language object. */
11
12
  loadGrammar(): unknown;
12
- /** Tree-sitter query source capturing every construct this language cares about. */
13
+ /** Tree-sitter query source capturing every construct of interest. */
13
14
  query: string;
14
- /** Node types ascendToDef() walks up to when resolving a name capture's enclosing def. */
15
+ /** Node types the parser climbs to when attaching a name capture to its definition. */
15
16
  defNodeTypes: Set<string>;
16
- /** Query capture-name (ending in ".def") -> the ParsedSymbolKind it represents. */
17
+ /** Symbol kind for each capture name ending in `.def`. */
17
18
  defKindOf: Record<string, ParsedSymbolKind>;
18
- /** Query capture-name (ending in ".name") -> the matching ".def" capture-name. */
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. Member calls to these (e.g. `arr.map(...)`)
27
- * must NOT create call edges to unrelated repo symbols that happen to share the
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 calls-list entries (e.g. YAML's
34
- * alias->anchor references aren't function calls). Defaults to "calls"
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
- /** Fallback name for a ".def" capture with no paired ".name" capture (e.g.
38
- * a whole-file root symbol with no natural identifier of its own). Omit
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 the source mean "this isn't actually
59
- * {id} text yet — it's a template that renders to {id} later" (Go/Jinja/Helm
60
- * delimiters in a .yaml file, e.g.). parse.ts still runs the real parse — a
61
- * genuine anchor/symbol elsewhere in the file is still extracted, same as any
62
- * other {id} file — but no longer lets the resulting ERROR nodes fail-close
63
- * the whole-repo scan completeness gate the way toleratedErrorScopes protects
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 within this language where EVERY file is inherently a template
70
- * regardless of content — unlike templatingMarkers, this never content-sniffs.
71
- * The extension alone means "this file is a template, parseable must never
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 worth drafting a decision from even though no LanguageSpec parses
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
- /** Broader than languageFor: "is this worth reasoning about" (synthesis input)
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
- * Language registry: one LanguageSpec per supported language, consumed by
3
- * parse.ts (tree-sitter grammar/query dispatch), indexer.ts / diff.ts /
4
- * synthesize.ts ("is this a code file?"). Adding a language is a new entry
5
- * here (+ a new tree-sitter-* dependency), not edits scattered across those
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", "flatMap",
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
- "replace", "replaceAll", "trim", "trimStart", "trimEnd", "padStart", "padEnd", "startsWith", "endsWith",
38
- "toLowerCase", "toUpperCase", "charAt", "charCodeAt", "substring", "substr", "repeat", "match", "matchAll",
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", "generator_function_declaration", "method_definition",
47
- "class_declaration", "interface_declaration", "type_alias_declaration", "variable_declarator",
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
- /** .tsx/.jsx use the TSX grammar variant; everything else in the TS spec uses
76
- * the plain typescript grammar. Both share the same query/def maps/builtins,
77
- * so this is a second LanguageSpec entry with a distinct grammarKey/loadGrammar
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
- name: (identifier) @class.name
101
- body: (block
102
- [
103
- (function_definition name: (identifier) @method.name) @method.def
104
- (decorated_definition definition: (function_definition name: (identifier) @method.name) @method.def)
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", "clear",
123
- "append", "extend", "insert", "remove", "reverse", "sort", "count", "index",
124
- "add", "discard", "union", "intersection", "difference",
125
- "format", "join", "split", "rsplit", "splitlines", "strip", "lstrip", "rstrip",
126
- "startswith", "endswith", "replace", "find", "rfind", "lower", "upper", "title", "capitalize",
127
- "encode", "decode", "isdigit", "isalpha", "isalnum", "isspace",
128
- "read", "write", "close", "open", "readline", "readlines",
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 reference an anchor from OUTSIDE that anchor's own byte range
276
- // (a sibling key, not a nested value) — without a whole-file fallback symbol,
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 charts / templated CI configs are common .yaml content that only
280
- // becomes valid YAML after a render step (issue #33). The negative lookbehind
281
- // on "{{" excludes GitHub Actions' `${{ expression }}` syntax, which is
282
- // ordinary, always-valid YAML — every workflow file in this repo uses it, so
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
- for (const lang of LANGUAGES) {
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 worth drafting a decision from even though no LanguageSpec parses
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
- /** Broader than languageFor: "is this worth reasoning about" (synthesis input)
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
- /** Load all native tree-sitter addons (the parser runtime + every grammar) from
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 lifetime; redirecting the upstream loaders means npm can replace the
39
- * installed package during an active MCP session without killing that session
40
- * or falling back to a stale binary. */
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
- "@tree-sitter-grammars/tree-sitter-yaml",
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
- return `${packageName.toUpperCase().replaceAll("-", "_")}_PREBUILD`;
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
- /** Load all native tree-sitter addons (the parser runtime + every grammar) from
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 lifetime; redirecting the upstream loaders means npm can replace the
204
- * installed package during an active MCP session without killing that session
205
- * or falling back to a stale binary. */
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
- // Three binding spellings: prebuilds ship as tree-sitter[-typescript|-python].node
223
- // or, for a scoped package like @tree-sitter-grammars/tree-sitter-yaml, as
224
- // @scope+name.node; from-source builds are named after the binding.gyp target
225
- // with underscores (tree_sitter_runtime_binding.node, tree_sitter_python_binding.node,
226
- // tree_sitter_yaml_binding.node, …). Missing the underscore names let an
227
- // already-loaded source-built addon slip past this guard and defeat the
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("@tree-sitter-grammars/tree-sitter-yaml");
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 a stored symbol's bodyText — large enough for review context, small
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
- * Keyed by the symbol's position (index) in `parsed.symbols` — NOT its
42
- * startByte, which is not a reliable per-symbol identity: a language whose
43
- * extractor merges a synthetic whole-file symbol with independently-derived
44
- * symbols (e.g. YAML's fallback-root synthetic symbol alongside Helm's
45
- * regex-derived `define` blocks) can produce two distinct symbols that both
46
- * start at byte 0. Indexing by array position is unique by construction,
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. */