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/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. v2: sub-chunk symbol lists scoped to the chunk's
81
- // own content (previously every split sub-chunk inherited the parent's
82
- // full defined/referenced symbol lists, fabricating graph edges).
83
- CHUNKER_VERSION: 2,
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
+ }