ucn 3.8.26 → 4.0.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/core/tracing.js CHANGED
@@ -3,6 +3,18 @@
3
3
  *
4
4
  * Extracted from project.js. All functions take an `index` (ProjectIndex)
5
5
  * as the first argument instead of using `this`.
6
+ *
7
+ * Tiered tree contract (v4): every command here runs findCallers/findCallees
8
+ * in collectAccount mode. Confirmed-tier edges form the tree trunk; unverified
9
+ * candidates are VISIBLE — caller-direction edges collect into a global
10
+ * `unverifiedFrontier` (with parent-node attribution and a reason), callee
11
+ * unknowns attach to their node as `unverifiedCallees`. Unverified edges are
12
+ * not expanded by default (expanding a possible-dispatch edge would assert
13
+ * transitive reach the evidence doesn't support); `expandUnverified` follows
14
+ * them, marking every downstream node `chainUnverified`. The root hop carries
15
+ * the same text-ground account as context/impact (composeAccount); interior
16
+ * hops conserve over the engine-candidate set, rolled up in `treeAccount`.
17
+ * `includeUncertain` is an implied no-op (the caller-contract precedent).
6
18
  */
7
19
 
8
20
  'use strict';
@@ -13,12 +25,118 @@ const { isTestFile } = require('./discovery');
13
25
  const { getCachedCalls } = require('./callers');
14
26
  const { detectLanguage, getLanguageModule } = require('../languages');
15
27
 
28
+ /**
29
+ * Contract-mode caller expansion for the tree commands. Memoizes the full
30
+ * findCallers(collectAccount) result per node and returns the tier partition.
31
+ * Unverified entries are fully enriched (content + enclosing caller): the
32
+ * frontier display and the affected-tests possible closure need them all.
33
+ */
34
+ function _contractCallers(index, funcDef, { includeMethods, callerCache, pin }) {
35
+ const nodeKey = `${funcDef.file}:${funcDef.startLine}`;
36
+ const cacheKey = funcDef.bindingId
37
+ ? `${funcDef.name}:${funcDef.bindingId}`
38
+ : `${funcDef.name}:${nodeKey}`;
39
+ let res = callerCache.get(cacheKey);
40
+ if (!res) {
41
+ const raw = index.findCallers(funcDef.name, {
42
+ includeMethods,
43
+ collectAccount: true,
44
+ unverifiedEnrichLimit: Infinity,
45
+ targetDefinitions: (funcDef.bindingId || pin) ? [funcDef] : undefined,
46
+ });
47
+ res = {
48
+ confirmed: raw.filter(c => c.tier !== 'unverified'),
49
+ unverified: [
50
+ ...raw.filter(c => c.tier === 'unverified'),
51
+ ...(raw.unverifiedEntries || []),
52
+ ],
53
+ raw,
54
+ };
55
+ callerCache.set(cacheKey, res);
56
+ }
57
+ return res;
58
+ }
59
+
60
+ /** Stable frontier ordering: hop, then parent node, then call site. */
61
+ function _sortFrontier(frontier) {
62
+ frontier.sort((a, b) =>
63
+ (a.hop - b.hop) ||
64
+ (a.atNode.file || '').localeCompare(b.atNode.file || '') ||
65
+ ((a.atNode.line || 0) - (b.atNode.line || 0)) ||
66
+ (a.relativePath || '').localeCompare(b.relativePath || '') ||
67
+ ((a.line || 0) - (b.line || 0)));
68
+ return frontier;
69
+ }
70
+
71
+ /** Aggregate one node's excluded-with-reason candidates into the tree account. */
72
+ function _aggregateExcluded(treeAccount, raw) {
73
+ const entries = (raw.accountRaw && raw.accountRaw.excludedEntries) || [];
74
+ for (const e of entries) {
75
+ const r = e.reason || 'excluded';
76
+ treeAccount.excludedTotal++;
77
+ treeAccount.excludedByReason[r] = (treeAccount.excludedByReason[r] || 0) + 1;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Dedupe caller call-sites into enclosing-function entries and resolve each
83
+ * to its symbol-table definition (pseudo-definition when absent). Shared by
84
+ * the confirmed trunk and the expand-unverified path.
85
+ */
86
+ function _resolveCallerEntries(index, callers, exclude) {
87
+ const uniqueCallers = new Map();
88
+ for (const c of callers) {
89
+ if (!c.callerName) continue; // skip module-level code
90
+ if (exclude.length > 0 && !index.matchesFilters(c.relativePath, { exclude })) continue;
91
+ const callerKey = c.callerStartLine
92
+ ? `${c.callerFile}:${c.callerStartLine}`
93
+ : `${c.callerFile}:${c.callerName}`;
94
+ if (!uniqueCallers.has(callerKey)) {
95
+ uniqueCallers.set(callerKey, {
96
+ name: c.callerName,
97
+ file: c.callerFile,
98
+ relativePath: c.relativePath,
99
+ startLine: c.callerStartLine,
100
+ endLine: c.callerEndLine,
101
+ callSites: 1,
102
+ reason: c.reason,
103
+ });
104
+ } else {
105
+ uniqueCallers.get(callerKey).callSites++;
106
+ }
107
+ }
108
+
109
+ const callerEntries = [];
110
+ for (const [, caller] of uniqueCallers) {
111
+ const defs = index.symbols.get(caller.name);
112
+ let callerDef = defs?.find(d => d.file === caller.file && d.startLine === caller.startLine);
113
+ if (!callerDef) {
114
+ // Pseudo-definition for callers not in symbol table
115
+ callerDef = {
116
+ name: caller.name,
117
+ file: caller.file,
118
+ relativePath: caller.relativePath,
119
+ startLine: caller.startLine,
120
+ endLine: caller.endLine,
121
+ type: 'function'
122
+ };
123
+ }
124
+ callerEntries.push({ def: callerDef, callSites: caller.callSites, reason: caller.reason });
125
+ }
126
+
127
+ // Stable sort by file + line
128
+ callerEntries.sort((a, b) =>
129
+ a.def.file.localeCompare(b.def.file) || a.def.startLine - b.def.startLine
130
+ );
131
+ return callerEntries;
132
+ }
133
+
16
134
  /**
17
135
  * Trace execution flow — build a tree of callees (down), callers (up), or both.
18
136
  *
19
137
  * @param {object} index - ProjectIndex instance
20
138
  * @param {string} name - Function name
21
- * @param {object} options - { depth, direction, file, className, all, includeMethods, includeUncertain }
139
+ * @param {object} options - { depth, direction, file, className, all, includeMethods, expandUnverified }
22
140
  * @returns {object|null} Trace tree with callers/callees
23
141
  */
24
142
  function trace(index, name, options = {}) {
@@ -37,11 +155,18 @@ function trace(index, name, options = {}) {
37
155
  return null;
38
156
  }
39
157
  const visited = new Set();
40
- // Memoize findCallees/findCallers results within this trace operation.
158
+ // Memoize findCallees results within this trace operation.
41
159
  // At depth 5, the same function appears at multiple tree positions — without
42
160
  // caching, findCallees is called redundantly (O(10^depth) → O(unique functions)).
43
161
  const calleeCache = new Map();
44
- const callerCache = new Map();
162
+
163
+ // Down-direction conservation rollup: every call site at every expanded
164
+ // node lands in exactly one bucket (per-node calleeAccount, summed here).
165
+ const downAccount = (direction === 'down' || direction === 'both') ? {
166
+ nodesExpanded: 0,
167
+ callSites: { total: 0, confirmed: 0, unverified: 0, external: 0, excluded: 0, filtered: 0 },
168
+ unverifiedByReason: {},
169
+ } : null;
45
170
 
46
171
  const buildTree = (funcDef, currentDepth, dir) => {
47
172
  const funcName = funcDef.name;
@@ -73,9 +198,30 @@ function trace(index, name, options = {}) {
73
198
  if (dir === 'down' || dir === 'both') {
74
199
  let callees = calleeCache.get(key);
75
200
  if (!callees) {
76
- callees = index.findCallees(funcDef, { includeMethods, includeUncertain: options.includeUncertain });
201
+ callees = index.findCallees(funcDef, { includeMethods, collectAccount: true });
77
202
  calleeCache.set(key, callees);
78
203
  }
204
+ // Callee contract: per-node account + visible unverified entries.
205
+ const acct = callees.calleeAccount;
206
+ if (acct && downAccount) {
207
+ node.calleeAccount = acct;
208
+ downAccount.nodesExpanded++;
209
+ downAccount.callSites.total += acct.totalSites;
210
+ downAccount.callSites.confirmed += acct.confirmed;
211
+ downAccount.callSites.unverified += acct.unverified;
212
+ downAccount.callSites.external += acct.external.count;
213
+ downAccount.callSites.excluded += acct.excluded.total;
214
+ downAccount.callSites.filtered += acct.filtered.count;
215
+ }
216
+ if (callees.unverifiedCallees && callees.unverifiedCallees.length > 0) {
217
+ node.unverifiedCallees = callees.unverifiedCallees;
218
+ if (downAccount) {
219
+ for (const u of node.unverifiedCallees) {
220
+ downAccount.unverifiedByReason[u.reason] =
221
+ (downAccount.unverifiedByReason[u.reason] || 0) + u.callCount;
222
+ }
223
+ }
224
+ }
79
225
  for (const callee of callees.slice(0, maxChildren)) {
80
226
  // callee already has the best-matched definition from findCallees
81
227
  const childTree = buildTree(callee, currentDepth + 1, 'down');
@@ -97,19 +243,42 @@ function trace(index, name, options = {}) {
97
243
 
98
244
  const tree = buildTree(def, 0, direction);
99
245
 
100
- // Also get callers if direction is 'up' or 'both'
246
+ // Also get callers if direction is 'up' or 'both' — one contract hop:
247
+ // confirmed callers render in CALLED BY, unverified candidates in the
248
+ // frontier, reconciled by the root text-ground account.
101
249
  let callers = [];
102
250
  let truncatedCallers = 0;
251
+ let unverifiedFrontier;
252
+ let account;
103
253
  if (direction === 'up' || direction === 'both') {
104
- const allCallers = index.findCallers(name, { includeMethods, includeUncertain: options.includeUncertain, targetDefinitions: [def] });
105
- callers = allCallers.slice(0, maxChildren).map(c => ({
254
+ const rawCallers = index.findCallers(name, {
255
+ includeMethods,
256
+ collectAccount: true,
257
+ unverifiedEnrichLimit: Infinity,
258
+ targetDefinitions: [def],
259
+ });
260
+ const confirmed = rawCallers.filter(c => c.tier !== 'unverified');
261
+ let unverified = [
262
+ ...rawCallers.filter(c => c.tier === 'unverified'),
263
+ ...(rawCallers.unverifiedEntries || []),
264
+ ];
265
+ const { composeAccount, callNotResolvedEntries } = require('./analysis');
266
+ account = composeAccount(index, name, rawCallers);
267
+ unverified = [...unverified, ...callNotResolvedEntries(index, account, options)];
268
+ const rootRef = { name: def.name, file: def.relativePath, line: def.startLine };
269
+ unverifiedFrontier = _sortFrontier(unverified.map(u => ({
270
+ atNode: rootRef,
271
+ hop: 1,
272
+ ...u,
273
+ })));
274
+ callers = confirmed.slice(0, maxChildren).map(c => ({
106
275
  name: c.callerName || '(anonymous)',
107
276
  file: c.relativePath,
108
277
  line: c.line,
109
- expression: c.content.trim()
278
+ expression: (c.content || '').trim()
110
279
  }));
111
- if (allCallers.length > maxChildren) {
112
- truncatedCallers = allCallers.length - maxChildren;
280
+ if (confirmed.length > maxChildren) {
281
+ truncatedCallers = confirmed.length - maxChildren;
113
282
  }
114
283
  }
115
284
 
@@ -136,6 +305,9 @@ function trace(index, name, options = {}) {
136
305
  tree,
137
306
  callers: direction !== 'down' ? callers : undefined,
138
307
  truncatedCallers: truncatedCallers > 0 ? truncatedCallers : undefined,
308
+ ...(unverifiedFrontier && unverifiedFrontier.length > 0 && { unverifiedFrontier }),
309
+ ...(downAccount && { treeAccount: downAccount }),
310
+ ...(account && { account }),
139
311
  warnings: warnings.length > 0 ? warnings : undefined
140
312
  };
141
313
  } finally { index._endOp(); }
@@ -147,7 +319,7 @@ function trace(index, name, options = {}) {
147
319
  *
148
320
  * @param {object} index - ProjectIndex instance
149
321
  * @param {string} name - Function name
150
- * @param {object} options - { depth, file, className, all, exclude, includeMethods, includeUncertain }
322
+ * @param {object} options - { depth, file, className, all, exclude, includeMethods, expandUnverified }
151
323
  * @returns {object|null} Blast radius tree with summary
152
324
  */
153
325
  function blast(index, name, options = {}) {
@@ -156,8 +328,8 @@ function blast(index, name, options = {}) {
156
328
  const maxDepth = Math.max(0, options.depth ?? 3);
157
329
  const maxChildren = options.all ? Infinity : 10;
158
330
  const includeMethods = options.includeMethods ?? true;
159
- const includeUncertain = options.includeUncertain || false;
160
331
  const exclude = options.exclude || [];
332
+ const expandUnverified = !!options.expandUnverified;
161
333
 
162
334
  const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
163
335
  if (!def) return null;
@@ -165,10 +337,24 @@ function blast(index, name, options = {}) {
165
337
  const visited = new Set();
166
338
  const callerCache = new Map();
167
339
  const affectedFunctions = new Set();
340
+ const possiblyAffectedSet = new Set();
168
341
  const affectedFiles = new Set();
342
+ const frontier = [];
169
343
  let maxDepthReached = 0;
344
+ let rootRaw = null;
345
+ let rootFiltered = 0;
346
+ const treeAccount = {
347
+ nodesExpanded: 0,
348
+ confirmedEdges: 0,
349
+ unverifiedEdges: 0,
350
+ unverifiedByReason: {},
351
+ excludedTotal: 0,
352
+ excludedByReason: {},
353
+ filteredEdges: 0,
354
+ depthLimitNodes: 0,
355
+ };
170
356
 
171
- const buildCallerTree = (funcDef, currentDepth) => {
357
+ const buildCallerTree = (funcDef, currentDepth, chainUnverified) => {
172
358
  const key = `${funcDef.file}:${funcDef.startLine}`;
173
359
  if (currentDepth > maxDepth) return null;
174
360
  if (visited.has(key)) {
@@ -185,8 +371,12 @@ function blast(index, name, options = {}) {
185
371
 
186
372
  if (currentDepth > maxDepthReached) maxDepthReached = currentDepth;
187
373
  if (currentDepth > 0) {
188
- affectedFunctions.add(key);
189
- affectedFiles.add(funcDef.file);
374
+ if (chainUnverified) {
375
+ possiblyAffectedSet.add(key);
376
+ } else {
377
+ affectedFunctions.add(key);
378
+ affectedFiles.add(funcDef.file);
379
+ }
190
380
  }
191
381
 
192
382
  const node = {
@@ -194,75 +384,51 @@ function blast(index, name, options = {}) {
194
384
  file: funcDef.relativePath,
195
385
  line: funcDef.startLine,
196
386
  type: funcDef.type || 'function',
387
+ ...(chainUnverified && { chainUnverified: true }),
197
388
  children: []
198
389
  };
199
390
 
200
391
  if (currentDepth < maxDepth) {
201
- const callerCacheKey = funcDef.bindingId
202
- ? `${funcDef.name}:${funcDef.bindingId}`
203
- : `${funcDef.name}:${key}`;
204
- let callers = callerCache.get(callerCacheKey);
205
- if (!callers) {
206
- callers = index.findCallers(funcDef.name, {
207
- includeMethods,
208
- includeUncertain,
209
- targetDefinitions: funcDef.bindingId ? [funcDef] : undefined,
210
- });
211
- callerCache.set(callerCacheKey, callers);
392
+ const { confirmed, unverified, raw } = _contractCallers(index, funcDef, { includeMethods, callerCache });
393
+ treeAccount.nodesExpanded++;
394
+ _aggregateExcluded(treeAccount, raw);
395
+ if (currentDepth === 0) rootRaw = raw;
396
+
397
+ // Confirmed-tier call sites form the trunk
398
+ let callers = confirmed;
399
+ if (exclude.length > 0) {
400
+ const before = callers.length;
401
+ callers = callers.filter(c => index.matchesFilters(c.relativePath, { exclude }));
402
+ treeAccount.filteredEdges += before - callers.length;
403
+ if (currentDepth === 0) rootFiltered += before - callers.length;
212
404
  }
213
-
214
- // Deduplicate callers by enclosing function (multiple call sites → one tree node)
215
- const uniqueCallers = new Map();
216
- for (const c of callers) {
217
- if (!c.callerName) continue; // skip module-level code
218
- // Apply exclude filter
219
- if (exclude.length > 0 && !index.matchesFilters(c.relativePath, { exclude })) continue;
220
- const callerKey = c.callerStartLine
221
- ? `${c.callerFile}:${c.callerStartLine}`
222
- : `${c.callerFile}:${c.callerName}`;
223
- if (!uniqueCallers.has(callerKey)) {
224
- uniqueCallers.set(callerKey, {
225
- name: c.callerName,
226
- file: c.callerFile,
227
- relativePath: c.relativePath,
228
- startLine: c.callerStartLine,
229
- endLine: c.callerEndLine,
230
- callSites: 1
231
- });
232
- } else {
233
- uniqueCallers.get(callerKey).callSites++;
234
- }
405
+ treeAccount.confirmedEdges += callers.length;
406
+
407
+ // Unverified-tier candidates: visible frontier entries,
408
+ // expanded only under expandUnverified.
409
+ let nodeUnverified = unverified;
410
+ if (exclude.length > 0) {
411
+ const before = nodeUnverified.length;
412
+ nodeUnverified = nodeUnverified.filter(c => index.matchesFilters(c.relativePath, { exclude }));
413
+ treeAccount.filteredEdges += before - nodeUnverified.length;
414
+ if (currentDepth === 0) rootFiltered += before - nodeUnverified.length;
235
415
  }
236
-
237
- // Resolve definitions and build child nodes
238
- const callerEntries = [];
239
- for (const [, caller] of uniqueCallers) {
240
- // Look up actual definition from symbol table
241
- const defs = index.symbols.get(caller.name);
242
- let callerDef = defs?.find(d => d.file === caller.file && d.startLine === caller.startLine);
243
-
244
- if (!callerDef) {
245
- // Pseudo-definition for callers not in symbol table
246
- callerDef = {
247
- name: caller.name,
248
- file: caller.file,
249
- relativePath: caller.relativePath,
250
- startLine: caller.startLine,
251
- endLine: caller.endLine,
252
- type: 'function'
253
- };
254
- }
255
-
256
- callerEntries.push({ def: callerDef, callSites: caller.callSites });
416
+ for (const u of nodeUnverified) {
417
+ treeAccount.unverifiedEdges++;
418
+ const r = u.reason || 'unverified';
419
+ treeAccount.unverifiedByReason[r] = (treeAccount.unverifiedByReason[r] || 0) + 1;
420
+ frontier.push({
421
+ atNode: { name: node.name, file: node.file, line: node.line },
422
+ hop: currentDepth + 1,
423
+ ...u,
424
+ ...(expandUnverified && u.callerName ? { expanded: true } : {}),
425
+ });
257
426
  }
258
427
 
259
- // Stable sort by file + line
260
- callerEntries.sort((a, b) =>
261
- a.def.file.localeCompare(b.def.file) || a.def.startLine - b.def.startLine
262
- );
428
+ const callerEntries = _resolveCallerEntries(index, callers, []);
263
429
 
264
430
  for (const { def: cDef, callSites } of callerEntries.slice(0, maxChildren)) {
265
- const childTree = buildCallerTree(cDef, currentDepth + 1);
431
+ const childTree = buildCallerTree(cDef, currentDepth + 1, chainUnverified);
266
432
  if (childTree) {
267
433
  childTree.callSites = callSites;
268
434
  node.children.push(childTree);
@@ -273,19 +439,66 @@ function blast(index, name, options = {}) {
273
439
  node.truncatedChildren = callerEntries.length - maxChildren;
274
440
  // Count truncated callers in summary
275
441
  for (const { def: cDef } of callerEntries.slice(maxChildren)) {
276
- const key = `${cDef.file}:${cDef.startLine}`;
277
- if (!visited.has(key)) {
278
- affectedFunctions.add(key);
279
- affectedFiles.add(cDef.file);
442
+ const tKey = `${cDef.file}:${cDef.startLine}`;
443
+ if (!visited.has(tKey)) {
444
+ if (chainUnverified) {
445
+ possiblyAffectedSet.add(tKey);
446
+ } else {
447
+ affectedFunctions.add(tKey);
448
+ affectedFiles.add(cDef.file);
449
+ }
280
450
  }
281
451
  }
282
452
  }
453
+
454
+ // Follow unverified edges on request: every downstream node is
455
+ // marked chainUnverified — reach asserted by an unverified hop
456
+ // is possible impact, never confirmed impact.
457
+ if (expandUnverified && nodeUnverified.length > 0) {
458
+ const unvEntries = _resolveCallerEntries(index, nodeUnverified, []);
459
+ for (const { def: cDef, callSites, reason } of unvEntries.slice(0, maxChildren)) {
460
+ const childTree = buildCallerTree(cDef, currentDepth + 1, true);
461
+ if (childTree) {
462
+ childTree.callSites = callSites;
463
+ childTree.viaUnverified = reason || 'unverified';
464
+ node.children.push(childTree);
465
+ }
466
+ }
467
+ for (const { def: cDef } of unvEntries.slice(maxChildren)) {
468
+ const tKey = `${cDef.file}:${cDef.startLine}`;
469
+ if (!visited.has(tKey)) possiblyAffectedSet.add(tKey);
470
+ }
471
+ }
472
+ } else {
473
+ // Depth limit: this node's callers were not searched.
474
+ treeAccount.depthLimitNodes++;
283
475
  }
284
476
 
285
477
  return node;
286
478
  };
287
479
 
288
- const tree = buildCallerTree(def, 0);
480
+ const tree = buildCallerTree(def, 0, false);
481
+
482
+ // Root text-ground account (the context/impact contract at hop 1).
483
+ // Ground call-lines no candidate claimed are frontier entries too —
484
+ // counted in the account's unverified total, listed here.
485
+ let account;
486
+ if (rootRaw) {
487
+ const { composeAccount, callNotResolvedEntries } = require('./analysis');
488
+ account = composeAccount(index, name, rootRaw,
489
+ rootFiltered > 0 ? { total: rootFiltered, byFlag: { exclude: rootFiltered } } : undefined);
490
+ for (const e of callNotResolvedEntries(index, account, options)) {
491
+ treeAccount.unverifiedEdges++;
492
+ treeAccount.unverifiedByReason['call-not-resolved'] =
493
+ (treeAccount.unverifiedByReason['call-not-resolved'] || 0) + 1;
494
+ frontier.push({
495
+ atNode: { name: def.name, file: def.relativePath, line: def.startLine },
496
+ hop: 1,
497
+ ...e,
498
+ });
499
+ }
500
+ }
501
+ _sortFrontier(frontier);
289
502
 
290
503
  // Smart hints
291
504
  if (tree && tree.children.length === 0) {
@@ -304,11 +517,17 @@ function blast(index, name, options = {}) {
304
517
  line: def.startLine,
305
518
  maxDepth,
306
519
  includeMethods,
520
+ expandUnverified: expandUnverified || undefined,
307
521
  tree,
522
+ unverifiedFrontier: frontier,
523
+ treeAccount,
524
+ ...(account && { account }),
308
525
  summary: {
309
526
  totalAffected: affectedFunctions.size,
310
527
  totalFiles: affectedFiles.size,
311
- maxDepthReached
528
+ maxDepthReached,
529
+ unverifiedEdges: treeAccount.unverifiedEdges,
530
+ ...(expandUnverified && { possiblyAffected: possiblyAffectedSet.size }),
312
531
  },
313
532
  warnings: warnings.length > 0 ? warnings : undefined
314
533
  };
@@ -320,9 +539,15 @@ function blast(index, name, options = {}) {
320
539
  * Like blast but focused on "how does execution reach this function?"
321
540
  * Marks leaf nodes (functions with no callers) as entry points.
322
541
  *
542
+ * Entry-point soundness (tree contract): a node is an entry point only when
543
+ * it has zero confirmed AND zero unverified caller candidates. Zero confirmed
544
+ * with unverified candidates renders `unverifiedCallerCount` instead — the
545
+ * legacy behavior marked such nodes "entry point" after silently dropping
546
+ * possible-dispatch callers.
547
+ *
323
548
  * @param {object} index - ProjectIndex instance
324
549
  * @param {string} name - Function name
325
- * @param {object} options - { depth, file, className, all, exclude, includeMethods, includeUncertain }
550
+ * @param {object} options - { depth, file, className, all, exclude, includeMethods, expandUnverified }
326
551
  * @returns {object|null} Reverse trace tree with entry points
327
552
  */
328
553
  function reverseTrace(index, name, options = {}) {
@@ -331,8 +556,8 @@ function reverseTrace(index, name, options = {}) {
331
556
  const maxDepth = Math.max(0, options.depth ?? 5);
332
557
  const maxChildren = options.all ? Infinity : 10;
333
558
  const includeMethods = options.includeMethods ?? true;
334
- const includeUncertain = options.includeUncertain || false;
335
559
  const exclude = options.exclude || [];
560
+ const expandUnverified = !!options.expandUnverified;
336
561
 
337
562
  const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
338
563
  if (!def) return null;
@@ -340,9 +565,37 @@ function reverseTrace(index, name, options = {}) {
340
565
  const visited = new Set();
341
566
  const callerCache = new Map();
342
567
  const entryPoints = [];
568
+ const frontier = [];
343
569
  let maxDepthReached = 0;
570
+ let rootRaw = null;
571
+ let rootFiltered = 0;
572
+ let rootUnverifiedCount = 0;
573
+ const treeAccount = {
574
+ nodesExpanded: 0,
575
+ confirmedEdges: 0,
576
+ unverifiedEdges: 0,
577
+ unverifiedByReason: {},
578
+ excludedTotal: 0,
579
+ excludedByReason: {},
580
+ filteredEdges: 0,
581
+ depthLimitNodes: 0,
582
+ };
344
583
 
345
- const buildCallerTree = (funcDef, currentDepth) => {
584
+ // Tier-partitioned, exclude-filtered callers of a node (memoized).
585
+ const nodeCallers = (funcDef, isExpansion) => {
586
+ const { confirmed, unverified, raw } = _contractCallers(index, funcDef, { includeMethods, callerCache });
587
+ let conf = confirmed;
588
+ let unv = unverified;
589
+ if (exclude.length > 0) {
590
+ const before = conf.length + unv.length;
591
+ conf = conf.filter(c => index.matchesFilters(c.relativePath, { exclude }));
592
+ unv = unv.filter(c => index.matchesFilters(c.relativePath, { exclude }));
593
+ if (isExpansion) treeAccount.filteredEdges += before - conf.length - unv.length;
594
+ }
595
+ return { confirmed: conf, unverified: unv, raw };
596
+ };
597
+
598
+ const buildCallerTree = (funcDef, currentDepth, chainUnverified) => {
346
599
  const key = `${funcDef.file}:${funcDef.startLine}`;
347
600
  if (currentDepth > maxDepth) return null;
348
601
  if (visited.has(key)) {
@@ -363,69 +616,35 @@ function reverseTrace(index, name, options = {}) {
363
616
  file: funcDef.relativePath,
364
617
  line: funcDef.startLine,
365
618
  type: funcDef.type || 'function',
619
+ ...(chainUnverified && { chainUnverified: true }),
366
620
  children: []
367
621
  };
368
622
 
369
623
  if (currentDepth < maxDepth) {
370
- const callerCacheKey = funcDef.bindingId
371
- ? `${funcDef.name}:${funcDef.bindingId}`
372
- : `${funcDef.name}:${key}`;
373
- let callers = callerCache.get(callerCacheKey);
374
- if (!callers) {
375
- callers = index.findCallers(funcDef.name, {
376
- includeMethods,
377
- includeUncertain,
378
- targetDefinitions: funcDef.bindingId ? [funcDef] : undefined,
379
- });
380
- callerCache.set(callerCacheKey, callers);
624
+ const { confirmed, unverified, raw } = nodeCallers(funcDef, true);
625
+ treeAccount.nodesExpanded++;
626
+ _aggregateExcluded(treeAccount, raw);
627
+ if (currentDepth === 0) {
628
+ rootRaw = raw;
629
+ rootUnverifiedCount = unverified.length;
381
630
  }
382
-
383
- // Deduplicate callers by enclosing function
384
- const uniqueCallers = new Map();
385
- for (const c of callers) {
386
- if (!c.callerName) continue;
387
- if (exclude.length > 0 && !index.matchesFilters(c.relativePath, { exclude })) continue;
388
- const callerKey = c.callerStartLine
389
- ? `${c.callerFile}:${c.callerStartLine}`
390
- : `${c.callerFile}:${c.callerName}`;
391
- if (!uniqueCallers.has(callerKey)) {
392
- uniqueCallers.set(callerKey, {
393
- name: c.callerName,
394
- file: c.callerFile,
395
- relativePath: c.relativePath,
396
- startLine: c.callerStartLine,
397
- endLine: c.callerEndLine,
398
- callSites: 1
399
- });
400
- } else {
401
- uniqueCallers.get(callerKey).callSites++;
402
- }
631
+ treeAccount.confirmedEdges += confirmed.length;
632
+ for (const u of unverified) {
633
+ treeAccount.unverifiedEdges++;
634
+ const r = u.reason || 'unverified';
635
+ treeAccount.unverifiedByReason[r] = (treeAccount.unverifiedByReason[r] || 0) + 1;
636
+ frontier.push({
637
+ atNode: { name: node.name, file: node.file, line: node.line },
638
+ hop: currentDepth + 1,
639
+ ...u,
640
+ ...(expandUnverified && u.callerName ? { expanded: true } : {}),
641
+ });
403
642
  }
404
643
 
405
- // Resolve definitions and build child nodes
406
- const callerEntries = [];
407
- for (const [, caller] of uniqueCallers) {
408
- const defs = index.symbols.get(caller.name);
409
- let callerDef = defs?.find(d => d.file === caller.file && d.startLine === caller.startLine);
410
- if (!callerDef) {
411
- callerDef = {
412
- name: caller.name,
413
- file: caller.file,
414
- relativePath: caller.relativePath,
415
- startLine: caller.startLine,
416
- endLine: caller.endLine,
417
- type: 'function'
418
- };
419
- }
420
- callerEntries.push({ def: callerDef, callSites: caller.callSites });
421
- }
422
-
423
- callerEntries.sort((a, b) =>
424
- a.def.file.localeCompare(b.def.file) || a.def.startLine - b.def.startLine
425
- );
644
+ const callerEntries = _resolveCallerEntries(index, confirmed, []);
426
645
 
427
646
  for (const { def: cDef, callSites } of callerEntries.slice(0, maxChildren)) {
428
- const childTree = buildCallerTree(cDef, currentDepth + 1);
647
+ const childTree = buildCallerTree(cDef, currentDepth + 1, chainUnverified);
429
648
  if (childTree) {
430
649
  childTree.callSites = callSites;
431
650
  node.children.push(childTree);
@@ -435,60 +654,87 @@ function reverseTrace(index, name, options = {}) {
435
654
  if (callerEntries.length > maxChildren) {
436
655
  node.truncatedChildren = callerEntries.length - maxChildren;
437
656
  // Count entry points in truncated branches so summary is accurate
438
- // Use callerCache to avoid redundant findCallers calls
439
657
  for (const { def: cDef } of callerEntries.slice(maxChildren)) {
440
658
  const cKey = `${cDef.file}:${cDef.startLine}`;
441
659
  if (!visited.has(cKey)) {
442
- const cCacheKey = cDef.bindingId
443
- ? `${cDef.name}:${cDef.bindingId}`
444
- : `${cDef.name}:${cKey}`;
445
- let cCallers = callerCache.get(cCacheKey);
446
- if (!cCallers) {
447
- cCallers = index.findCallers(cDef.name, {
448
- includeMethods, includeUncertain,
449
- targetDefinitions: cDef.bindingId ? [cDef] : undefined,
450
- maxResults: 1, // Only need to know if any exist
451
- });
452
- callerCache.set(cCacheKey, cCallers);
453
- }
454
- if (cCallers.length === 0) {
660
+ const tiers = nodeCallers(cDef, false);
661
+ if (tiers.confirmed.length === 0 && tiers.unverified.length === 0) {
455
662
  entryPoints.push({ name: cDef.name, file: cDef.relativePath || path.relative(index.root, cDef.file), line: cDef.startLine });
456
663
  }
457
664
  }
458
665
  }
459
666
  }
460
667
 
461
- // Mark as entry point if no callers found (and not at depth limit)
462
- if (uniqueCallers.size === 0 && currentDepth > 0) {
463
- node.entryPoint = true;
464
- entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
668
+ if (expandUnverified && unverified.length > 0) {
669
+ const unvEntries = _resolveCallerEntries(index, unverified, []);
670
+ for (const { def: cDef, callSites, reason } of unvEntries.slice(0, maxChildren)) {
671
+ const childTree = buildCallerTree(cDef, currentDepth + 1, true);
672
+ if (childTree) {
673
+ childTree.callSites = callSites;
674
+ childTree.viaUnverified = reason || 'unverified';
675
+ node.children.push(childTree);
676
+ }
677
+ }
678
+ }
679
+
680
+ // Entry point only when BOTH tiers are empty; unverified-only
681
+ // nodes are visibly not-confirmed instead.
682
+ if (callerEntries.length === 0 && currentDepth > 0) {
683
+ if (unverified.length === 0) {
684
+ node.entryPoint = true;
685
+ entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
686
+ } else {
687
+ node.unverifiedCallerCount = unverified.length;
688
+ }
465
689
  }
466
690
  } else if (currentDepth > 0) {
467
691
  // At depth limit: check if this node is an entry point
468
- const callers = index.findCallers(funcDef.name, {
469
- includeMethods,
470
- includeUncertain,
471
- targetDefinitions: funcDef.bindingId ? [funcDef] : undefined,
472
- });
473
- const hasCallers = callers.some(c => c.callerName &&
474
- (exclude.length === 0 || index.matchesFilters(c.relativePath, { exclude })));
475
- if (!hasCallers) {
476
- node.entryPoint = true;
477
- entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
692
+ treeAccount.depthLimitNodes++;
693
+ const tiers = nodeCallers(funcDef, false);
694
+ if (tiers.confirmed.filter(c => c.callerName).length === 0) {
695
+ if (tiers.unverified.length === 0) {
696
+ node.entryPoint = true;
697
+ entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
698
+ } else {
699
+ node.unverifiedCallerCount = tiers.unverified.length;
700
+ }
478
701
  }
479
702
  }
480
703
 
481
704
  return node;
482
705
  };
483
706
 
484
- const tree = buildCallerTree(def, 0);
707
+ const tree = buildCallerTree(def, 0, false);
485
708
 
486
- // Also mark root as entry point if it has no callers
709
+ // Also mark root as entry point if it has no callers in either tier
487
710
  if (tree && tree.children.length === 0 && maxDepth > 0) {
488
- tree.entryPoint = true;
489
- entryPoints.push({ name: def.name, file: def.relativePath, line: def.startLine });
711
+ if (rootUnverifiedCount === 0) {
712
+ tree.entryPoint = true;
713
+ entryPoints.push({ name: def.name, file: def.relativePath, line: def.startLine });
714
+ } else {
715
+ tree.unverifiedCallerCount = rootUnverifiedCount;
716
+ }
490
717
  }
491
718
 
719
+ // Root text-ground account + unclaimed ground call-lines
720
+ let account;
721
+ if (rootRaw) {
722
+ const { composeAccount, callNotResolvedEntries } = require('./analysis');
723
+ account = composeAccount(index, name, rootRaw,
724
+ rootFiltered > 0 ? { total: rootFiltered, byFlag: { exclude: rootFiltered } } : undefined);
725
+ for (const e of callNotResolvedEntries(index, account, options)) {
726
+ treeAccount.unverifiedEdges++;
727
+ treeAccount.unverifiedByReason['call-not-resolved'] =
728
+ (treeAccount.unverifiedByReason['call-not-resolved'] || 0) + 1;
729
+ frontier.push({
730
+ atNode: { name: def.name, file: def.relativePath, line: def.startLine },
731
+ hop: 1,
732
+ ...e,
733
+ });
734
+ }
735
+ }
736
+ _sortFrontier(frontier);
737
+
492
738
  // Smart hints
493
739
  if (tree && tree.children.length === 0) {
494
740
  if (maxDepth === 0) {
@@ -506,12 +752,17 @@ function reverseTrace(index, name, options = {}) {
506
752
  line: def.startLine,
507
753
  maxDepth,
508
754
  includeMethods,
755
+ expandUnverified: expandUnverified || undefined,
509
756
  tree,
510
757
  entryPoints,
758
+ unverifiedFrontier: frontier,
759
+ treeAccount,
760
+ ...(account && { account }),
511
761
  summary: {
512
762
  totalEntryPoints: entryPoints.length,
513
763
  totalFunctions: visited.size - 1, // exclude root
514
- maxDepthReached
764
+ maxDepthReached,
765
+ unverifiedEdges: treeAccount.unverifiedEdges,
515
766
  },
516
767
  warnings: warnings.length > 0 ? warnings : undefined
517
768
  };
@@ -522,42 +773,99 @@ function reverseTrace(index, name, options = {}) {
522
773
  * Find tests affected by a change to the given function.
523
774
  * Composes blast() (transitive callers) with test file scanning.
524
775
  *
776
+ * Two bands (tree contract): `affectedFunctions`/`testFiles` come from the
777
+ * confirmed-chain closure; names reachable only through >= 1 unverified hop
778
+ * land in `possiblyAffected`, their additional test files in
779
+ * `possiblyAffectedTests`. Coverage/uncovered claims are confirmed-band only.
780
+ *
525
781
  * @param {object} index - ProjectIndex instance
526
782
  * @param {string} name - Function name
527
- * @param {object} options - { depth, file, className, exclude, includeMethods, includeUncertain }
783
+ * @param {object} options - { depth, file, className, exclude, includeMethods }
528
784
  * @returns {object|null} Affected test files with coverage stats
529
785
  */
530
786
  function affectedTests(index, name, options = {}) {
531
787
  index._beginOp();
532
788
  try {
533
- // Step 1: Get all transitively affected functions via blast
789
+ const maxDepth = Math.max(0, options.depth ?? 3);
790
+ // Step 1: confirmed closure via blast (contract mode, no truncation)
534
791
  const blastResult = index.blast(name, {
535
- depth: options.depth ?? 3,
792
+ depth: maxDepth,
536
793
  file: options.file,
537
794
  className: options.className,
538
795
  all: true,
539
796
  exclude: options.exclude,
540
797
  includeMethods: options.includeMethods,
541
- includeUncertain: options.includeUncertain,
542
798
  });
543
799
  if (!blastResult) return null;
544
800
 
545
- // Step 2: Collect all affected function names from the tree
801
+ // Step 2: Collect confirmed-affected function names (and node keys)
546
802
  const affectedNames = new Set();
803
+ const confirmedKeys = new Set();
547
804
  affectedNames.add(name);
548
805
  const collectNames = (node) => {
549
806
  if (!node) return;
550
807
  affectedNames.add(node.name);
808
+ confirmedKeys.add(`${node.file}:${node.line}`);
551
809
  for (const child of node.children || []) collectNames(child);
552
810
  };
553
811
  collectNames(blastResult.tree);
554
812
 
555
- // Step 3: Scan test files for all affected names using AST
556
- // Only count call and test-case matches as real coverage — not imports or bare references.
813
+ // Step 2b: possible closure BFS seeded by the frontier's enclosing
814
+ // functions, following both edge tiers, bounded by the same depth.
815
+ // Names reached only this way are possibly affected, never confirmed.
557
816
  const exclude = options.exclude;
558
817
  const excludeArr = exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [];
818
+ const includeMethods = options.includeMethods ?? true;
819
+ const possiblyNames = new Set();
820
+ {
821
+ const callerCache = new Map();
822
+ const possibleVisited = new Set();
823
+ const queue = [];
824
+ const enqueueCaller = (c, depth) => {
825
+ if (!c.callerName || !c.callerFile) return;
826
+ if (excludeArr.length > 0 && !index.matchesFilters(c.relativePath, { exclude: excludeArr })) return;
827
+ const defs = index.symbols.get(c.callerName);
828
+ let cDef = defs?.find(d => d.file === c.callerFile && d.startLine === c.callerStartLine);
829
+ if (!cDef) {
830
+ cDef = {
831
+ name: c.callerName,
832
+ file: c.callerFile,
833
+ relativePath: c.relativePath,
834
+ startLine: c.callerStartLine,
835
+ endLine: c.callerEndLine,
836
+ type: 'function'
837
+ };
838
+ }
839
+ queue.push({ def: cDef, depth });
840
+ };
841
+ for (const fe of blastResult.unverifiedFrontier || []) {
842
+ enqueueCaller(fe, fe.hop);
843
+ }
844
+ while (queue.length > 0) {
845
+ const { def: d, depth } = queue.shift();
846
+ if (depth > maxDepth) continue;
847
+ const k = `${d.relativePath || path.relative(index.root, d.file)}:${d.startLine}`;
848
+ if (possibleVisited.has(k)) continue;
849
+ possibleVisited.add(k);
850
+ if (!confirmedKeys.has(k) && !affectedNames.has(d.name)) {
851
+ possiblyNames.add(d.name);
852
+ }
853
+ if (depth < maxDepth) {
854
+ const { confirmed, unverified } = _contractCallers(index, d, { includeMethods, callerCache });
855
+ for (const c of confirmed) enqueueCaller(c, depth + 1);
856
+ for (const c of unverified) enqueueCaller(c, depth + 1);
857
+ }
858
+ }
859
+ // A name in both bands is confirmed — the possible band only adds.
860
+ for (const n of affectedNames) possiblyNames.delete(n);
861
+ }
862
+
863
+ // Step 3: Scan test files for all affected names using AST
864
+ // Only count call and test-case matches as real coverage — not imports or bare references.
559
865
  const className = options.className || null;
560
866
  const results = [];
867
+ const possibleResults = [];
868
+ const scanNames = [...affectedNames, ...possiblyNames];
561
869
  for (const [filePath, fileEntry] of index.files) {
562
870
  let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
563
871
  // Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
@@ -573,7 +881,7 @@ function affectedTests(index, name, options = {}) {
573
881
  const content = index._readFile(filePath);
574
882
  const fileMatches = new Map();
575
883
 
576
- for (const funcName of affectedNames) {
884
+ for (const funcName of scanNames) {
577
885
  // Fast pre-check
578
886
  if (!content.includes(funcName)) continue;
579
887
 
@@ -647,23 +955,41 @@ function affectedTests(index, name, options = {}) {
647
955
 
648
956
  // Only count functions with call or test-case matches as covered.
649
957
  // Import-only or reference-only functions are not real coverage.
650
- const realCoveredFunctions = coveredFunctions.filter(fn => {
958
+ const realCoveredAll = coveredFunctions.filter(fn => {
651
959
  const fnMatches = deduped.filter(m => m.functionName === fn);
652
960
  return fnMatches.some(m => m.matchType === 'call' || m.matchType === 'test-case');
653
961
  });
962
+ const realCoveredFunctions = realCoveredAll.filter(fn => affectedNames.has(fn));
963
+ const possiblyCovered = realCoveredAll.filter(fn => possiblyNames.has(fn));
654
964
 
655
- // Only include file if it has real coverage
656
- const realMatches = deduped.filter(m =>
657
- m.matchType === 'call' || m.matchType === 'test-case' ||
658
- realCoveredFunctions.includes(m.functionName)
659
- );
660
965
  if (realCoveredFunctions.length > 0) {
966
+ // Confirmed band: matches for confirmed-covered names
967
+ const realMatches = deduped.filter(m =>
968
+ affectedNames.has(m.functionName) &&
969
+ (m.matchType === 'call' || m.matchType === 'test-case' ||
970
+ realCoveredFunctions.includes(m.functionName))
971
+ );
661
972
  results.push({
662
973
  file: fileEntry.relativePath,
663
974
  coveredFunctions: realCoveredFunctions,
975
+ ...(possiblyCovered.length > 0 && { possiblyCovered }),
664
976
  matchCount: realMatches.length,
665
977
  matches: realMatches
666
978
  });
979
+ } else if (possiblyCovered.length > 0) {
980
+ // Possible band: file reaches the change only through
981
+ // unverified chains.
982
+ const possibleMatches = deduped.filter(m =>
983
+ possiblyNames.has(m.functionName) &&
984
+ (m.matchType === 'call' || m.matchType === 'test-case' ||
985
+ possiblyCovered.includes(m.functionName))
986
+ );
987
+ possibleResults.push({
988
+ file: fileEntry.relativePath,
989
+ coveredFunctions: possiblyCovered,
990
+ matchCount: possibleMatches.length,
991
+ matches: possibleMatches
992
+ });
667
993
  }
668
994
  }
669
995
  } catch (e) { /* skip unreadable */ }
@@ -671,22 +997,33 @@ function affectedTests(index, name, options = {}) {
671
997
 
672
998
  // Sort by coverage breadth then alphabetically
673
999
  results.sort((a, b) => b.coveredFunctions.length - a.coveredFunctions.length || a.file.localeCompare(b.file));
1000
+ possibleResults.sort((a, b) => b.coveredFunctions.length - a.coveredFunctions.length || a.file.localeCompare(b.file));
674
1001
 
675
1002
  // Compute coverage stats.
676
1003
  // Filter out test function names from affectedNames — they are callers,
677
1004
  // not production symbols that need test coverage.
678
- const productionNames = new Set();
679
- for (const n of affectedNames) {
680
- // Check if this name is only found in test files
681
- let foundInSource = false;
1005
+ const isProductionName = (n) => {
1006
+ // Check if this name is only found in test files. Inline test
1007
+ // functions (#[test] fns in Rust's #[cfg(test)] mods, Go Test*)
1008
+ // live in production-path FILES but are tests themselves — the
1009
+ // language's getEntryPointKind says so; they need no coverage.
682
1010
  for (const [fp, fe] of index.files) {
683
1011
  if (isTestFile(fe.relativePath, fe.language)) continue;
684
- if (fe.symbols?.some(s => s.name === n)) { foundInSource = true; break; }
1012
+ const langModule = getLanguageModule(fe.language);
1013
+ const kindOf = langModule?.getEntryPointKind;
1014
+ if (fe.symbols?.some(s => s.name === n && (!kindOf || kindOf(s) !== 'test'))) {
1015
+ return true;
1016
+ }
685
1017
  }
686
- if (foundInSource) productionNames.add(n);
1018
+ return false;
1019
+ };
1020
+ const productionNames = new Set();
1021
+ for (const n of affectedNames) {
1022
+ if (isProductionName(n)) productionNames.add(n);
687
1023
  }
688
1024
  // Fall back to full set if filtering removed everything (e.g., test-only project)
689
1025
  const namesForCoverage = productionNames.size > 0 ? productionNames : affectedNames;
1026
+ const possiblyProduction = [...possiblyNames].filter(isProductionName);
690
1027
 
691
1028
  const coveredSet = new Set();
692
1029
  for (const r of results) for (const f of r.coveredFunctions) {
@@ -698,12 +1035,19 @@ function affectedTests(index, name, options = {}) {
698
1035
  root: blastResult.root, file: blastResult.file, line: blastResult.line,
699
1036
  depth: blastResult.maxDepth,
700
1037
  affectedFunctions: [...namesForCoverage],
1038
+ possiblyAffected: possiblyProduction,
701
1039
  testFiles: results,
1040
+ possiblyAffectedTests: possibleResults,
1041
+ ...(blastResult.account && { account: blastResult.account }),
1042
+ treeAccount: blastResult.treeAccount,
702
1043
  summary: {
703
1044
  totalAffected: namesForCoverage.size,
704
1045
  totalTestFiles: results.length,
705
1046
  coveredFunctions: coveredSet.size,
706
1047
  uncoveredCount: uncovered.length,
1048
+ possiblyAffected: possiblyProduction.length,
1049
+ possiblyAffectedTests: possibleResults.length,
1050
+ unverifiedEdges: blastResult.summary ? blastResult.summary.unverifiedEdges : 0,
707
1051
  },
708
1052
  uncovered,
709
1053
  warnings: blastResult.warnings,