@davesheffer/hunch 1.46.0 → 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 +151 -321
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/correction.js +9 -9
- 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/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- 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 +45 -64
- 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 +126 -178
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +28 -37
- package/dist/extractors/languages.js +55 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +7 -13
- package/dist/extractors/parse.js +39 -51
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +60 -73
- package/dist/store/hunchStore.js +12 -18
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +67 -76
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +32 -49
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -1,21 +1,22 @@
|
|
|
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>;
|
|
20
21
|
/** Optional query binding a local name to an imported module, run with
|
|
21
22
|
* matches() so captures pair up: @bind.src (module specifier), @bind.local
|
|
@@ -32,21 +33,17 @@ export interface LanguageSpec {
|
|
|
32
33
|
edgeType: Edge["type"];
|
|
33
34
|
label: string;
|
|
34
35
|
}>;
|
|
35
|
-
/** Common builtin/stdlib method names.
|
|
36
|
-
*
|
|
37
|
-
* 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. */
|
|
38
38
|
builtinMethods: Set<string>;
|
|
39
39
|
/** Direct-call builtins for languages such as PHP. These never become repo
|
|
40
40
|
* call edges merely because a project symbol shares the runtime name. */
|
|
41
41
|
builtinFunctions?: Set<string>;
|
|
42
|
-
/** Edge type emitted for this language's
|
|
43
|
-
* alias
|
|
44
|
-
* 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. */
|
|
45
44
|
referenceEdgeType?: Edge["type"];
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
* for a language where every ".def" always has a name capture — parse.ts
|
|
49
|
-
* 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. */
|
|
50
47
|
fallbackDefName?: (file: string) => string;
|
|
51
48
|
/** Ancestor shapes in which an ERROR node is a known limitation of THIS grammar
|
|
52
49
|
* rather than a real syntax error. parse.ts forgives an error only when some
|
|
@@ -64,30 +61,24 @@ export interface LanguageSpec {
|
|
|
64
61
|
* every captured value from the original source at the tree's unchanged
|
|
65
62
|
* offsets. Keep replacements syntax-equivalent outside the grammar defect. */
|
|
66
63
|
parseErrorRecovery?: (source: string) => string;
|
|
67
|
-
/** Patterns whose presence anywhere in
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* narrower, per-grammar false positives (issue #33). Keep these narrow: a
|
|
74
|
-
* pattern that also matches ordinary, always-valid {id} syntax (e.g. GitHub
|
|
75
|
-
* Actions' `${{ }}` expressions) would silently disable the fail-closed
|
|
76
|
-
* 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. */
|
|
77
70
|
templatingMarkers?: readonly RegExp[];
|
|
78
|
-
/** Extensions
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* become false for it," matching Helm's `_helpers.tpl` convention where the
|
|
82
|
-
* 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. */
|
|
83
74
|
alwaysTemplatedExtensions?: readonly string[];
|
|
84
75
|
}
|
|
85
76
|
export declare const LANGUAGES: LanguageSpec[];
|
|
77
|
+
/** Every registered extension, de-duplicated, in registry order. */
|
|
86
78
|
export declare const CODE_EXTENSIONS: string[];
|
|
79
|
+
/** First spec (registry order) with an extension the file name ends with. */
|
|
87
80
|
export declare function languageFor(file: string): LanguageSpec | null;
|
|
88
|
-
/** Prose formats
|
|
89
|
-
* them — no grammar, no symbols/edges, just eligible input to synthesis (issue #12). */
|
|
81
|
+
/** Prose formats eligible as synthesis input but never parsed. */
|
|
90
82
|
export declare const PROSE_EXTENSIONS: string[];
|
|
91
|
-
/**
|
|
92
|
-
* rather than "can tree-sitter parse this" (symbol/dependency extraction). */
|
|
83
|
+
/** True for parseable code or prose. Deliberately broader than parseability. */
|
|
93
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";
|
|
@@ -53,23 +52,36 @@ const TS_BINDING_QUERY = `
|
|
|
53
52
|
value: (await_expression (call_expression function: (import) arguments: (arguments . (string) @bind.src .))))
|
|
54
53
|
`;
|
|
55
54
|
const TS_BUILTIN_METHODS = new Set([
|
|
55
|
+
// arrays
|
|
56
56
|
"map", "filter", "forEach", "reduce", "find", "findIndex", "some", "every", "includes",
|
|
57
|
-
"push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "split", "flat",
|
|
58
|
-
"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
|
|
59
60
|
"get", "set", "has", "add", "delete", "clear",
|
|
61
|
+
// Promise
|
|
60
62
|
"then", "catch", "finally", "all", "race", "resolve", "reject",
|
|
63
|
+
// object basics
|
|
61
64
|
"toString", "valueOf", "toJSON", "hasOwnProperty",
|
|
62
|
-
|
|
63
|
-
"
|
|
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
|
|
64
70
|
"call", "apply", "bind", "test", "exec", "now", "parse", "stringify", "from", "of", "isArray", "assign",
|
|
71
|
+
// console
|
|
65
72
|
"log", "error", "warn", "info", "debug",
|
|
66
73
|
]);
|
|
67
74
|
const TS_SHARED = {
|
|
68
75
|
id: "typescript",
|
|
69
76
|
query: TS_QUERY,
|
|
70
77
|
defNodeTypes: new Set([
|
|
71
|
-
"function_declaration",
|
|
72
|
-
"
|
|
78
|
+
"function_declaration",
|
|
79
|
+
"generator_function_declaration",
|
|
80
|
+
"method_definition",
|
|
81
|
+
"class_declaration",
|
|
82
|
+
"interface_declaration",
|
|
83
|
+
"type_alias_declaration",
|
|
84
|
+
"variable_declarator",
|
|
73
85
|
"public_field_definition",
|
|
74
86
|
]),
|
|
75
87
|
defKindOf: {
|
|
@@ -99,10 +111,9 @@ const TYPESCRIPT = {
|
|
|
99
111
|
grammarKey: "ts",
|
|
100
112
|
loadGrammar: () => loadNativeTreeSitter().typescript,
|
|
101
113
|
};
|
|
102
|
-
/**
|
|
103
|
-
* the
|
|
104
|
-
*
|
|
105
|
-
* 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. */
|
|
106
117
|
const TSX = {
|
|
107
118
|
...TS_SHARED,
|
|
108
119
|
extensions: [".tsx", ".jsx"],
|
|
@@ -124,13 +135,15 @@ const TSX = {
|
|
|
124
135
|
parseErrorRecovery: (source) => source.replaceAll("&", "|"),
|
|
125
136
|
};
|
|
126
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.
|
|
127
141
|
(class_definition
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
])) @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
|
|
134
147
|
;; Every class, including one with no directly-nested def: dataclasses, Exception
|
|
135
148
|
;; subclasses, Enums, TypedDicts and pydantic models are method-less by design and
|
|
136
149
|
;; were invisible to the entire graph (no symbol, no component, no edges), so
|
|
@@ -141,20 +154,18 @@ const PY_QUERY = `
|
|
|
141
154
|
(function_definition name: (identifier) @fn.name) @fn.def
|
|
142
155
|
(import_statement name: (dotted_name) @import.src)
|
|
143
156
|
(import_statement name: (aliased_import name: (dotted_name) @import.src))
|
|
144
|
-
(import_from_statement module_name: (dotted_name) @import.src)
|
|
145
|
-
(import_from_statement module_name: (relative_import) @import.src)
|
|
157
|
+
(import_from_statement module_name: [(dotted_name) (relative_import)] @import.src)
|
|
146
158
|
(call function: (identifier) @call.id)
|
|
147
159
|
(call function: (attribute attribute: (identifier) @call.member))
|
|
148
160
|
`;
|
|
149
161
|
const PY_BUILTIN_METHODS = new Set([
|
|
150
|
-
"get", "set", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "copy",
|
|
151
|
-
"append", "extend", "insert", "remove", "reverse", "sort", "count", "index",
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"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",
|
|
158
169
|
]);
|
|
159
170
|
const PYTHON = {
|
|
160
171
|
id: "python",
|
|
@@ -282,7 +293,9 @@ const PHP = {
|
|
|
282
293
|
const YAML_QUERY = `
|
|
283
294
|
(block_node (anchor (anchor_name) @anchor.name)) @anchor.def
|
|
284
295
|
(flow_node (anchor (anchor_name) @anchor.name)) @anchor.def
|
|
296
|
+
;; An alias is a reference to its anchor.
|
|
285
297
|
(alias (alias_name) @call.id)
|
|
298
|
+
;; The whole document gets a root symbol (named via fallbackDefName).
|
|
286
299
|
(stream) @doc.def
|
|
287
300
|
`;
|
|
288
301
|
const YAML = {
|
|
@@ -291,42 +304,30 @@ const YAML = {
|
|
|
291
304
|
grammarKey: "yaml",
|
|
292
305
|
loadGrammar: () => loadNativeTreeSitter().yaml,
|
|
293
306
|
query: YAML_QUERY,
|
|
294
|
-
// Both block-style (`key: &x\n ...`) and flow-style (`key: &x {...}`) anchors
|
|
295
|
-
// wrap only the sigil+name; ascendToDef climbs from anchor_name -> anchor ->
|
|
296
|
-
// this enclosing node to find the def whose byte range covers the full value.
|
|
297
307
|
defNodeTypes: new Set(["block_node", "flow_node"]),
|
|
298
308
|
defKindOf: { "anchor.def": "variable", "doc.def": "file" },
|
|
299
309
|
nameToDef: { "anchor.name": "anchor.def" },
|
|
300
310
|
builtinMethods: new Set(),
|
|
301
|
-
// Alias references aren't calls; label the edge accordingly (Task 5).
|
|
302
311
|
referenceEdgeType: "references",
|
|
303
|
-
// Most aliases
|
|
304
|
-
//
|
|
305
|
-
// 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.
|
|
306
314
|
fallbackDefName: (file) => basename(file),
|
|
307
|
-
// Helm
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
// treating it as a templating marker would blanket-disable the fail-closed
|
|
312
|
-
// gate for .github/workflows/**.
|
|
313
|
-
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.
|
|
314
319
|
alwaysTemplatedExtensions: [".tpl"],
|
|
315
320
|
};
|
|
316
321
|
export const LANGUAGES = [TYPESCRIPT, TSX, PYTHON, GO, PHP, YAML];
|
|
322
|
+
/** Every registered extension, de-duplicated, in registry order. */
|
|
317
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. */
|
|
318
325
|
export function languageFor(file) {
|
|
319
|
-
|
|
320
|
-
if (lang.extensions.some((ext) => file.endsWith(ext)))
|
|
321
|
-
return lang;
|
|
322
|
-
}
|
|
323
|
-
return null;
|
|
326
|
+
return LANGUAGES.find((l) => l.extensions.some((ext) => file.endsWith(ext))) ?? null;
|
|
324
327
|
}
|
|
325
|
-
/** Prose formats
|
|
326
|
-
* them — no grammar, no symbols/edges, just eligible input to synthesis (issue #12). */
|
|
328
|
+
/** Prose formats eligible as synthesis input but never parsed. */
|
|
327
329
|
export const PROSE_EXTENSIONS = [".md"];
|
|
328
|
-
/**
|
|
329
|
-
* rather than "can tree-sitter parse this" (symbol/dependency extraction). */
|
|
330
|
+
/** True for parseable code or prose. Deliberately broader than parseability. */
|
|
330
331
|
export function isSubstantive(file) {
|
|
331
332
|
return languageFor(file) !== null || PROSE_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
332
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 {
|
|
@@ -46,24 +46,18 @@ export interface ParsedFile {
|
|
|
46
46
|
namespace: string | null;
|
|
47
47
|
parseable: boolean;
|
|
48
48
|
}
|
|
49
|
-
/** Cap on
|
|
50
|
-
* 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. */
|
|
51
50
|
export declare const MAX_BODY_TEXT_CHARS = 4000;
|
|
52
51
|
export declare function parseSource(file: string, source: string, opts?: {
|
|
53
52
|
throwOnParseError?: boolean;
|
|
54
53
|
}): ParsedFile | null;
|
|
55
54
|
/** Map each call site to the innermost symbol whose byte-range contains it.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* regardless of byte overlap — the caller must consume the exact same
|
|
63
|
-
* `parsed.symbols` array (or an equivalently-ordered copy) to look up a
|
|
64
|
-
* symbol by the index this function returns. The value maps callee name ->
|
|
65
|
-
* `memberOnly` (true iff every occurrence was a `x.foo()` member call, never
|
|
66
|
-
* 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. */
|
|
67
61
|
export declare function attributeCalls(parsed: ParsedFile): Map<number, Map<string, boolean>>;
|
|
68
62
|
/** Map each call bound to an imported module — `ns.fn()` through a module-object
|
|
69
63
|
* binding, or `g()` through an aliased member import — to the innermost symbol
|
package/dist/extractors/parse.js
CHANGED
|
@@ -20,28 +20,30 @@ function bundleFor(spec) {
|
|
|
20
20
|
}
|
|
21
21
|
return b;
|
|
22
22
|
}
|
|
23
|
+
/** Per-spec templating markers with g/y stripped, so `.test()` is stateless.
|
|
24
|
+
* Built once per language spec rather than on every parseSource call. */
|
|
25
|
+
const statelessMarkers = new WeakMap();
|
|
26
|
+
function templatingMarkersFor(spec) {
|
|
27
|
+
let markers = statelessMarkers.get(spec);
|
|
28
|
+
if (!markers) {
|
|
29
|
+
markers = (spec.templatingMarkers ?? []).map((re) => new RegExp(re.source, re.flags.replace(/[gy]/g, "")));
|
|
30
|
+
statelessMarkers.set(spec, markers);
|
|
31
|
+
}
|
|
32
|
+
return markers;
|
|
33
|
+
}
|
|
23
34
|
const STR_QUOTES = /^['"`]|['"`]$/g;
|
|
24
|
-
/** Cap on
|
|
25
|
-
* enough that a huge function/file doesn't bloat every JSON symbol record. */
|
|
35
|
+
/** Cap on stored symbol body text: enough context for review without bloating JSON records. */
|
|
26
36
|
export const MAX_BODY_TEXT_CHARS = 4000;
|
|
27
37
|
export function parseSource(file, source, opts = {}) {
|
|
28
38
|
const spec = languageFor(file);
|
|
29
39
|
if (!spec)
|
|
30
40
|
return null;
|
|
31
|
-
// Templated text
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// recovery badly enough that even the whole-file root node never forms
|
|
38
|
-
// (root.type becomes "ERROR", not "stream") — the fallback-symbol synthesis
|
|
39
|
-
// below covers that case so the file doesn't vanish from the component graph.
|
|
40
|
-
// String.prototype.search ignores lastIndex (unlike RegExp.test with a /g or
|
|
41
|
-
// /y flag), so a future templatingMarkers entry can't introduce cross-call
|
|
42
|
-
// statefulness here even if it forgets to keep its pattern flag-free.
|
|
43
|
-
const templated = (spec.alwaysTemplatedExtensions?.some((ext) => file.endsWith(ext)) ?? false)
|
|
44
|
-
|| (spec.templatingMarkers?.some((marker) => source.search(marker) !== -1) ?? false);
|
|
41
|
+
// Templated text is not valid language text until rendered. Still parse, so real
|
|
42
|
+
// anchors/symbols are extracted, but do not let the expected errors mark the file
|
|
43
|
+
// unparseable. Severe templating may stop even the root document node from
|
|
44
|
+
// forming; the fallback whole-file symbol below covers that case.
|
|
45
|
+
const templated = (spec.alwaysTemplatedExtensions ?? []).some((ext) => file.endsWith(ext))
|
|
46
|
+
|| templatingMarkersFor(spec).some((re) => re.test(source));
|
|
45
47
|
const { parser, query, bindingQuery } = bundleFor(spec);
|
|
46
48
|
// The native binding caps its scratch buffer at 32 KB unless bufferSize is
|
|
47
49
|
// given — without this, any source >= 32768 bytes throws "Invalid argument"
|
|
@@ -94,15 +96,12 @@ export function parseSource(file, source, opts = {}) {
|
|
|
94
96
|
const cname = cap.name;
|
|
95
97
|
const node = cap.node;
|
|
96
98
|
if (cname.endsWith(".def")) {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// (e.g. a Python method inside a class body matches both a class-nested
|
|
100
|
-
// "method.def" pattern and a general "fn.def" pattern — Task 4 relies on
|
|
101
|
-
// this to classify methods correctly without special-casing Python here).
|
|
99
|
+
// First classification wins: a node matched by several patterns keeps the
|
|
100
|
+
// kind it received first (Python nested methods, Go struct/interface specs).
|
|
102
101
|
if (!pendingDefs.has(node.id))
|
|
103
102
|
pendingDefs.set(node.id, { kind: spec.defKindOf[cname], def: node });
|
|
104
103
|
}
|
|
105
|
-
else if (spec.nameToDef
|
|
104
|
+
else if (Object.hasOwn(spec.nameToDef, cname)) {
|
|
106
105
|
// name capture: find its parent def node id by walking up to the def type
|
|
107
106
|
const defNode = ascendToDef(node, spec.defNodeTypes);
|
|
108
107
|
if (defNode) {
|
|
@@ -168,29 +167,21 @@ export function parseSource(file, source, opts = {}) {
|
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
for (const { kind, def, name } of pendingDefs.values()) {
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
170
|
+
const symbolName = name ?? spec.fallbackDefName?.(file);
|
|
171
|
+
if (!symbolName)
|
|
173
172
|
continue;
|
|
174
173
|
const loc = def.endPosition.row - def.startPosition.row + 1;
|
|
175
174
|
symbols.push({
|
|
176
|
-
name:
|
|
175
|
+
name: symbolName, kind,
|
|
177
176
|
startByte: def.startIndex, endByte: def.endIndex, loc,
|
|
178
177
|
bodyText: originalText(def).slice(0, MAX_BODY_TEXT_CHARS),
|
|
179
178
|
});
|
|
180
179
|
}
|
|
181
|
-
//
|
|
182
|
-
// (fallbackDefName). If templating broke error recovery badly enough that
|
|
183
|
-
// the doc.def capture never fired, synthesize the same fallback here rather
|
|
184
|
-
// than let the file silently drop out of the component graph. Push it before
|
|
185
|
-
// the sort below — parse()'s callers (indexer.ts) rely on symbols staying in
|
|
186
|
-
// start-byte order.
|
|
180
|
+
// Severe templating can stop the root node from forming; synthesize the whole-file symbol.
|
|
187
181
|
if (templated && spec.fallbackDefName && !symbols.some((s) => s.kind === "file")) {
|
|
188
182
|
symbols.push({
|
|
189
|
-
name: spec.fallbackDefName(file),
|
|
190
|
-
|
|
191
|
-
startByte: 0,
|
|
192
|
-
endByte: source.length,
|
|
193
|
-
loc: source.split("\n").length,
|
|
183
|
+
name: spec.fallbackDefName(file), kind: "file",
|
|
184
|
+
startByte: 0, endByte: source.length, loc: source.split("\n").length,
|
|
194
185
|
bodyText: source.slice(0, MAX_BODY_TEXT_CHARS),
|
|
195
186
|
});
|
|
196
187
|
}
|
|
@@ -241,7 +232,7 @@ function inToleratedScope(node, scopes) {
|
|
|
241
232
|
}
|
|
242
233
|
return false;
|
|
243
234
|
}
|
|
244
|
-
/**
|
|
235
|
+
/** Nearest ancestor of `node` whose type is a definition type, or null. */
|
|
245
236
|
function ascendToDef(node, defNodeTypes) {
|
|
246
237
|
let cur = node.parent;
|
|
247
238
|
while (cur) {
|
|
@@ -252,17 +243,12 @@ function ascendToDef(node, defNodeTypes) {
|
|
|
252
243
|
return null;
|
|
253
244
|
}
|
|
254
245
|
/** Map each call site to the innermost symbol whose byte-range contains it.
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* regardless of byte overlap — the caller must consume the exact same
|
|
262
|
-
* `parsed.symbols` array (or an equivalently-ordered copy) to look up a
|
|
263
|
-
* symbol by the index this function returns. The value maps callee name ->
|
|
264
|
-
* `memberOnly` (true iff every occurrence was a `x.foo()` member call, never
|
|
265
|
-
* a direct `foo()`), so the indexer can resolve member calls conservatively. */
|
|
246
|
+
* The result is keyed by the symbol's INDEX in `parsed.symbols`, not its start
|
|
247
|
+
* byte: two distinct symbols can share a start byte (a synthetic whole-file root
|
|
248
|
+
* beside others starting at byte 0), so callers must look symbols up in the same
|
|
249
|
+
* array order. Each value maps callee name -> memberOnly, true iff every
|
|
250
|
+
* occurrence of that callee inside the symbol was a member call, so the indexer
|
|
251
|
+
* can resolve member calls conservatively. */
|
|
266
252
|
export function attributeCalls(parsed) {
|
|
267
253
|
const out = new Map();
|
|
268
254
|
for (const call of parsed.calls) {
|
|
@@ -278,9 +264,11 @@ export function attributeCalls(parsed) {
|
|
|
278
264
|
}
|
|
279
265
|
}
|
|
280
266
|
if (best && best.name !== call.callee) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
267
|
+
let m = out.get(bestIndex);
|
|
268
|
+
if (!m) {
|
|
269
|
+
m = new Map();
|
|
270
|
+
out.set(bestIndex, m);
|
|
271
|
+
}
|
|
284
272
|
const prev = m.get(call.callee);
|
|
285
273
|
m.set(call.callee, prev === undefined ? call.member : prev && call.member);
|
|
286
274
|
}
|
|
@@ -82,6 +82,7 @@ export function renderHunchSection(store, root) {
|
|
|
82
82
|
bugs: store.json.loadAll("bugs").length,
|
|
83
83
|
constraints: store.json.loadAll("constraints").length,
|
|
84
84
|
components: store.json.loadAll("components").length,
|
|
85
|
+
// Public policies only: private-overlay policies must not change the committed block.
|
|
85
86
|
policies: root ? new PolicyRepository(root, store).listPolicies({ publicOnly: true }).length : 0,
|
|
86
87
|
findings: store.json.loadAll("findings").filter((f) => f.triage === "open" || f.triage === "accepted-risk" || f.triage === "scheduled").length,
|
|
87
88
|
};
|
|
@@ -32,12 +32,10 @@ const ENTRIES = [
|
|
|
32
32
|
".hunch/local.json",
|
|
33
33
|
// The strict hook's append-only catch-log: clone-local, never a memory record.
|
|
34
34
|
".hunch/events.log",
|
|
35
|
-
// The post-merge hook's queue of
|
|
36
|
-
//
|
|
37
|
-
// --apply` confirms and clears it).
|
|
35
|
+
// The post-merge hook's clone-local queue of unconfirmed provenance repairs —
|
|
36
|
+
// never committed; cleared by `hunch repair-provenance --apply`.
|
|
38
37
|
".hunch/pending-commit-repairs.json",
|
|
39
|
-
// Tombstones for
|
|
40
|
-
// scratch state, same discipline as the queue above.
|
|
38
|
+
// Tombstones for matches rejected via `--drop` — clone-local, never committed.
|
|
41
39
|
".hunch/dropped-commit-repairs.json",
|
|
42
40
|
// A local PRIVATE overlay store (HUNCH_PRIVATE_DIR) for sensitive memory — never
|
|
43
41
|
// committed. This is the conventional in-repo path; point the env elsewhere for a
|