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
package/dist/config.js
CHANGED
|
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.INDEXABLE_EXTENSIONS = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
|
|
36
|
+
exports.INDEXABLE_EXTENSIONS = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
|
|
37
|
+
exports.describeChunkerGap = describeChunkerGap;
|
|
38
|
+
exports.describeEmbeddingGap = describeEmbeddingGap;
|
|
37
39
|
const os = __importStar(require("node:os"));
|
|
38
40
|
const path = __importStar(require("node:path"));
|
|
39
41
|
exports.MODEL_TIERS = {
|
|
@@ -77,11 +79,84 @@ exports.CONFIG = {
|
|
|
77
79
|
WORKER_THREADS: DEFAULT_WORKER_THREADS,
|
|
78
80
|
QUERY_PREFIX: "",
|
|
79
81
|
// Bump when chunk metadata semantics change in a way that requires a full
|
|
80
|
-
// reindex to take effect.
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
CHUNKER_VERSION:
|
|
82
|
+
// reindex to take effect. Must equal the latest entry's `v` in
|
|
83
|
+
// CHUNKER_VERSION_HISTORY below — see that list for per-version severity and
|
|
84
|
+
// the user-facing note rendered by `gmax doctor` and the staleness hint.
|
|
85
|
+
CHUNKER_VERSION: 3,
|
|
84
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Per-version record of what changed in the chunker and how much it matters to
|
|
89
|
+
* an already-built index. `severity` drives tone: an "additive" change only
|
|
90
|
+
* adds new metadata (older indexes under-cover but aren't wrong), while a
|
|
91
|
+
* "breaking" change means older indexes carry incorrect metadata until a
|
|
92
|
+
* reindex. The `note` is shown verbatim to the user for the versions their
|
|
93
|
+
* index is missing. CONFIG.CHUNKER_VERSION must equal the highest `v` here.
|
|
94
|
+
*/
|
|
95
|
+
exports.CHUNKER_VERSION_HISTORY = [
|
|
96
|
+
{
|
|
97
|
+
v: 2,
|
|
98
|
+
severity: "breaking",
|
|
99
|
+
note: "sub-chunk symbol scoping; graph overcounted callers before this.",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
v: 3,
|
|
103
|
+
severity: "additive",
|
|
104
|
+
note: "type-position edges; dead/trace miss type-only callers until reindex.",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Describe the gap between an index's stamped chunker version and the current
|
|
109
|
+
* one, or null when the index is already current. Shared by `gmax doctor` and
|
|
110
|
+
* the query-time staleness hint so both render the same severity + notes.
|
|
111
|
+
*/
|
|
112
|
+
function describeChunkerGap(indexedVersion) {
|
|
113
|
+
const fromVersion = indexedVersion !== null && indexedVersion !== void 0 ? indexedVersion : 1;
|
|
114
|
+
if (fromVersion >= exports.CONFIG.CHUNKER_VERSION)
|
|
115
|
+
return null;
|
|
116
|
+
const missed = exports.CHUNKER_VERSION_HISTORY.filter((h) => h.v > fromVersion && h.v <= exports.CONFIG.CHUNKER_VERSION);
|
|
117
|
+
const severity = missed.some((h) => h.severity === "breaking")
|
|
118
|
+
? "breaking"
|
|
119
|
+
: "additive";
|
|
120
|
+
return {
|
|
121
|
+
fromVersion,
|
|
122
|
+
toVersion: exports.CONFIG.CHUNKER_VERSION,
|
|
123
|
+
severity,
|
|
124
|
+
notes: missed.map((h) => h.note),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Describe the gap between an index's stored embedding identity (model tier +
|
|
129
|
+
* vector dim) and the current global config, or null when they already agree.
|
|
130
|
+
*
|
|
131
|
+
* A dimension change is `breaking`: vectors of differing widths cannot be
|
|
132
|
+
* compared, and the single fixed-dim `chunks` table silently pads/truncates a
|
|
133
|
+
* mismatched query embedding, yielding meaningless scores. A same-width model
|
|
134
|
+
* swap is `additive`: the stored vectors are a different model's output but the
|
|
135
|
+
* same width, so search keeps working with mixed-model quality until a re-embed.
|
|
136
|
+
*
|
|
137
|
+
* Mirrors describeChunkerGap so `gmax doctor` and the query-time hint render the
|
|
138
|
+
* same severity. Pure over MODEL_TIERS — callers pass the current identity (read
|
|
139
|
+
* from the global config) so this module stays free of config I/O.
|
|
140
|
+
*/
|
|
141
|
+
function describeEmbeddingGap(stored, current) {
|
|
142
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
143
|
+
const fromModel = (_a = stored.modelTier) !== null && _a !== void 0 ? _a : exports.DEFAULT_MODEL_TIER;
|
|
144
|
+
const toModel = (_b = current.modelTier) !== null && _b !== void 0 ? _b : exports.DEFAULT_MODEL_TIER;
|
|
145
|
+
const fromDim = (_e = (_c = stored.vectorDim) !== null && _c !== void 0 ? _c : (_d = exports.MODEL_TIERS[fromModel]) === null || _d === void 0 ? void 0 : _d.vectorDim) !== null && _e !== void 0 ? _e : exports.CONFIG.VECTOR_DIM;
|
|
146
|
+
const toDim = (_h = (_f = current.vectorDim) !== null && _f !== void 0 ? _f : (_g = exports.MODEL_TIERS[toModel]) === null || _g === void 0 ? void 0 : _g.vectorDim) !== null && _h !== void 0 ? _h : exports.CONFIG.VECTOR_DIM;
|
|
147
|
+
const dimChanged = fromDim !== toDim;
|
|
148
|
+
const modelChanged = fromModel !== toModel;
|
|
149
|
+
if (!dimChanged && !modelChanged)
|
|
150
|
+
return null;
|
|
151
|
+
return {
|
|
152
|
+
fromModel,
|
|
153
|
+
toModel,
|
|
154
|
+
fromDim,
|
|
155
|
+
toDim,
|
|
156
|
+
dimChanged,
|
|
157
|
+
severity: dimChanged ? "breaking" : "additive",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
85
160
|
exports.WORKER_TIMEOUT_MS = Number.parseInt(process.env.GMAX_WORKER_TIMEOUT_MS || "60000", 10);
|
|
86
161
|
exports.WORKER_BOOT_TIMEOUT_MS = Number.parseInt(process.env.GMAX_WORKER_BOOT_TIMEOUT_MS || "300000", 10);
|
|
87
162
|
exports.MAX_WORKER_MEMORY_MB = (() => {
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Navigation-precision fixture — the measurement gate for the type-position
|
|
4
|
+
* reference cluster (chunker type-position edges, Phase 10 graph-distance,
|
|
5
|
+
* `gmax dead` precision). Both standing plans defer that cluster behind one
|
|
6
|
+
* unmet condition: a `trace --inbound` / `dead` truth set that EXISTS and
|
|
7
|
+
* MOVES. This is that instrument.
|
|
8
|
+
*
|
|
9
|
+
* WHAT IT MEASURES. The call graph is built from tree-sitter call-expression
|
|
10
|
+
* captures, so `referenced_symbols` holds call/construct sites (`foo()`,
|
|
11
|
+
* `new Foo()`) but NOT type-position uses (`: Foo`, `<Foo>`, `extends Foo`,
|
|
12
|
+
* `x as Foo`). `gmax trace --inbound Foo` and `gmax dead Foo` both read that
|
|
13
|
+
* graph, so a type used purely in annotations looks like it has no callers —
|
|
14
|
+
* trace under-reports, dead false-positives. This harness quantifies the gap by
|
|
15
|
+
* contrast: call-position recall (the baseline the graph achieves on the shapes
|
|
16
|
+
* it does capture) vs type-position recall (≈0 today).
|
|
17
|
+
*
|
|
18
|
+
* IT IS A GAUGE, NOT A PASS/FAIL GATE. Call-position recall is already <100%
|
|
19
|
+
* (chunk rollup, uncaptured shapes) — that is the status quo, not a regression,
|
|
20
|
+
* so the run always exits 0. The load-bearing number is type-position recall:
|
|
21
|
+
* low today = standing evidence to build chunker type-position edges; re-run
|
|
22
|
+
* after building them to confirm it moved. (For a true regression gate on the
|
|
23
|
+
* call graph, see the caller-count guard in eval-graph-sanity.ts.)
|
|
24
|
+
*
|
|
25
|
+
* HOW IT STAYS HONEST (no rot). Ground truth is derived live: for each symbol,
|
|
26
|
+
* `git grep` every reference and classify each file as a call-position caller or
|
|
27
|
+
* a type-position-only caller; `getCallers` is the measured set. The only
|
|
28
|
+
* hand-annotation is each symbol's character (callable vs type-only). The
|
|
29
|
+
* harness excludes its own file from both grep and the graph so listing the
|
|
30
|
+
* fixture symbols here can't contaminate the measurement. Run after a reindex.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* npx tsx src/eval-graph-nav.ts # table output
|
|
34
|
+
* npx tsx src/eval-graph-nav.ts --json # machine-readable
|
|
35
|
+
*/
|
|
36
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
37
|
+
if (k2 === undefined) k2 = k;
|
|
38
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
39
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
40
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
41
|
+
}
|
|
42
|
+
Object.defineProperty(o, k2, desc);
|
|
43
|
+
}) : (function(o, m, k, k2) {
|
|
44
|
+
if (k2 === undefined) k2 = k;
|
|
45
|
+
o[k2] = m[k];
|
|
46
|
+
}));
|
|
47
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
48
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
49
|
+
}) : function(o, v) {
|
|
50
|
+
o["default"] = v;
|
|
51
|
+
});
|
|
52
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
53
|
+
var ownKeys = function(o) {
|
|
54
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
55
|
+
var ar = [];
|
|
56
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
57
|
+
return ar;
|
|
58
|
+
};
|
|
59
|
+
return ownKeys(o);
|
|
60
|
+
};
|
|
61
|
+
return function (mod) {
|
|
62
|
+
if (mod && mod.__esModule) return mod;
|
|
63
|
+
var result = {};
|
|
64
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
65
|
+
__setModuleDefault(result, mod);
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
})();
|
|
69
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
70
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
71
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
72
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
73
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
74
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
75
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
var _a;
|
|
79
|
+
var _b;
|
|
80
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81
|
+
(_a = (_b = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_b.GMAX_WORKER_COUNT = "1");
|
|
82
|
+
const node_child_process_1 = require("node:child_process");
|
|
83
|
+
const path = __importStar(require("node:path"));
|
|
84
|
+
const config_1 = require("./config");
|
|
85
|
+
const graph_builder_1 = require("./lib/graph/graph-builder");
|
|
86
|
+
const vector_db_1 = require("./lib/store/vector-db");
|
|
87
|
+
const exit_1 = require("./lib/utils/exit");
|
|
88
|
+
const filter_builder_1 = require("./lib/utils/filter-builder");
|
|
89
|
+
const GMAX_ROOT = path.resolve(__dirname, "..");
|
|
90
|
+
const SELF = "src/eval-graph-nav.ts"; // exclude from grep + graph (self-reference)
|
|
91
|
+
// Files that embed the fixture symbols as *string* data (template-literal
|
|
92
|
+
// source, `.toContain("…")` assertions) rather than real type/call references.
|
|
93
|
+
// grep can't tell those apart from genuine uses, so they would manufacture
|
|
94
|
+
// phantom "expected callers" that no correct graph ever surfaces. Excluded from
|
|
95
|
+
// both the grep truth and the getCallers set so the exclusion stays symmetric.
|
|
96
|
+
const FIXTURE_FILES = new Set([SELF, "tests/graph-edges.type-position.test.ts"]);
|
|
97
|
+
const rel = (p) => p.replace(`${GMAX_ROOT}/`, "");
|
|
98
|
+
const INBOUND_SYMBOLS = [
|
|
99
|
+
// ── Baselines: called/constructed, so the graph captures them. ──────────────
|
|
100
|
+
{
|
|
101
|
+
symbol: "getWorkerPool",
|
|
102
|
+
character: "callable",
|
|
103
|
+
note: "plain function call sites",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
symbol: "GraphBuilder",
|
|
107
|
+
character: "callable",
|
|
108
|
+
note: "class via `new GraphBuilder(...)`",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
symbol: "withQueryTimeout",
|
|
112
|
+
character: "callable",
|
|
113
|
+
note: "wrapper function call sites",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
symbol: "isFileCached",
|
|
117
|
+
character: "callable",
|
|
118
|
+
note: "predicate function call sites",
|
|
119
|
+
},
|
|
120
|
+
// ── The gap: interfaces / type aliases used only in `: T` / `<T>` position. ──
|
|
121
|
+
{
|
|
122
|
+
symbol: "SearchResponse",
|
|
123
|
+
character: "type-only",
|
|
124
|
+
note: "return/param annotation across search + eval",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
symbol: "VectorRecord",
|
|
128
|
+
character: "type-only",
|
|
129
|
+
note: "row interface annotated across index/store/worker",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
symbol: "GraphNode",
|
|
133
|
+
character: "type-only",
|
|
134
|
+
note: "graph row interface in dead/formatter consumers",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
symbol: "NeighborHit",
|
|
138
|
+
character: "type-only",
|
|
139
|
+
note: "traversal interface, type position only",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
symbol: "CallerTree",
|
|
143
|
+
character: "type-only",
|
|
144
|
+
note: "recursive caller interface, type position only",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
symbol: "EdgeDirection",
|
|
148
|
+
character: "type-only",
|
|
149
|
+
note: "string-literal type alias, pure type position",
|
|
150
|
+
},
|
|
151
|
+
// ── Python (mlx-embed-server/server.py): proves Shape 6 closes the gap in a
|
|
152
|
+
// grammar with no `type_identifier` node. ─────────────────────────────────
|
|
153
|
+
{
|
|
154
|
+
symbol: "EmbedResponse",
|
|
155
|
+
character: "callable",
|
|
156
|
+
note: "Python: Pydantic model via `EmbedResponse(...)` construct site",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
symbol: "EmbedRequest",
|
|
160
|
+
character: "type-only",
|
|
161
|
+
note: "Python: Pydantic model in `request: EmbedRequest` annotation only",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
// Dead-precision truth is derived (LIVE iff grep finds any use beyond def/import).
|
|
165
|
+
const DEAD_SYMBOLS = [
|
|
166
|
+
"getWorkerPool", // LIVE baseline (call users) — dead must read LIVE
|
|
167
|
+
"GraphBuilder", // LIVE baseline (construct users)
|
|
168
|
+
"DeadResult", // non-exported, type-only users → today DEAD (false positive)
|
|
169
|
+
"EdgeDirection", // exported, type-only users → today PUBLIC_EXPORT (masked)
|
|
170
|
+
"ResolvedCaller", // exported, type-only users → today PUBLIC_EXPORT (masked)
|
|
171
|
+
"EmbedRequest", // Python: annotation-only Pydantic model — the canonical false-dead, now LIVE via Shape 6
|
|
172
|
+
];
|
|
173
|
+
function classify(symbol, text) {
|
|
174
|
+
const s = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
175
|
+
if (/\bimport\b/.test(text))
|
|
176
|
+
return "import";
|
|
177
|
+
if (new RegExp(`\\b(interface|type|class|function|enum)\\s+${s}\\b`).test(text))
|
|
178
|
+
return "def";
|
|
179
|
+
if (new RegExp(`\\b(const|let|var)\\s+${s}\\b\\s*[:=]`).test(text))
|
|
180
|
+
return "def";
|
|
181
|
+
if (new RegExp(`(\\bnew\\s+${s}\\b|\\b${s}\\s*\\()`).test(text))
|
|
182
|
+
return "call";
|
|
183
|
+
if (new RegExp(`([:<,|&(]\\s*${s}\\b|\\bextends\\s+${s}\\b|\\bimplements\\s+${s}\\b|\\bas\\s+${s}\\b|\\b${s}\\s*[[<])`).test(text))
|
|
184
|
+
return "type";
|
|
185
|
+
return "other";
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Live ground truth for `symbol`: files that reference it from a call position
|
|
189
|
+
* vs a type-only position. Definition-only / import-only files are not callers.
|
|
190
|
+
* A file with any call site counts as a call caller even if it also uses the
|
|
191
|
+
* symbol as a type (the graph should find it via the call). The harness's own
|
|
192
|
+
* file is excluded so the fixture list can't contaminate the truth.
|
|
193
|
+
*/
|
|
194
|
+
function referenceTruth(symbol) {
|
|
195
|
+
let raw;
|
|
196
|
+
try {
|
|
197
|
+
raw = (0, node_child_process_1.execFileSync)("git", [
|
|
198
|
+
"grep",
|
|
199
|
+
"-nwF",
|
|
200
|
+
symbol,
|
|
201
|
+
"--",
|
|
202
|
+
"*.ts",
|
|
203
|
+
"*.py",
|
|
204
|
+
...[...FIXTURE_FILES].map((f) => `:!${f}`),
|
|
205
|
+
], { cwd: GMAX_ROOT, encoding: "utf-8" });
|
|
206
|
+
}
|
|
207
|
+
catch (_a) {
|
|
208
|
+
return { callFiles: new Set(), typeOnlyFiles: new Set() };
|
|
209
|
+
}
|
|
210
|
+
const callFiles = new Set();
|
|
211
|
+
const typeFiles = new Set();
|
|
212
|
+
for (const line of raw.split("\n")) {
|
|
213
|
+
if (!line)
|
|
214
|
+
continue;
|
|
215
|
+
const parts = line.split(":");
|
|
216
|
+
const file = parts[0];
|
|
217
|
+
const text = parts.slice(2).join(":");
|
|
218
|
+
const kind = classify(symbol, text);
|
|
219
|
+
if (kind === "call")
|
|
220
|
+
callFiles.add(file);
|
|
221
|
+
else if (kind === "type")
|
|
222
|
+
typeFiles.add(file);
|
|
223
|
+
}
|
|
224
|
+
const typeOnlyFiles = new Set([...typeFiles].filter((f) => !callFiles.has(f)));
|
|
225
|
+
return { callFiles, typeOnlyFiles };
|
|
226
|
+
}
|
|
227
|
+
/** getCallers file set, excluding the harness's own (possibly stale) chunks. */
|
|
228
|
+
function callerFiles(builder, symbol) {
|
|
229
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
230
|
+
const callers = yield builder.getCallers(symbol);
|
|
231
|
+
return new Set(callers.map((g) => rel(g.file)).filter((f) => !FIXTURE_FILES.has(f)));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function recall(found, expected) {
|
|
235
|
+
let hit = 0;
|
|
236
|
+
for (const f of expected)
|
|
237
|
+
if (found.has(f))
|
|
238
|
+
hit++;
|
|
239
|
+
return { hit, total: expected.size };
|
|
240
|
+
}
|
|
241
|
+
function runInbound(builder) {
|
|
242
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const c of INBOUND_SYMBOLS) {
|
|
245
|
+
const { callFiles, typeOnlyFiles } = referenceTruth(c.symbol);
|
|
246
|
+
const found = yield callerFiles(builder, c.symbol);
|
|
247
|
+
out.push({
|
|
248
|
+
symbol: c.symbol,
|
|
249
|
+
character: c.character,
|
|
250
|
+
call: recall(found, callFiles),
|
|
251
|
+
type: recall(found, typeOnlyFiles),
|
|
252
|
+
missedType: [...typeOnlyFiles].filter((f) => !found.has(f)),
|
|
253
|
+
note: c.note,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function runDead(db, builder) {
|
|
260
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
const table = yield db.ensureTable();
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const symbol of DEAD_SYMBOLS) {
|
|
264
|
+
const { callFiles, typeOnlyFiles } = referenceTruth(symbol);
|
|
265
|
+
const trueLive = callFiles.size + typeOnlyFiles.size > 0;
|
|
266
|
+
const defRows = yield table
|
|
267
|
+
.query()
|
|
268
|
+
.select(["is_exported"])
|
|
269
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}')`)
|
|
270
|
+
.limit(1)
|
|
271
|
+
.toArray();
|
|
272
|
+
const found = yield callerFiles(builder, symbol);
|
|
273
|
+
let status;
|
|
274
|
+
if (defRows.length === 0)
|
|
275
|
+
status = "NO_DEF";
|
|
276
|
+
else if (found.size > 0)
|
|
277
|
+
status = "LIVE";
|
|
278
|
+
else
|
|
279
|
+
status = defRows[0].is_exported ? "PUBLIC_EXPORT" : "DEAD";
|
|
280
|
+
let verdict;
|
|
281
|
+
if (trueLive && status === "LIVE")
|
|
282
|
+
verdict = "correct";
|
|
283
|
+
else if (!trueLive && status === "DEAD")
|
|
284
|
+
verdict = "correct";
|
|
285
|
+
else if (trueLive && status === "DEAD")
|
|
286
|
+
verdict = "false-dead";
|
|
287
|
+
else if (trueLive && status === "PUBLIC_EXPORT")
|
|
288
|
+
verdict = "masked-by-export";
|
|
289
|
+
else
|
|
290
|
+
verdict = "over-live";
|
|
291
|
+
out.push({ symbol, trueLive, status, verdict });
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function pool(rows) {
|
|
297
|
+
const hit = rows.reduce((a, r) => a + r.hit, 0);
|
|
298
|
+
const total = rows.reduce((a, r) => a + r.total, 0);
|
|
299
|
+
return { hit, total, pct: total === 0 ? 0 : (100 * hit) / total };
|
|
300
|
+
}
|
|
301
|
+
function main() {
|
|
302
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
303
|
+
const jsonMode = process.argv.includes("--json") || process.env.GMAX_EVAL_JSON === "1";
|
|
304
|
+
const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
305
|
+
const builder = new graph_builder_1.GraphBuilder(db, GMAX_ROOT);
|
|
306
|
+
const inbound = yield runInbound(builder);
|
|
307
|
+
const dead = yield runDead(db, builder);
|
|
308
|
+
yield db.close();
|
|
309
|
+
const callBaseline = pool(inbound.filter((r) => r.character === "callable").map((r) => r.call));
|
|
310
|
+
const typeGap = pool(inbound.filter((r) => r.character === "type-only").map((r) => r.type));
|
|
311
|
+
const falseDead = dead.filter((r) => r.verdict === "false-dead").length;
|
|
312
|
+
const maskedByExport = dead.filter((r) => r.verdict === "masked-by-export").length;
|
|
313
|
+
const summary = {
|
|
314
|
+
callPositionRecall: callBaseline,
|
|
315
|
+
typePositionRecall: typeGap,
|
|
316
|
+
deadFalsePositives: falseDead,
|
|
317
|
+
deadMaskedByExport: maskedByExport,
|
|
318
|
+
};
|
|
319
|
+
if (jsonMode) {
|
|
320
|
+
process.stdout.write(`${JSON.stringify({ summary, inbound, dead }, null, 2)}\n`);
|
|
321
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const pct = (r) => r.total === 0
|
|
325
|
+
? " n/a"
|
|
326
|
+
: `${Math.round((100 * r.hit) / r.total)}%`.padStart(4);
|
|
327
|
+
console.log(`Navigation-precision fixture (trace --inbound / dead)`);
|
|
328
|
+
console.log(`root: ${GMAX_ROOT}\n`);
|
|
329
|
+
console.log(`Inbound caller recall (getCallers vs grep truth, by reference position):`);
|
|
330
|
+
for (const r of inbound) {
|
|
331
|
+
const tag = r.character === "callable" ? "callable " : "type-only";
|
|
332
|
+
console.log(` ${r.symbol.padEnd(16)} [${tag}] ` +
|
|
333
|
+
`call ${pct(r.call)} (${String(r.call.hit).padStart(2)}/${String(r.call.total).padStart(2)}) ` +
|
|
334
|
+
`type ${pct(r.type)} (${String(r.type.hit).padStart(2)}/${String(r.type.total).padStart(2)})` +
|
|
335
|
+
(r.character === "type-only" && r.missedType.length
|
|
336
|
+
? ` misses: ${r.missedType.slice(0, 3).join(", ")}${r.missedType.length > 3 ? ", …" : ""}`
|
|
337
|
+
: ""));
|
|
338
|
+
}
|
|
339
|
+
console.log(`\nDead-code precision (truth derived from grep):`);
|
|
340
|
+
for (const r of dead) {
|
|
341
|
+
const flag = r.verdict === "false-dead"
|
|
342
|
+
? " ← FALSE POSITIVE (really LIVE)"
|
|
343
|
+
: r.verdict === "masked-by-export"
|
|
344
|
+
? " ← type-only users hidden behind PUBLIC EXPORT"
|
|
345
|
+
: "";
|
|
346
|
+
console.log(` ${r.symbol.padEnd(16)} truth=${(r.trueLive ? "LIVE" : "DEAD").padEnd(4)} reported=${r.status.padEnd(13)}${flag}`);
|
|
347
|
+
}
|
|
348
|
+
console.log(`\nHeadline` +
|
|
349
|
+
`\n call-position recall (baseline): ${callBaseline.hit}/${callBaseline.total} (${Math.round(callBaseline.pct)}%)` +
|
|
350
|
+
`\n type-position recall (the gap): ${typeGap.hit}/${typeGap.total} (${Math.round(typeGap.pct)}%)` +
|
|
351
|
+
`\n dead false-positives: ${falseDead} · dead masked-by-export: ${maskedByExport}`);
|
|
352
|
+
const delta = Math.round(callBaseline.pct - typeGap.pct);
|
|
353
|
+
if (callBaseline.pct < 40) {
|
|
354
|
+
console.log(`\nVerdict: INCONCLUSIVE — call-position baseline is unexpectedly low (${Math.round(callBaseline.pct)}%); ` +
|
|
355
|
+
`the index may be stale. Reindex (gmax index) and re-run.`);
|
|
356
|
+
}
|
|
357
|
+
else if (typeGap.pct < 50) {
|
|
358
|
+
console.log(`\nVerdict: GAP OPEN (${delta} pts) — navigation recovers call sites at ${Math.round(callBaseline.pct)}% ` +
|
|
359
|
+
`but type-position references at only ${Math.round(typeGap.pct)}%. Standing evidence to build chunker ` +
|
|
360
|
+
`type-position edges; re-run after to confirm the gap closes.`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
console.log(`\nVerdict: GAP CLOSING — type-position recall is up to ${Math.round(typeGap.pct)}% ` +
|
|
364
|
+
`(call baseline ${Math.round(callBaseline.pct)}%).`);
|
|
365
|
+
}
|
|
366
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (require.main === module) {
|
|
370
|
+
main().catch((e) => {
|
|
371
|
+
console.error(e);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
});
|
|
374
|
+
}
|