grepmax 0.17.2 → 0.17.4

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.
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.computePageRank = computePageRank;
46
+ exports.buildGraphFromDb = buildGraphFromDb;
47
+ exports.writeDiskCache = writeDiskCache;
48
+ exports.loadOrComputePageRank = loadOrComputePageRank;
49
+ exports.pageRankBoostForSymbols = pageRankBoostForSymbols;
50
+ exports._clearMemoryCacheForTests = _clearMemoryCacheForTests;
51
+ exports._cachePathForTests = _cachePathForTests;
52
+ const crypto = __importStar(require("node:crypto"));
53
+ const fs = __importStar(require("node:fs"));
54
+ const path = __importStar(require("node:path"));
55
+ const config_1 = require("../../config");
56
+ const filter_builder_1 = require("../utils/filter-builder");
57
+ const DEFAULT_DAMPING = 0.85;
58
+ const DEFAULT_MAX_ITER = 50;
59
+ const DEFAULT_TOL = 1e-6;
60
+ const DEFAULT_TTL_MS = 60 * 60 * 1000;
61
+ const memoryCache = new Map();
62
+ function computePageRank(graph, damping = DEFAULT_DAMPING, maxIter = DEFAULT_MAX_ITER, tol = DEFAULT_TOL) {
63
+ const N = graph.nodes.length;
64
+ const result = new Map();
65
+ if (N === 0)
66
+ return result;
67
+ const idx = new Map();
68
+ for (let i = 0; i < N; i++)
69
+ idx.set(graph.nodes[i], i);
70
+ const outNeighbors = Array.from({ length: N }, () => []);
71
+ for (const [src, targets] of graph.edges) {
72
+ const si = idx.get(src);
73
+ if (si === undefined)
74
+ continue;
75
+ const seen = new Set();
76
+ for (const tgt of targets) {
77
+ const ti = idx.get(tgt);
78
+ if (ti === undefined || ti === si || seen.has(ti))
79
+ continue;
80
+ seen.add(ti);
81
+ outNeighbors[si].push(ti);
82
+ }
83
+ }
84
+ const outDegree = new Int32Array(N);
85
+ for (let i = 0; i < N; i++)
86
+ outDegree[i] = outNeighbors[i].length;
87
+ const inNeighbors = Array.from({ length: N }, () => []);
88
+ for (let i = 0; i < N; i++) {
89
+ for (const j of outNeighbors[i])
90
+ inNeighbors[j].push(i);
91
+ }
92
+ let pr = new Float64Array(N).fill(1 / N);
93
+ let next = new Float64Array(N);
94
+ const teleport = (1 - damping) / N;
95
+ for (let iter = 0; iter < maxIter; iter++) {
96
+ let dangling = 0;
97
+ for (let i = 0; i < N; i++) {
98
+ if (outDegree[i] === 0)
99
+ dangling += pr[i];
100
+ }
101
+ const danglingShare = (damping * dangling) / N;
102
+ for (let i = 0; i < N; i++) {
103
+ let sum = 0;
104
+ const ins = inNeighbors[i];
105
+ for (let k = 0; k < ins.length; k++) {
106
+ const j = ins[k];
107
+ sum += pr[j] / outDegree[j];
108
+ }
109
+ next[i] = teleport + danglingShare + damping * sum;
110
+ }
111
+ let delta = 0;
112
+ for (let i = 0; i < N; i++)
113
+ delta += Math.abs(next[i] - pr[i]);
114
+ const tmp = pr;
115
+ pr = next;
116
+ next = tmp;
117
+ if (delta < tol)
118
+ break;
119
+ }
120
+ for (let i = 0; i < N; i++)
121
+ result.set(graph.nodes[i], pr[i]);
122
+ return result;
123
+ }
124
+ function toStringArray(val) {
125
+ if (!val)
126
+ return [];
127
+ if (Array.isArray(val))
128
+ return val.filter((v) => typeof v === "string");
129
+ const maybe = val;
130
+ if (typeof maybe.toArray === "function") {
131
+ try {
132
+ const arr = maybe.toArray();
133
+ return Array.isArray(arr)
134
+ ? arr.filter((v) => typeof v === "string")
135
+ : [];
136
+ }
137
+ catch (_a) {
138
+ return [];
139
+ }
140
+ }
141
+ return [];
142
+ }
143
+ function buildGraphFromDb(db, pathPrefix) {
144
+ return __awaiter(this, void 0, void 0, function* () {
145
+ const table = yield db.ensureTable();
146
+ const prefix = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
147
+ const rows = yield table
148
+ .query()
149
+ .select(["defined_symbols", "referenced_symbols"])
150
+ .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
151
+ .toArray();
152
+ const nodes = new Set();
153
+ const edges = new Map();
154
+ for (const row of rows) {
155
+ const defs = toStringArray(row.defined_symbols);
156
+ const refs = toStringArray(row.referenced_symbols);
157
+ for (const d of defs)
158
+ nodes.add(d);
159
+ if (refs.length === 0)
160
+ continue;
161
+ for (const d of defs) {
162
+ let set = edges.get(d);
163
+ if (!set) {
164
+ set = new Set();
165
+ edges.set(d, set);
166
+ }
167
+ for (const r of refs)
168
+ set.add(r);
169
+ }
170
+ }
171
+ return { nodes: Array.from(nodes), edges };
172
+ });
173
+ }
174
+ function cachePathFor(pathPrefix) {
175
+ const hash = crypto
176
+ .createHash("sha1")
177
+ .update(pathPrefix)
178
+ .digest("hex")
179
+ .slice(0, 16);
180
+ return path.join(config_1.PATHS.globalRoot, "pagerank", `${hash}.json`);
181
+ }
182
+ function getTtlMs() {
183
+ var _a;
184
+ const env = Number.parseInt((_a = process.env.GMAX_PAGERANK_TTL_MS) !== null && _a !== void 0 ? _a : "", 10);
185
+ return Number.isFinite(env) && env > 0 ? env : DEFAULT_TTL_MS;
186
+ }
187
+ function readDiskCache(pathPrefix) {
188
+ const file = cachePathFor(pathPrefix);
189
+ if (!fs.existsSync(file))
190
+ return null;
191
+ try {
192
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
193
+ const computedAt = Date.parse(data.computedAt);
194
+ if (!Number.isFinite(computedAt))
195
+ return null;
196
+ if (Date.now() - computedAt > getTtlMs())
197
+ return null;
198
+ const scores = new Map();
199
+ let max = 0;
200
+ for (const [k, v] of Object.entries(data.scores)) {
201
+ const n = Number(v);
202
+ if (!Number.isFinite(n))
203
+ continue;
204
+ scores.set(k, n);
205
+ if (n > max)
206
+ max = n;
207
+ }
208
+ return { scores, max, computedAt };
209
+ }
210
+ catch (_a) {
211
+ return null;
212
+ }
213
+ }
214
+ function writeDiskCache(pathPrefix, scores) {
215
+ const file = cachePathFor(pathPrefix);
216
+ fs.mkdirSync(path.dirname(file), { recursive: true });
217
+ const obj = {
218
+ pathPrefix,
219
+ computedAt: new Date().toISOString(),
220
+ nodeCount: scores.size,
221
+ scores: Object.fromEntries(scores),
222
+ };
223
+ fs.writeFileSync(file, JSON.stringify(obj), "utf8");
224
+ }
225
+ function loadOrComputePageRank(db, pathPrefix) {
226
+ return __awaiter(this, void 0, void 0, function* () {
227
+ const mem = memoryCache.get(pathPrefix);
228
+ if (mem && Date.now() - mem.computedAt < getTtlMs()) {
229
+ return { scores: mem.scores, max: mem.max };
230
+ }
231
+ const disk = readDiskCache(pathPrefix);
232
+ if (disk) {
233
+ memoryCache.set(pathPrefix, disk);
234
+ return { scores: disk.scores, max: disk.max };
235
+ }
236
+ const graph = yield buildGraphFromDb(db, pathPrefix);
237
+ const scores = computePageRank(graph);
238
+ let max = 0;
239
+ for (const v of scores.values())
240
+ if (v > max)
241
+ max = v;
242
+ const entry = { scores, max, computedAt: Date.now() };
243
+ memoryCache.set(pathPrefix, entry);
244
+ try {
245
+ writeDiskCache(pathPrefix, scores);
246
+ }
247
+ catch (_a) { }
248
+ return { scores, max };
249
+ });
250
+ }
251
+ function pageRankBoostForSymbols(symbols, scores, max) {
252
+ if (!symbols || symbols.length === 0 || max <= 0)
253
+ return 0;
254
+ let best = 0;
255
+ for (const s of symbols) {
256
+ const v = scores.get(s);
257
+ if (v !== undefined && v > best)
258
+ best = v;
259
+ }
260
+ return best / max;
261
+ }
262
+ function _clearMemoryCacheForTests() {
263
+ memoryCache.clear();
264
+ }
265
+ function _cachePathForTests(pathPrefix) {
266
+ return cachePathFor(pathPrefix);
267
+ }
@@ -15,6 +15,7 @@ const config_1 = require("../../config");
15
15
  const filter_builder_1 = require("../utils/filter-builder");
