grepmax 0.17.23 → 0.18.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/commands/audit.js +10 -2
- package/dist/commands/dead.js +3 -0
- package/dist/commands/doctor.js +103 -10
- package/dist/commands/impact.js +3 -0
- package/dist/commands/peek.js +3 -0
- package/dist/commands/related.js +3 -0
- package/dist/commands/search-output.js +531 -0
- package/dist/commands/search.js +46 -462
- package/dist/commands/similar.js +3 -0
- package/dist/commands/test-find.js +3 -0
- package/dist/commands/trace.js +3 -0
- package/dist/config.js +80 -5
- package/dist/eval-graph-nav.js +374 -0
- package/dist/eval-graph-sanity.js +138 -19
- package/dist/lib/daemon/daemon.js +29 -643
- package/dist/lib/daemon/mlx-server-manager.js +191 -0
- package/dist/lib/daemon/process-manager.js +139 -0
- package/dist/lib/daemon/watcher-manager.js +484 -0
- package/dist/lib/graph/graph-builder.js +30 -8
- package/dist/lib/graph/impact.js +5 -3
- package/dist/lib/index/chunker.js +149 -4
- package/dist/lib/llm/tools.js +58 -16
- package/dist/lib/store/vector-db.js +43 -13
- package/dist/lib/utils/stale-hint.js +164 -0
- package/dist/lib/workers/orchestrator.js +2 -1
- package/package.json +17 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -359,7 +359,13 @@ class TreeSitterChunker {
|
|
|
359
359
|
if (t !== "lexical_declaration" && t !== "variable_declaration")
|
|
360
360
|
return false;
|
|
361
361
|
const parentType = ((_a = node.parent) === null || _a === void 0 ? void 0 : _a.type) || "";
|
|
362
|
-
const allowedParents = [
|
|
362
|
+
const allowedParents = [
|
|
363
|
+
"program",
|
|
364
|
+
"module",
|
|
365
|
+
"source_file",
|
|
366
|
+
"class_body",
|
|
367
|
+
"export_statement",
|
|
368
|
+
];
|
|
363
369
|
if (parentType && !allowedParents.includes(parentType))
|
|
364
370
|
return false;
|
|
365
371
|
// Exported declarations are part of the API surface, so always index
|
|
@@ -367,7 +373,8 @@ class TreeSitterChunker {
|
|
|
367
373
|
// is `export const typeDefs = gql\`...\`` (template literals, object
|
|
368
374
|
// literals, or non-PascalCase consts) had no defined_symbols entry
|
|
369
375
|
// and peek/extract returned "Symbol not found".
|
|
370
|
-
if (parentType === "export_statement" ||
|
|
376
|
+
if (parentType === "export_statement" ||
|
|
377
|
+
parentType === "export_declaration") {
|
|
371
378
|
return true;
|
|
372
379
|
}
|
|
373
380
|
const text = node.text || "";
|
|
@@ -553,6 +560,15 @@ class TreeSitterChunker {
|
|
|
553
560
|
referencedSymbols.push(name);
|
|
554
561
|
}
|
|
555
562
|
};
|
|
563
|
+
// Type-position references go in a SEPARATE list so they never inflate
|
|
564
|
+
// referencedSymbols.length (which drives role classification below and
|
|
565
|
+
// the search structural boost). Navigation consumers union the two.
|
|
566
|
+
const typeReferencedSymbols = [];
|
|
567
|
+
const addTypeRef = (n) => {
|
|
568
|
+
if (n && !typeReferencedSymbols.includes(n)) {
|
|
569
|
+
typeReferencedSymbols.push(n);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
556
572
|
// Leaf identifier node types across grammars (a bare name with no
|
|
557
573
|
// named children — `ErrorCodes`, not `a.ErrorCodes`).
|
|
558
574
|
const LEAF_ID_TYPES = new Set([
|
|
@@ -623,8 +639,33 @@ class TreeSitterChunker {
|
|
|
623
639
|
"struct_expression", // Rust
|
|
624
640
|
"instance_expression", // Scala
|
|
625
641
|
]);
|
|
642
|
+
// Harvest Capitalized type names out of an annotation subtree, for the
|
|
643
|
+
// grammars whose type names are NOT `type_identifier` nodes (so Shape 4
|
|
644
|
+
// can't see them): Python/C# spell them `identifier`, PHP spells them
|
|
645
|
+
// `name`. We walk the WHOLE subtree and keep every Capitalized leaf of
|
|
646
|
+
// those kinds — safe because we only ever enter from a type position (a
|
|
647
|
+
// `type` / `return_type` / `returns` field, or a base/heritage list).
|
|
648
|
+
// Excludes the chunk's own name; lowercase builtins (`list`, `int`,
|
|
649
|
+
// `string`) and non-name leaves (`predefined_type`, `string_content`
|
|
650
|
+
// in `Literal["x"]`) fall out via the kind + Capitalized gates.
|
|
651
|
+
const TYPE_NAME_LEAF_TYPES = new Set(["identifier", "name"]);
|
|
652
|
+
const harvestTypeLeaves = (node) => {
|
|
653
|
+
var _a, _b, _c;
|
|
654
|
+
if (!node)
|
|
655
|
+
return;
|
|
656
|
+
if (((_b = (_a = node.namedChildren) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) === 0) {
|
|
657
|
+
if (TYPE_NAME_LEAF_TYPES.has(node.type) &&
|
|
658
|
+
/^[A-Z]/.test(node.text) &&
|
|
659
|
+
node.text !== name) {
|
|
660
|
+
addTypeRef(node.text);
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
for (const c of (_c = node.namedChildren) !== null && _c !== void 0 ? _c : [])
|
|
665
|
+
harvestTypeLeaves(c);
|
|
666
|
+
};
|
|
626
667
|
const extractRefs = (n) => {
|
|
627
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
668
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
|
|
628
669
|
// Handle JS/TS (call_expression), Python (call), Lua (function_call)
|
|
629
670
|
if (n.type === "call_expression" ||
|
|
630
671
|
n.type === "call" ||
|
|
@@ -737,10 +778,110 @@ class TreeSitterChunker {
|
|
|
737
778
|
addRef(head.text);
|
|
738
779
|
}
|
|
739
780
|
}
|
|
740
|
-
|
|
781
|
+
// Shape 4 — type-position reference (TS/TSX): `: T`, `<T>`, `T[]`,
|
|
782
|
+
// `as T`, `interface X extends T`, type aliases. TS uses a distinct
|
|
783
|
+
// `type_identifier` node for type names (primitives are
|
|
784
|
+
// `predefined_type`, so they never reach here). Capture Capitalized
|
|
785
|
+
// ones, skipping the chunk's own name and any locally-declared type
|
|
786
|
+
// parameter (`<T>` and its uses `value: T`). Constraints
|
|
787
|
+
// (`T extends Foo`) still yield `Foo`. Type list, never referencedSymbols.
|
|
788
|
+
if (n.type === "type_identifier" &&
|
|
789
|
+
/^[A-Z]/.test(n.text) &&
|
|
790
|
+
n.text !== name &&
|
|
791
|
+
!declaredTypeParams.has(n.text)) {
|
|
792
|
+
addTypeRef(n.text);
|
|
793
|
+
}
|
|
794
|
+
// Shape 5 — class heritage `class A extends B`: the superclass is an
|
|
795
|
+
// `identifier` (a runtime value), not a `type_identifier`, so Shape 4
|
|
796
|
+
// misses it. `implements I` and `interface X extends Y` use
|
|
797
|
+
// type_identifier and are already covered. Reduce `extends ns.Base`
|
|
798
|
+
// to `Base`; skip type arguments (Shape 4 handles those).
|
|
799
|
+
if (n.type === "extends_clause") {
|
|
800
|
+
for (const c of (_j = n.namedChildren) !== null && _j !== void 0 ? _j : []) {
|
|
801
|
+
if (c.type === "type_arguments")
|
|
802
|
+
continue;
|
|
803
|
+
const sup = simpleRefName(c);
|
|
804
|
+
if (sup && /^[A-Z]/.test(sup) && sup !== name)
|
|
805
|
+
addTypeRef(sup);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Shape 6 — type-position references for grammars whose type names
|
|
809
|
+
// are plain `identifier` / `name` nodes (not `type_identifier`), so
|
|
810
|
+
// Shape 4 can't reach them. Read the annotation off the carrier
|
|
811
|
+
// node's type field and harvest from THAT subtree only — never the
|
|
812
|
+
// node as a whole — so value expressions stay out of the type list.
|
|
813
|
+
// Each grammar is gated by `lang` because the carrier node/field
|
|
814
|
+
// names collide across grammars (`function_definition`,
|
|
815
|
+
// `method_declaration`, `assignment`, …). The other typed grammars
|
|
816
|
+
// (Go/Rust/Java/Kotlin/Scala/Swift) spell type names as
|
|
817
|
+
// `type_identifier` and are already covered by Shape 4.
|
|
818
|
+
if (lang === "python") {
|
|
819
|
+
// `x: Foo`, `-> Bar`, `v: Baz = …`, and `class C(Base)` bases.
|
|
820
|
+
if (n.type === "typed_parameter" ||
|
|
821
|
+
n.type === "typed_default_parameter") {
|
|
822
|
+
harvestTypeLeaves((_l = (_k = n.childForFieldName) === null || _k === void 0 ? void 0 : _k.call(n, "type")) !== null && _l !== void 0 ? _l : null);
|
|
823
|
+
}
|
|
824
|
+
else if (n.type === "function_definition") {
|
|
825
|
+
harvestTypeLeaves((_o = (_m = n.childForFieldName) === null || _m === void 0 ? void 0 : _m.call(n, "return_type")) !== null && _o !== void 0 ? _o : null);
|
|
826
|
+
}
|
|
827
|
+
else if (n.type === "assignment") {
|
|
828
|
+
harvestTypeLeaves((_q = (_p = n.childForFieldName) === null || _p === void 0 ? void 0 : _p.call(n, "type")) !== null && _q !== void 0 ? _q : null);
|
|
829
|
+
}
|
|
830
|
+
else if (n.type === "class_definition") {
|
|
831
|
+
harvestTypeLeaves((_s = (_r = n.childForFieldName) === null || _r === void 0 ? void 0 : _r.call(n, "superclasses")) !== null && _s !== void 0 ? _s : null);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
else if (lang === "c_sharp") {
|
|
835
|
+
// Method return (`returns`), parameter / property / variable
|
|
836
|
+
// (`type`), and the base/interface list.
|
|
837
|
+
if (n.type === "parameter" ||
|
|
838
|
+
n.type === "property_declaration" ||
|
|
839
|
+
n.type === "variable_declaration") {
|
|
840
|
+
harvestTypeLeaves((_u = (_t = n.childForFieldName) === null || _t === void 0 ? void 0 : _t.call(n, "type")) !== null && _u !== void 0 ? _u : null);
|
|
841
|
+
}
|
|
842
|
+
else if (n.type === "method_declaration") {
|
|
843
|
+
harvestTypeLeaves((_w = (_v = n.childForFieldName) === null || _v === void 0 ? void 0 : _v.call(n, "returns")) !== null && _w !== void 0 ? _w : null);
|
|
844
|
+
}
|
|
845
|
+
else if (n.type === "base_list") {
|
|
846
|
+
harvestTypeLeaves(n);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
else if (lang === "php") {
|
|
850
|
+
// Types are `name` inside `named_type` / `optional_type`.
|
|
851
|
+
// Parameter / property (`type`), function & method return
|
|
852
|
+
// (`return_type`), and the extends / implements clauses.
|
|
853
|
+
if (n.type === "simple_parameter" ||
|
|
854
|
+
n.type === "property_declaration") {
|
|
855
|
+
harvestTypeLeaves((_y = (_x = n.childForFieldName) === null || _x === void 0 ? void 0 : _x.call(n, "type")) !== null && _y !== void 0 ? _y : null);
|
|
856
|
+
}
|
|
857
|
+
else if (n.type === "function_definition" ||
|
|
858
|
+
n.type === "method_declaration") {
|
|
859
|
+
harvestTypeLeaves((_0 = (_z = n.childForFieldName) === null || _z === void 0 ? void 0 : _z.call(n, "return_type")) !== null && _0 !== void 0 ? _0 : null);
|
|
860
|
+
}
|
|
861
|
+
else if (n.type === "base_clause" ||
|
|
862
|
+
n.type === "class_interface_clause") {
|
|
863
|
+
harvestTypeLeaves(n);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
for (const child of (_1 = n.namedChildren) !== null && _1 !== void 0 ? _1 : []) {
|
|
741
867
|
extractRefs(child);
|
|
742
868
|
}
|
|
743
869
|
};
|
|
870
|
+
// Collect locally-declared type-parameter names (`<T, K>`) so their
|
|
871
|
+
// *uses* (`value: T`) are excluded from type refs, not just their
|
|
872
|
+
// declarations. Chunk-scoped, which matches single-definition chunks.
|
|
873
|
+
const declaredTypeParams = new Set();
|
|
874
|
+
const collectTypeParams = (n) => {
|
|
875
|
+
var _a;
|
|
876
|
+
if (n.type === "type_parameter") {
|
|
877
|
+
const nm = firstNamed(n);
|
|
878
|
+
if ((nm === null || nm === void 0 ? void 0 : nm.type) === "type_identifier")
|
|
879
|
+
declaredTypeParams.add(nm.text);
|
|
880
|
+
}
|
|
881
|
+
for (const child of (_a = n.namedChildren) !== null && _a !== void 0 ? _a : [])
|
|
882
|
+
collectTypeParams(child);
|
|
883
|
+
};
|
|
884
|
+
collectTypeParams(effective);
|
|
744
885
|
extractRefs(effective);
|
|
745
886
|
// Classify role
|
|
746
887
|
let role = "IMPLEMENTATION";
|
|
@@ -761,6 +902,7 @@ class TreeSitterChunker {
|
|
|
761
902
|
isExported,
|
|
762
903
|
definedSymbols,
|
|
763
904
|
referencedSymbols,
|
|
905
|
+
typeReferencedSymbols,
|
|
764
906
|
role,
|
|
765
907
|
parentSymbol: stack.length > 1
|
|
766
908
|
? stack[stack.length - 1].replace(/^(Class|Method|Function|Interface|Type): /, "")
|
|
@@ -843,6 +985,9 @@ class TreeSitterChunker {
|
|
|
843
985
|
if (sub.referencedSymbols) {
|
|
844
986
|
sub.referencedSymbols = sub.referencedSymbols.filter(occurs);
|
|
845
987
|
}
|
|
988
|
+
if (sub.typeReferencedSymbols) {
|
|
989
|
+
sub.typeReferencedSymbols = sub.typeReferencedSymbols.filter(occurs);
|
|
990
|
+
}
|
|
846
991
|
return sub;
|
|
847
992
|
}
|
|
848
993
|
splitIfTooBig(chunk) {
|
package/dist/lib/llm/tools.js
CHANGED
|
@@ -61,10 +61,22 @@ exports.TOOLS = [
|
|
|
61
61
|
parameters: {
|
|
62
62
|
type: "object",
|
|
63
63
|
properties: {
|
|
64
|
-
query: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
query: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Natural language search query",
|
|
67
|
+
},
|
|
68
|
+
max_count: {
|
|
69
|
+
type: "integer",
|
|
70
|
+
description: "Max results (default 5)",
|
|
71
|
+
},
|
|
72
|
+
lang: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Filter by extension (e.g. 'ts', 'py')",
|
|
75
|
+
},
|
|
76
|
+
file: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Filter to files matching this name",
|
|
79
|
+
},
|
|
68
80
|
},
|
|
69
81
|
required: ["query"],
|
|
70
82
|
},
|
|
@@ -79,7 +91,10 @@ exports.TOOLS = [
|
|
|
79
91
|
type: "object",
|
|
80
92
|
properties: {
|
|
81
93
|
symbol: { type: "string", description: "Symbol name to trace" },
|
|
82
|
-
depth: {
|
|
94
|
+
depth: {
|
|
95
|
+
type: "integer",
|
|
96
|
+
description: "Caller depth 1-3 (default 1)",
|
|
97
|
+
},
|
|
83
98
|
},
|
|
84
99
|
required: ["symbol"],
|
|
85
100
|
},
|
|
@@ -94,7 +109,10 @@ exports.TOOLS = [
|
|
|
94
109
|
type: "object",
|
|
95
110
|
properties: {
|
|
96
111
|
symbol: { type: "string", description: "Symbol name to peek at" },
|
|
97
|
-
depth: {
|
|
112
|
+
depth: {
|
|
113
|
+
type: "integer",
|
|
114
|
+
description: "Caller depth 1-3 (default 1)",
|
|
115
|
+
},
|
|
98
116
|
},
|
|
99
117
|
required: ["symbol"],
|
|
100
118
|
},
|
|
@@ -109,7 +127,10 @@ exports.TOOLS = [
|
|
|
109
127
|
type: "object",
|
|
110
128
|
properties: {
|
|
111
129
|
target: { type: "string", description: "Symbol name or file path" },
|
|
112
|
-
depth: {
|
|
130
|
+
depth: {
|
|
131
|
+
type: "integer",
|
|
132
|
+
description: "Traversal depth 1-3 (default 1)",
|
|
133
|
+
},
|
|
113
134
|
},
|
|
114
135
|
required: ["target"],
|
|
115
136
|
},
|
|
@@ -123,7 +144,10 @@ exports.TOOLS = [
|
|
|
123
144
|
parameters: {
|
|
124
145
|
type: "object",
|
|
125
146
|
properties: {
|
|
126
|
-
file: {
|
|
147
|
+
file: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "File path relative to project root",
|
|
150
|
+
},
|
|
127
151
|
},
|
|
128
152
|
required: ["file"],
|
|
129
153
|
},
|
|
@@ -161,9 +185,13 @@ function executeSearch(args, ctx) {
|
|
|
161
185
|
const rp = rel(absPath, ctx.projectRoot);
|
|
162
186
|
const defs = (0, arrow_1.toArr)((_d = r.definedSymbols) !== null && _d !== void 0 ? _d : r.defined_symbols);
|
|
163
187
|
const sym = defs[0] || "(anonymous)";
|
|
164
|
-
const role = String((_e = r.role) !== null && _e !== void 0 ? _e : "IMPL")
|
|
188
|
+
const role = String((_e = r.role) !== null && _e !== void 0 ? _e : "IMPL")
|
|
189
|
+
.slice(0, 4)
|
|
190
|
+
.toUpperCase();
|
|
165
191
|
const startLine = (_j = (_h = (_f = r.startLine) !== null && _f !== void 0 ? _f : (_g = r.generated_metadata) === null || _g === void 0 ? void 0 : _g.start_line) !== null && _h !== void 0 ? _h : r.start_line) !== null && _j !== void 0 ? _j : 0;
|
|
166
|
-
const hint = String((_l = (_k = r.text) !== null && _k !== void 0 ? _k : r.content) !== null && _l !== void 0 ? _l : "")
|
|
192
|
+
const hint = String((_l = (_k = r.text) !== null && _k !== void 0 ? _k : r.content) !== null && _l !== void 0 ? _l : "")
|
|
193
|
+
.split("\n")[0]
|
|
194
|
+
.slice(0, 100);
|
|
167
195
|
return `${rp}:${startLine} ${sym} [${role}] — ${hint}`;
|
|
168
196
|
});
|
|
169
197
|
return lines.join("\n");
|
|
@@ -220,8 +248,12 @@ function executePeek(args, ctx) {
|
|
|
220
248
|
.limit(1)
|
|
221
249
|
.toArray();
|
|
222
250
|
const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
|
|
223
|
-
const startLine = metaRows.length > 0
|
|
224
|
-
|
|
251
|
+
const startLine = metaRows.length > 0
|
|
252
|
+
? Number(metaRows[0].start_line || 0)
|
|
253
|
+
: center.line;
|
|
254
|
+
const endLine = metaRows.length > 0
|
|
255
|
+
? Number(metaRows[0].end_line || 0)
|
|
256
|
+
: center.line;
|
|
225
257
|
// Extract signature
|
|
226
258
|
let sig = "(source not available)";
|
|
227
259
|
try {
|
|
@@ -294,7 +326,11 @@ function executeRelated(args, ctx) {
|
|
|
294
326
|
// Get file's symbols
|
|
295
327
|
const fileChunks = yield table
|
|
296
328
|
.query()
|
|
297
|
-
.select([
|
|
329
|
+
.select([
|
|
330
|
+
"defined_symbols",
|
|
331
|
+
"referenced_symbols",
|
|
332
|
+
"type_referenced_symbols",
|
|
333
|
+
])
|
|
298
334
|
.where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
|
|
299
335
|
.toArray();
|
|
300
336
|
if (fileChunks.length === 0)
|
|
@@ -306,6 +342,8 @@ function executeRelated(args, ctx) {
|
|
|
306
342
|
definedHere.add(s);
|
|
307
343
|
for (const s of (0, arrow_1.toArr)(chunk.referenced_symbols))
|
|
308
344
|
referencedHere.add(s);
|
|
345
|
+
for (const s of (0, arrow_1.toArr)(chunk.type_referenced_symbols))
|
|
346
|
+
referencedHere.add(s);
|
|
309
347
|
}
|
|
310
348
|
// Dependencies: files that DEFINE symbols this file REFERENCES
|
|
311
349
|
const depCounts = new Map();
|
|
@@ -331,7 +369,7 @@ function executeRelated(args, ctx) {
|
|
|
331
369
|
const rows = yield table
|
|
332
370
|
.query()
|
|
333
371
|
.select(["path"])
|
|
334
|
-
.where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')`)
|
|
372
|
+
.where(`(array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') OR array_contains(type_referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}'))`)
|
|
335
373
|
.limit(20)
|
|
336
374
|
.toArray();
|
|
337
375
|
for (const row of rows) {
|
|
@@ -341,8 +379,12 @@ function executeRelated(args, ctx) {
|
|
|
341
379
|
revCounts.set(p, (revCounts.get(p) || 0) + 1);
|
|
342
380
|
}
|
|
343
381
|
}
|
|
344
|
-
const topDeps = [...depCounts.entries()]
|
|
345
|
-
|
|
382
|
+
const topDeps = [...depCounts.entries()]
|
|
383
|
+
.sort((a, b) => b[1] - a[1])
|
|
384
|
+
.slice(0, 10);
|
|
385
|
+
const topRevs = [...revCounts.entries()]
|
|
386
|
+
.sort((a, b) => b[1] - a[1])
|
|
387
|
+
.slice(0, 10);
|
|
346
388
|
if (topDeps.length === 0 && topRevs.length === 0)
|
|
347
389
|
return "(none)";
|
|
348
390
|
const lines = [];
|
|
@@ -48,9 +48,9 @@ const fs = __importStar(require("node:fs"));
|
|
|
48
48
|
const lancedb = __importStar(require("@lancedb/lancedb"));
|
|
49
49
|
const apache_arrow_1 = require("apache-arrow");
|
|
50
50
|
const config_1 = require("../../config");
|
|
51
|
+
const cleanup_1 = require("../utils/cleanup");
|
|
51
52
|
const filter_builder_1 = require("../utils/filter-builder");
|
|
52
53
|
const logger_1 = require("../utils/logger");
|
|
53
|
-
const cleanup_1 = require("../utils/cleanup");
|
|
54
54
|
class DiskPressureError extends Error {
|
|
55
55
|
constructor(message = "Disk critically low — writes suspended") {
|
|
56
56
|
super(message);
|
|
@@ -266,6 +266,7 @@ class VectorDB {
|
|
|
266
266
|
doc_token_ids: [],
|
|
267
267
|
defined_symbols: [],
|
|
268
268
|
referenced_symbols: [],
|
|
269
|
+
type_referenced_symbols: [],
|
|
269
270
|
imports: [],
|
|
270
271
|
exports: [],
|
|
271
272
|
role: "",
|
|
@@ -308,6 +309,7 @@ class VectorDB {
|
|
|
308
309
|
new apache_arrow_1.Field("doc_token_ids", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Int32(), true)), true),
|
|
309
310
|
new apache_arrow_1.Field("defined_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
|
|
310
311
|
new apache_arrow_1.Field("referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
|
|
312
|
+
new apache_arrow_1.Field("type_referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
|
|
311
313
|
new apache_arrow_1.Field("imports", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
|
|
312
314
|
new apache_arrow_1.Field("exports", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
|
|
313
315
|
new apache_arrow_1.Field("role", new apache_arrow_1.Utf8(), true),
|
|
@@ -316,12 +318,38 @@ class VectorDB {
|
|
|
316
318
|
new apache_arrow_1.Field("summary", new apache_arrow_1.Utf8(), true),
|
|
317
319
|
]);
|
|
318
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* In-place, non-breaking schema evolution for additive list columns. Older
|
|
323
|
+
* tables predate `type_referenced_symbols`; adding it via `addColumns` (rather
|
|
324
|
+
* than forcing `gmax index --reset`) keeps a live daemon's incremental writes
|
|
325
|
+
* working — existing rows read back as empty until their file is reindexed,
|
|
326
|
+
* which is when the new edges would populate anyway. Idempotent: re-adding an
|
|
327
|
+
* existing column throws, which we swallow.
|
|
328
|
+
*/
|
|
329
|
+
evolveSchema(table) {
|
|
330
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
331
|
+
const schema = yield table.schema();
|
|
332
|
+
const fields = new Set(schema.fields.map((f) => f.name));
|
|
333
|
+
if (fields.has("type_referenced_symbols"))
|
|
334
|
+
return;
|
|
335
|
+
try {
|
|
336
|
+
yield table.addColumns(new apache_arrow_1.Field("type_referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true));
|
|
337
|
+
(0, logger_1.log)("db", "Added type_referenced_symbols column to existing table");
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
// Lost a race with another writer that already added it, or a transient
|
|
341
|
+
// commit conflict — the next ensureTable() re-checks and no-ops.
|
|
342
|
+
(0, logger_1.debug)("vectordb", `evolveSchema skipped: ${err.message}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
319
346
|
ensureTable() {
|
|
320
347
|
return __awaiter(this, void 0, void 0, function* () {
|
|
321
348
|
const db = yield this.getDb();
|
|
322
349
|
try {
|
|
323
350
|
const table = yield db.openTable(TABLE_NAME);
|
|
324
351
|
yield this.validateSchema(table);
|
|
352
|
+
yield this.evolveSchema(table);
|
|
325
353
|
return table;
|
|
326
354
|
}
|
|
327
355
|
catch (_err) {
|
|
@@ -337,7 +365,7 @@ class VectorDB {
|
|
|
337
365
|
}
|
|
338
366
|
insertBatch(records) {
|
|
339
367
|
return __awaiter(this, void 0, void 0, function* () {
|
|
340
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
368
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
341
369
|
if (!records.length)
|
|
342
370
|
return;
|
|
343
371
|
this.ensureDiskOk();
|
|
@@ -410,12 +438,13 @@ class VectorDB {
|
|
|
410
438
|
: null;
|
|
411
439
|
rec.defined_symbols = (_g = rec.defined_symbols) !== null && _g !== void 0 ? _g : [];
|
|
412
440
|
rec.referenced_symbols = (_h = rec.referenced_symbols) !== null && _h !== void 0 ? _h : [];
|
|
413
|
-
rec.
|
|
414
|
-
rec.
|
|
415
|
-
rec.
|
|
416
|
-
rec.
|
|
417
|
-
rec.
|
|
418
|
-
rec.
|
|
441
|
+
rec.type_referenced_symbols = (_j = rec.type_referenced_symbols) !== null && _j !== void 0 ? _j : [];
|
|
442
|
+
rec.imports = (_k = rec.imports) !== null && _k !== void 0 ? _k : [];
|
|
443
|
+
rec.exports = (_l = rec.exports) !== null && _l !== void 0 ? _l : [];
|
|
444
|
+
rec.role = (_m = rec.role) !== null && _m !== void 0 ? _m : "";
|
|
445
|
+
rec.parent_symbol = (_o = rec.parent_symbol) !== null && _o !== void 0 ? _o : "";
|
|
446
|
+
rec.file_skeleton = (_p = rec.file_skeleton) !== null && _p !== void 0 ? _p : "";
|
|
447
|
+
rec.summary = (_q = rec.summary) !== null && _q !== void 0 ? _q : null;
|
|
419
448
|
}
|
|
420
449
|
try {
|
|
421
450
|
yield this.withWriteGate(() => table.add(records));
|
|
@@ -488,7 +517,9 @@ class VectorDB {
|
|
|
488
517
|
const table = yield this.ensureTable();
|
|
489
518
|
const cutoff = new Date(Date.now() - retentionMs);
|
|
490
519
|
let resolveCompacting;
|
|
491
|
-
this.compactingPromise = new Promise((r) => {
|
|
520
|
+
this.compactingPromise = new Promise((r) => {
|
|
521
|
+
resolveCompacting = r;
|
|
522
|
+
});
|
|
492
523
|
try {
|
|
493
524
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
494
525
|
yield this.drainWrites();
|
|
@@ -519,7 +550,8 @@ class VectorDB {
|
|
|
519
550
|
return;
|
|
520
551
|
}
|
|
521
552
|
// ENOSPC: return immediately — retrying will only make things worse
|
|
522
|
-
if (msg.includes("No space left on device") ||
|
|
553
|
+
if (msg.includes("No space left on device") ||
|
|
554
|
+
msg.includes("os error 28")) {
|
|
523
555
|
(0, logger_1.log)("vectordb", `Optimize failed (ENOSPC): disk full — skipping retries`);
|
|
524
556
|
return;
|
|
525
557
|
}
|
|
@@ -753,9 +785,7 @@ class VectorDB {
|
|
|
753
785
|
yield this.withWriteGate(() => __awaiter(this, void 0, void 0, function* () {
|
|
754
786
|
for (let i = 0; i < unique.length; i += batchSize) {
|
|
755
787
|
const slice = unique.slice(i, i + batchSize);
|
|
756
|
-
const values = slice
|
|
757
|
-
.map((p) => `'${(0, filter_builder_1.escapeSqlString)(p)}'`)
|
|
758
|
-
.join(",");
|
|
788
|
+
const values = slice.map((p) => `'${(0, filter_builder_1.escapeSqlString)(p)}'`).join(",");
|
|
759
789
|
const where = `path IN (${values})${idExclusion}`;
|
|
760
790
|
const existing = yield table
|
|
761
791
|
.query()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._resetStaleHintForTests = _resetStaleHintForTests;
|
|
4
|
+
exports.maybeWarnStaleChunker = maybeWarnStaleChunker;
|
|
5
|
+
exports.maybeWarnStaleEmbedding = maybeWarnStaleEmbedding;
|
|
6
|
+
exports.maybeWarnCrossProjectDim = maybeWarnCrossProjectDim;
|
|
7
|
+
/**
|
|
8
|
+
* Query-time staleness nudges.
|
|
9
|
+
*
|
|
10
|
+
* When a graph/search command resolves a project whose index is out of date —
|
|
11
|
+
* built by an older chunker, or with a different embedding model/dim than the
|
|
12
|
+
* current config — emit a single concise line to STDERR. Writing to stderr
|
|
13
|
+
* (never stdout) keeps `--json` / `--agent` machine output byte-identical —
|
|
14
|
+
* agents parse stdout, so the hint never pollutes it. In `--agent` mode the line
|
|
15
|
+
* is rendered as a structured TSV record instead of prose so a tool that *does*
|
|
16
|
+
* capture stderr can parse it.
|
|
17
|
+
*
|
|
18
|
+
* Each concern fires at most once per process (independent latches, so a stale
|
|
19
|
+
* chunker does not mask a stale embedding). Suppress all with GMAX_NO_STALE_HINT=1.
|
|
20
|
+
*/
|
|
21
|
+
const config_1 = require("../../config");
|
|
22
|
+
const index_config_1 = require("../index/index-config");
|
|
23
|
+
const project_registry_1 = require("./project-registry");
|
|
24
|
+
let chunkerEmitted = false;
|
|
25
|
+
let embeddingEmitted = false;
|
|
26
|
+
let crossDimEmitted = false;
|
|
27
|
+
/** Reset the once-per-process latches. Test-only. */
|
|
28
|
+
function _resetStaleHintForTests() {
|
|
29
|
+
chunkerEmitted = false;
|
|
30
|
+
embeddingEmitted = false;
|
|
31
|
+
crossDimEmitted = false;
|
|
32
|
+
}
|
|
33
|
+
function maybeWarnStaleChunker(projectRoot, opts) {
|
|
34
|
+
if (chunkerEmitted)
|
|
35
|
+
return;
|
|
36
|
+
if (!projectRoot)
|
|
37
|
+
return;
|
|
38
|
+
if (process.env.GMAX_NO_STALE_HINT === "1")
|
|
39
|
+
return;
|
|
40
|
+
const project = (0, project_registry_1.getProject)(projectRoot);
|
|
41
|
+
// Only nudge for an index that is supposed to be complete. A "pending"
|
|
42
|
+
// project is mid-index (will land current); "error" is ignored by the
|
|
43
|
+
// daemon; an unregistered root would already have errored upstream.
|
|
44
|
+
if (!project)
|
|
45
|
+
return;
|
|
46
|
+
if (project.status && project.status !== "indexed")
|
|
47
|
+
return;
|
|
48
|
+
const gap = (0, config_1.describeChunkerGap)(project.chunkerVersion);
|
|
49
|
+
if (!gap)
|
|
50
|
+
return;
|
|
51
|
+
// Latch only once we have something to say, so a suppressed-by-status call
|
|
52
|
+
// does not silence a later command in the same process (CLI is one command
|
|
53
|
+
// per process, but search resolves the root in two places).
|
|
54
|
+
chunkerEmitted = true;
|
|
55
|
+
const name = project.name || projectRoot;
|
|
56
|
+
if (opts === null || opts === void 0 ? void 0 : opts.agent) {
|
|
57
|
+
const fields = [
|
|
58
|
+
"stale_chunker",
|
|
59
|
+
`project=${name}`,
|
|
60
|
+
`indexed_v=${gap.fromVersion}`,
|
|
61
|
+
`current_v=${gap.toVersion}`,
|
|
62
|
+
`severity=${gap.severity}`,
|
|
63
|
+
`note=${gap.notes.join("; ")}`,
|
|
64
|
+
"fix=gmax index --reset",
|
|
65
|
+
].join("\t");
|
|
66
|
+
process.stderr.write(`${fields}\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const label = gap.severity === "breaking" ? "WARN" : "hint";
|
|
70
|
+
process.stderr.write(`${label} gmax: '${name}' indexed by chunker v${gap.fromVersion} (now v${gap.toVersion}) — ${gap.notes.join(" ")} Run 'gmax index --reset' to refresh. (silence: GMAX_NO_STALE_HINT=1)\n`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Nudge when a project's stored embedding model/dim differs from the current
|
|
74
|
+
* global config. A dim change is `breaking` (search scores are invalid until a
|
|
75
|
+
* re-embed); a same-dim model swap is `additive` (results just mix models).
|
|
76
|
+
* Mirrors maybeWarnStaleChunker's discipline: stderr-only, once-per-process,
|
|
77
|
+
* `--agent` TSV, GMAX_NO_STALE_HINT-suppressible.
|
|
78
|
+
*/
|
|
79
|
+
function maybeWarnStaleEmbedding(projectRoot, opts) {
|
|
80
|
+
if (embeddingEmitted)
|
|
81
|
+
return;
|
|
82
|
+
if (!projectRoot)
|
|
83
|
+
return;
|
|
84
|
+
if (process.env.GMAX_NO_STALE_HINT === "1")
|
|
85
|
+
return;
|
|
86
|
+
const project = (0, project_registry_1.getProject)(projectRoot);
|
|
87
|
+
if (!project)
|
|
88
|
+
return;
|
|
89
|
+
if (project.status && project.status !== "indexed")
|
|
90
|
+
return;
|
|
91
|
+
const current = (0, index_config_1.readGlobalConfig)();
|
|
92
|
+
const gap = (0, config_1.describeEmbeddingGap)({ modelTier: project.modelTier, vectorDim: project.vectorDim }, { modelTier: current.modelTier, vectorDim: current.vectorDim });
|
|
93
|
+
if (!gap)
|
|
94
|
+
return;
|
|
95
|
+
embeddingEmitted = true;
|
|
96
|
+
const name = project.name || projectRoot;
|
|
97
|
+
if (opts === null || opts === void 0 ? void 0 : opts.agent) {
|
|
98
|
+
const fields = [
|
|
99
|
+
"stale_embedding",
|
|
100
|
+
`project=${name}`,
|
|
101
|
+
`indexed_model=${gap.fromModel}`,
|
|
102
|
+
`current_model=${gap.toModel}`,
|
|
103
|
+
`indexed_dim=${gap.fromDim}`,
|
|
104
|
+
`current_dim=${gap.toDim}`,
|
|
105
|
+
`dim_changed=${gap.dimChanged}`,
|
|
106
|
+
`severity=${gap.severity}`,
|
|
107
|
+
"fix=gmax index --reset",
|
|
108
|
+
].join("\t");
|
|
109
|
+
process.stderr.write(`${fields}\n`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const label = gap.severity === "breaking" ? "WARN" : "hint";
|
|
113
|
+
const detail = gap.dimChanged
|
|
114
|
+
? `vector dim ${gap.fromDim}→${gap.toDim} (incompatible — scores invalid until re-embed)`
|
|
115
|
+
: `model '${gap.fromModel}'→'${gap.toModel}' (same dim; results mix models until re-embed)`;
|
|
116
|
+
process.stderr.write(`${label} gmax: '${name}' indexed with embedding ${detail}. Run 'gmax index --reset'. (silence: GMAX_NO_STALE_HINT=1)\n`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Cross-project guard: when a `--all-projects` / `--projects` search spans
|
|
120
|
+
* projects whose stored vector widths disagree (with each other or with the
|
|
121
|
+
* current query width), the shared fixed-dim table cannot answer all of them
|
|
122
|
+
* correctly — mismatched vectors were padded/truncated at insert and score as
|
|
123
|
+
* noise. Warn (stderr) so the caller can re-embed or exclude them, rather than
|
|
124
|
+
* silently returning invalid cross-project rankings.
|
|
125
|
+
*/
|
|
126
|
+
function maybeWarnCrossProjectDim(roots, opts) {
|
|
127
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
128
|
+
if (crossDimEmitted)
|
|
129
|
+
return;
|
|
130
|
+
if (process.env.GMAX_NO_STALE_HINT === "1")
|
|
131
|
+
return;
|
|
132
|
+
if (!roots || roots.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const current = (0, index_config_1.readGlobalConfig)();
|
|
135
|
+
const currentDim = (_d = (_a = current.vectorDim) !== null && _a !== void 0 ? _a : (_c = config_1.MODEL_TIERS[(_b = current.modelTier) !== null && _b !== void 0 ? _b : config_1.DEFAULT_MODEL_TIER]) === null || _c === void 0 ? void 0 : _c.vectorDim) !== null && _d !== void 0 ? _d : config_1.CONFIG.VECTOR_DIM;
|
|
136
|
+
const byRoot = new Map((0, project_registry_1.listProjects)().map((p) => [p.root, p]));
|
|
137
|
+
const dims = new Set([currentDim]);
|
|
138
|
+
const mismatched = [];
|
|
139
|
+
for (const r of roots) {
|
|
140
|
+
const p = byRoot.get(r.root);
|
|
141
|
+
if (!p)
|
|
142
|
+
continue;
|
|
143
|
+
const dim = (_g = (_e = p.vectorDim) !== null && _e !== void 0 ? _e : (_f = config_1.MODEL_TIERS[p.modelTier]) === null || _f === void 0 ? void 0 : _f.vectorDim) !== null && _g !== void 0 ? _g : currentDim;
|
|
144
|
+
dims.add(dim);
|
|
145
|
+
if (dim !== currentDim)
|
|
146
|
+
mismatched.push({ name: p.name || r.name, dim });
|
|
147
|
+
}
|
|
148
|
+
// All in-scope projects share the query width → nothing to warn about.
|
|
149
|
+
if (dims.size <= 1)
|
|
150
|
+
return;
|
|
151
|
+
crossDimEmitted = true;
|
|
152
|
+
if (opts === null || opts === void 0 ? void 0 : opts.agent) {
|
|
153
|
+
const fields = [
|
|
154
|
+
"stale_embedding_crossdim",
|
|
155
|
+
`query_dim=${currentDim}`,
|
|
156
|
+
`mismatched=${mismatched.map((m) => `${m.name}:${m.dim}`).join(",")}`,
|
|
157
|
+
"fix=re-embed or --exclude-projects",
|
|
158
|
+
].join("\t");
|
|
159
|
+
process.stderr.write(`${fields}\n`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const list = mismatched.map((m) => `${m.name} (${m.dim}d)`).join(", ");
|
|
163
|
+
process.stderr.write(`WARN gmax: cross-project search mixes embedding dims (query ${currentDim}d) — ${list} return invalid scores. Re-embed them or drop with --exclude-projects. (silence: GMAX_NO_STALE_HINT=1)\n`);
|
|
164
|
+
}
|