16
16
  const pool_1 = require("../workers/pool");
17
17
  const intent_1 = require("./intent");
18
+ const pagerank_1 = require("./pagerank");
18
19
  function buildWhereClause(pathPrefix, filters, searchIntent) {
19
20
  var _a;
20
21
  const parts = [];
@@ -338,7 +339,7 @@ class Searcher {
338
339
  }
339
340
  search(query, top_k, _search_options, _filters, pathPrefix, intent, signal) {
340
341
  return __awaiter(this, void 0, void 0, function* () {
341
- var _a, _b, _c, _d, _e, _f, _g, _h;
342
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
342
343
  const finalLimit = top_k !== null && top_k !== void 0 ? top_k : 10;
343
344
  // ColBERT rerank is opt-in as of v0.17.1. On the 97-case eval it
344
345
  // regresses MRR@10 by ~3% and doubles query latency; sweep across
@@ -371,7 +372,7 @@ class Searcher {
371
372
  try {
372
373
  table = yield this.db.ensureTable();
373
374
  }
374
- catch (_j) {
375
+ catch (_k) {
375
376
  return { data: [] };
376
377
  }
377
378
  // Ensure FTS index exists (lazy init, retry periodically on failure)
@@ -391,10 +392,15 @@ class Searcher {
391
392
  }
392
393
  }
393
394
  // Phase A: Lightweight retrieval — only columns needed for RRF, cosine, boost, dedup
395
+ // PageRank tiebreaker needs defined_symbols for the per-chunk lookup; include
396
+ // it in the lightweight path only when the flag is on so we don't bloat the
397
+ // default query path.
398
+ const pagerankEnabled = process.env.GMAX_PAGERANK === "1" && !!pathPrefix;
394
399
  const LIGHTWEIGHT_COLUMNS = [
395
400
  "id", "path", "hash", "chunk_index", "start_line", "end_line",
396
401
  "is_anchor", "chunk_type", "role", "complexity", "is_exported",
397
402
  "content", "parent_symbol", "referenced_symbols", "pooled_colbert_48d",
403
+ ...(pagerankEnabled ? ["defined_symbols"] : []),
398
404
  ];
399
405
  // _distance is auto-added by vectorSearch, _score by FTS — include each
400
406
  // in the respective query to suppress LanceDB deprecation warnings
@@ -432,7 +438,7 @@ class Searcher {
432
438
  this.ftsAvailable = true;
433
439
  console.warn("[Searcher] Rebuilt FTS index with position support — retry search");
434
440
  }
435
- catch (_k) { }
441
+ catch (_l) { }
436
442
  }
437
443
  else {
438
444
  console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
@@ -571,6 +577,40 @@ class Searcher {
571
577
  : undefined,
572
578
  };
573
579
  });
580
+ // PageRank tiebreaker (opt-in via GMAX_PAGERANK=1). Small additive delta on
581
+ // top of the post-boost score, sourced from the per-project call graph. Read
582
+ // docs/plans/2026-05-25-semantic-search-landscape.md (Bundle B / G1) for the
583
+ // measurement criterion that decides whether this flag becomes default-on.
584
+ if (pagerankEnabled && pathPrefix) {
585
+ try {
586
+ const { scores: prScores, max: prMax } = yield (0, pagerank_1.loadOrComputePageRank)(this.db, pathPrefix);
587
+ if (prMax > 0) {
588
+ const envWeight = Number.parseFloat((_h = process.env.GMAX_PR_WEIGHT) !== null && _h !== void 0 ? _h : "");
589
+ const PR_WEIGHT = Number.isFinite(envWeight) && envWeight >= 0 ? envWeight : 0.05;
590
+ for (const item of scored) {
591
+ const raw = item.record.defined_symbols;
592
+ let defs = [];
593
+ if (Array.isArray(raw)) {
594
+ defs = raw.filter((v) => typeof v === "string");
595
+ }
596
+ else if (raw && typeof raw.toArray === "function") {
597
+ try {
598
+ const arr = raw.toArray();
599
+ if (Array.isArray(arr)) {
600
+ defs = arr.filter((v) => typeof v === "string");
601
+ }
602
+ }
603
+ catch (_m) { }
604
+ }
605
+ const norm = (0, pagerank_1.pageRankBoostForSymbols)(defs, prScores, prMax);
606
+ item.score += PR_WEIGHT * norm;
607
+ }
608
+ }
609
+ }
610
+ catch (e) {
611
+ console.warn(`[Searcher] PageRank tiebreaker failed: ${e}`);
612
+ }
613
+ }
574
614
  // Note: "boosted" was not previously declared -- fix to use "scored"
575
615
  scored.sort((a, b) => b.score - a.score);
576
616
  // Item 11: Intelligent Deduplication
@@ -578,7 +618,7 @@ class Searcher {
578
618
  // Item 10: Per-file diversification
579
619
  const seenFiles = new Map();
580
620
  const diversified = [];
581
- const envMaxPerFile = Number.parseInt((_h = process.env.GMAX_MAX_PER_FILE) !== null && _h !== void 0 ? _h : "", 10);
621
+ const envMaxPerFile = Number.parseInt((_j = process.env.GMAX_MAX_PER_FILE) !== null && _j !== void 0 ? _j : "", 10);
582
622
  const MAX_PER_FILE = Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3;
583
623
  for (const item of uniqueScored) {
584
624
  const path = item.record.path || "";
@@ -25,6 +25,30 @@ os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = "1"
25
25
  os.environ["HF_HUB_VERBOSITY"] = "error"
26
26
  os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
27
27
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
28
+
29
+ # Auto-enable offline mode when the model is already cached, so steady-state
30
+ # startups don't HEAD-check huggingface.co (annoying firewall prompts, slow
31
+ # starts on flaky networks). First run with an empty cache stays online so
32
+ # the model can be fetched. Force one or the other with GMAX_HF_ONLINE=1
33
+ # or GMAX_HF_OFFLINE=1.
34
+ def _hf_cache_has_model(model_id: str) -> bool:
35
+ hf_home = os.environ.get("HF_HOME") or os.path.expanduser(
36
+ os.path.join("~", ".cache", "huggingface")
37
+ )
38
+ cache_dir = os.path.join(
39
+ hf_home, "hub", "models--" + model_id.replace("/", "--")
40
+ )
41
+ return os.path.isdir(cache_dir) and bool(os.listdir(cache_dir))
42
+
43
+ _model_id_for_cache_check = os.environ.get(
44
+ "MLX_EMBED_MODEL", "ibm-granite/granite-embedding-small-english-r2"
45
+ )
46
+ if os.environ.get("GMAX_HF_OFFLINE") == "1" or (
47
+ os.environ.get("GMAX_HF_ONLINE") != "1"
48
+ and _hf_cache_has_model(_model_id_for_cache_check)
49
+ ):
50
+ os.environ["HF_HUB_OFFLINE"] = "1"
51
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
28
52
  warnings.filterwarnings("ignore", message=".*PyTorch.*")
29
53
  warnings.filterwarnings("ignore", message=".*resource_tracker.*")
30
54
  logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -28,6 +28,8 @@
28
28
  "benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts",
29
29
  "bench:recall": "npx tsx src/eval.ts",
30
30
  "bench:recall:json": "GMAX_EVAL_JSON=1 npx tsx src/eval.ts",
31
+ "bench:oss": "npx tsx src/eval-oss.ts all",
32
+ "bench:oss:json": "GMAX_EVAL_JSON=1 npx tsx src/eval-oss.ts all",
31
33
  "format": "biome check --write .",
32
34
  "format:check": "biome check .",
33
35
  "lint": "biome lint .",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -13,6 +13,7 @@ allowed-tools: "Bash(gmax:*), Read"
13
13
  - **Quick symbol overview?** → `Bash(gmax peek <symbol>)` (signature + callers + callees)
14
14
  - **Need file structure?** → `Bash(gmax skeleton <path>)`
15
15
  - **Need call flow?** → `Bash(gmax trace <symbol>)`
16
+ - **Is a symbol unused?** → `Bash(gmax dead <symbol>)` (call-graph check — hypothesis, not proof)
16
17
 
17
18
  ## Quick start
18
19
 
@@ -40,7 +41,7 @@ If search returns "This project hasn't been added to gmax yet", run `Bash(gmax a
40
41
 
41
42
  ### Search — `gmax "query" --agent`
42
43
 
43
- The `--agent` flag produces compact, token-efficient output for AI agents. It works on most commands — `search`, `peek`, `extract`, `trace`, `test`, `impact`, `similar`, `log`, `related`, `symbols`, `status`, `project`, `context`, `skeleton`, and `doctor`.
44
+ The `--agent` flag produces compact, token-efficient output for AI agents. It works on most commands — `search`, `peek`, `extract`, `trace`, `test`, `impact`, `similar`, `dead`, `log`, `related`, `symbols`, `status`, `project`, `context`, `skeleton`, and `doctor`.
44
45
 
45
46
  ```
46
47
  gmax "where do we handle authentication" --agent
@@ -177,6 +178,14 @@ gmax similar src/lib/auth.ts # files with similar structure
177
178
  gmax similar handleAuth -m 5 --agent # top 5, compact output
178
179
  ```
179
180
 
181
+ ### Dead — `gmax dead <symbol>`
182
+ ```
183
+ gmax dead handleAuth # zero-inbound-callers check via the call graph
184
+ gmax dead handleAuth --agent # TSV: status\tdef:line\tcaller_count\tcallers_top3
185
+ gmax dead handleAuth --in src/ # restrict to a sub-path
186
+ ```
187
+ Status is `DEAD` (no callers, not exported), `PUBLIC EXPORT` (no internal callers but the defining chunk is exported — check external usage), or `LIVE` (with caller count + top-3 file:line). The call graph reflects what tree-sitter chunked: dynamic dispatch, reflection, eval, and string-built call sites won't show up — `DEAD` is a hypothesis, not a proof.
188
+
180
189
  ### Context — `gmax context <topic> --budget <tokens>`
181
190
  ```
182
191
  gmax context "authentication system" --budget 4000
@@ -219,13 +228,14 @@ gmax llm on/off/start/stop/status # manage local LLM server
219
228
  10. **Test** — `Bash(gmax test <symbol>)` to find tests covering a symbol before editing
220
229
  11. **Impact** — `Bash(gmax impact <symbol>)` for blast radius before significant changes
221
230
  12. **Similar** — `Bash(gmax similar <symbol>)` to find similar patterns for DRY analysis
222
- 13. **Context** — `Bash(gmax context "topic" --budget 4000)` for a token-budgeted topic summary
223
- 14. **Related** — `Bash(gmax related <file>)` to see what else to look at
224
- 15. **Status** — `Bash(gmax status)` to check index state across all projects
231
+ 13. **Dead** — `Bash(gmax dead <symbol>)` to check if a symbol has zero inbound callers (hypothesis, not proof)
232
+ 14. **Context** — `Bash(gmax context "topic" --budget 4000)` for a token-budgeted topic summary
233
+ 15. **Related** — `Bash(gmax related <file>)` to see what else to look at
234
+ 16. **Status** — `Bash(gmax status)` to check index state across all projects
225
235
 
226
236
  ## Tips
227
237
 
228
- - **Use `--agent` for compact output** — works on most commands: search, peek, extract, trace, log, test, impact, similar, related, status, project, doctor.
238
+ - **Use `--agent` for compact output** — works on most commands: search, peek, extract, trace, log, test, impact, similar, dead, related, status, project, doctor.
229
239
  - **Be specific.** 5+ words. "auth" returns noise. "where does the server validate JWT tokens" is specific.
230
240
  - **Use `--role ORCHESTRATION`** to skip type definitions and find the actual logic.
231
241
  - **Use `--symbol`** when the query is a function/class name — gets search + trace in one call.