ucn 5.2.2 → 5.3.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/cli/index.js CHANGED
@@ -30,6 +30,16 @@ const { buildPublicParams, isPublicCommand } = require('../core/public-command')
30
30
  const { execute } = require('../core/execute');
31
31
  const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
32
32
  const { clearAllCaches } = require('../core/cache');
33
+ const { commentLines } = require('../core/output/lines');
34
+
35
+ // A downstream consumer such as head may finish before our write drains.
36
+ // Handle the pipe closure without an unhandled Node error or bypassing cache
37
+ // cleanup. Other write failures are real command errors.
38
+ process.stdout.on('error', error => {
39
+ if (error.code === 'EPIPE') return;
40
+ process.stderr.write(`Error writing stdout: ${error.message}\n`);
41
+ process.exitCode = 2;
42
+ });
33
43
 
34
44
  let activeCanonicalCommand = null;
35
45
 
@@ -315,6 +325,8 @@ function parseFlags(tokens) {
315
325
  functions: tokens.includes('--functions') || undefined,
316
326
  hot: tokens.includes('--hot') || undefined,
317
327
  diverse: tokens.includes('--diverse') || undefined,
328
+ raw: tokens.includes('--raw') || undefined,
329
+ lines: tokens.includes('--lines') || undefined,
318
330
  git: tokens.includes('--git') || undefined,
319
331
  className: getValueFlag('--class-name'),
320
332
  // Explicit line pin (fix #249: our own disambiguation notes advertise
@@ -396,7 +408,7 @@ if (unknownFlags.length > 0) {
396
408
  emitCliError(
397
409
  `Unknown flag(s): ${unknownFlags.join(', ')}. Use --help to see available flags.`,
398
410
  );
399
- process.exit(1);
411
+ process.exit(flags.lines || flags.raw ? 2 : 1);
400
412
  }
401
413
 
402
414
  // Validate numeric flag values up front so bad input fails before we build
@@ -407,7 +419,7 @@ try {
407
419
  } catch (e) {
408
420
  if (e instanceof FlagValidationError) {
409
421
  emitCliError(e.message);
410
- process.exit(1);
422
+ process.exit(flags.lines || flags.raw ? 2 : 1);
411
423
  }
412
424
  throw e;
413
425
  }
@@ -456,6 +468,16 @@ function formatCliText(command, result, params, execution, displayFlags) {
456
468
  ...execution,
457
469
  surface: 'cli',
458
470
  });
471
+ // --lines / --raw are pipe surfaces: records are compact, a truncated
472
+ // function body is worse than a long one, and an empty answer must stay
473
+ // empty (grep prints nothing and exits 1). An explicit --max-chars acts
474
+ // as a fail-before-output guard, never a lossy source/record truncation.
475
+ if (params?.lines || params?.raw) {
476
+ if (displayFlags?.maxChars && text.length > displayFlags.maxChars) {
477
+ fail(`Output exceeds --max-chars=${displayFlags.maxChars}; shell output cannot be truncated. Narrow the query, use --limit/--max-lines, or omit --max-chars.`);
478
+ }
479
+ return text;
480
+ }
459
481
  return applyOutputBudget(text, {
460
482
  command,
461
483
  maxChars: displayFlags?.maxChars,
@@ -465,6 +487,36 @@ function formatCliText(command, result, params, execution, displayFlags) {
465
487
  }).text;
466
488
  }
467
489
 
490
+ /**
491
+ * Print a formatted answer the way the mode asks for it (fix #341).
492
+ * --lines: `path:line:text` records on stdout, `# ` comment lines (ACCOUNT,
493
+ * notes) on stderr, exit 1 when nothing matched — grep's own contract.
494
+ * --raw: the text verbatim with exactly one trailing newline.
495
+ */
496
+ function emitCliText(text, params, json, note) {
497
+ if (!json && params?.lines) {
498
+ const records = [];
499
+ const comments = [];
500
+ for (const line of String(text).split('\n')) {
501
+ if (line === '') continue;
502
+ (line.startsWith('# ') ? comments : records).push(line);
503
+ }
504
+ if (records.length > 0) process.stdout.write(records.join('\n') + '\n');
505
+ if (comments.length > 0) process.stderr.write(comments.join('\n') + '\n');
506
+ if (records.length === 0) process.exitCode = Math.max(process.exitCode || 0, 1);
507
+ return;
508
+ }
509
+ if (!json && params?.raw) {
510
+ const body = String(text);
511
+ process.stdout.write(body.endsWith('\n') ? body : body + '\n');
512
+ // Code lines are never reinterpreted (a Python comment starts with
513
+ // "# " too), so the note travels on its own channel.
514
+ if (note) process.stderr.write(commentLines(formatSurfaceMessage(note, 'cli')).join('\n') + '\n');
515
+ return;
516
+ }
517
+ console.log(text);
518
+ }
519
+
468
520
  // ============================================================================
469
521
  // MAIN
470
522
  // ============================================================================
@@ -555,7 +607,7 @@ function main() {
555
607
  if (!(e instanceof CommandError)) {
556
608
  emitCliError(`Error: ${e.message}`);
557
609
  }
558
- process.exitCode = 1;
610
+ process.exitCode = flags.lines || flags.raw ? 2 : 1;
559
611
  }
560
612
  }
561
613
 
@@ -665,11 +717,11 @@ function runFileCommand(filePath, command, arg) {
665
717
  const execution = execute(index, canonical, params);
666
718
  const { ok, result, error } = execution;
667
719
  if (!ok) fail(formatSurfaceMessage(error, 'cli'));
668
- console.log(flags.json
720
+ emitCliText(flags.json
669
721
  ? output.formatPublicJson(canonical, result, params, {
670
722
  ...execution, surface: 'cli',
671
723
  })
672
- : formatCliText(canonical, result, params, execution, scopedFlags));
724
+ : formatCliText(canonical, result, params, execution, scopedFlags), params, flags.json, execution.note);
673
725
  }
674
726
 
675
727
  // ============================================================================
@@ -737,11 +789,12 @@ function runProjectCommand(rootDir, command, arg) {
737
789
  if (!publicExecution.ok) {
738
790
  fail(formatSurfaceMessage(publicExecution.error, 'cli'));
739
791
  }
740
- console.log(flags.json
792
+ emitCliText(flags.json
741
793
  ? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
742
794
  ...publicExecution, surface: 'cli',
743
795
  })
744
- : formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags));
796
+ : formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags),
797
+ publicParams, flags.json, publicExecution.note);
745
798
  // A gate that could not run (check outside git / bad base ref) must not
746
799
  // exit 0 — CI gating on the exit code would read "could not run" as "passed".
747
800
  process.exitCode = Math.max(process.exitCode || 0,
@@ -750,7 +803,7 @@ function runProjectCommand(rootDir, command, arg) {
750
803
  if (!(e instanceof CommandError)) {
751
804
  emitCliError(`Error: ${e.message}`);
752
805
  }
753
- process.exitCode = 1;
806
+ process.exitCode = flags.lines || flags.raw ? 2 : 1;
754
807
  } finally {
755
808
  // Save cache after command execution so callsCache populated
756
809
  // by findCallers/findCallees gets persisted to disk.
@@ -796,11 +849,12 @@ function runGlobCommand(pattern, command, arg) {
796
849
  if (!publicExecution.ok) {
797
850
  fail(formatSurfaceMessage(publicExecution.error, 'cli'));
798
851
  }
799
- console.log(flags.json
852
+ emitCliText(flags.json
800
853
  ? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
801
854
  ...publicExecution, surface: 'cli',
802
855
  })
803
- : formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags));
856
+ : formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags),
857
+ publicParams, flags.json, publicExecution.note);
804
858
  process.exitCode = Math.max(process.exitCode || 0,
805
859
  resultExitCode(canonical, publicExecution.result));
806
860
  }
@@ -860,6 +914,13 @@ Common flags:
860
914
  --range=N-M (source with --file=PATH)
861
915
  --base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
862
916
  --max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
917
+ --lines find/usages/search/show/impact: grep -n shape, one path:line:text
918
+ record per line (tags after a tab: # unverified: <reason>, # import,
919
+ # callee); accounting and notes go to stderr as "# " lines; exit 1
920
+ when nothing matched; exit 2 on errors. No default result cap.
921
+ show defaults to callers; --sections=callers,callees selects bands.
922
+ --raw source: full code, no header or gutter (including large classes).
923
+ Shell modes fail before output if an explicit --max-chars is exceeded.
863
924
  Cache: per-user by default; set UCN_CACHE_DIR to override the cache root.
864
925
 
865
926
  Accepted flags by command:
package/core/cache.js CHANGED
@@ -690,7 +690,14 @@ function clearAllCaches() {
690
690
  // types for values bound from declared map, slice, and array indexes.
691
691
  // v208 (fix #335): Go indexed-value calls preserve receiver-root/field
692
692
  // provenance so sibling-file container declarations resolve query-time.
693
- const CACHE_FORMAT_VERSION = 208;
693
+ // v210 (fixes #337-#339): importDetails persisted for every language (was Python-only),
694
+ // require(path.join(__dirname, ...)) composes to a static relative specifier,
695
+ // and import records carry deferredReason (function-local / type-checking / type-only).
696
+ // v211: path-utility composition requires unshadowed binding evidence; mixed
697
+ // default/type imports and inline type re-exports preserve execution timing.
698
+ // Python TYPE_CHECKING guards require typing ownership and no rebinding.
699
+ // v212 (fix #342): extendsGraph/extendedByGraph no longer persisted (rebuilt on load).
700
+ const CACHE_FORMAT_VERSION = 212;
694
701
  const USAGE_CACHE_FILE = 'usage-results.json';
695
702
 
696
703
  /**
@@ -901,9 +908,13 @@ function saveCache(index, cachePath) {
901
908
  symbols: strippedSymbols,
902
909
  importGraph: relGraph(index.importGraph),
903
910
  exportGraph: relGraph(index.exportGraph),
904
- // extendsGraph/extendedByGraph use class names as keys (not file paths)
905
- extendsGraph: Array.from(index.extendsGraph.entries()),
906
- extendedByGraph: Array.from(index.extendedByGraph.entries()),
911
+ // extendsGraph/extendedByGraph are NOT persisted (fix #342): their
912
+ // entries carry absolute file paths in the build-time spelling, and
913
+ // the cache key is realpath-normalized — a root reached through a
914
+ // symlink (/var → /private/var on macOS) loaded a graph whose files
915
+ // matched nothing, so an overriding Go embed read as a non-overriding
916
+ // subclass and confirmed two false callers. loadCache rebuilds both
917
+ // from the rehydrated symbols (8ms on 1037 files).
907
918
  failedFiles: index.failedFiles
908
919
  ? Array.from(index.failedFiles).map(f => path.relative(root, f))
909
920
  : [],
@@ -1116,13 +1127,8 @@ function loadCache(index, cachePath) {
1116
1127
  index.buildTime = cacheData.buildTime;
1117
1128
 
1118
1129
  // Restore optional graphs if present
1119
- // extendsGraph/extendedByGraph use class names as keys (not file paths)
1120
- if (Array.isArray(cacheData.extendsGraph)) {
1121
- index.extendsGraph = new Map(cacheData.extendsGraph);
1122
- }
1123
- if (Array.isArray(cacheData.extendedByGraph)) {
1124
- index.extendedByGraph = new Map(cacheData.extendedByGraph);
1125
- }
1130
+ // extendsGraph/extendedByGraph are derived below from the rehydrated
1131
+ // symbols (fix #342) — never read from the payload.
1126
1132
 
1127
1133
  // Prepare lazy calls cache loading — load manifest but defer shard parsing.
1128
1134
  // Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
@@ -1194,13 +1200,16 @@ function loadCache(index, cachePath) {
1194
1200
  }
1195
1201
  }
1196
1202
 
1197
- // Only rebuild graphs if config changed (e.g., aliases modified)
1203
+ // Only rebuild the import graph if config changed (e.g., aliases
1204
+ // modified); it is persisted with relative paths. The inheritance
1205
+ // graph is always derived from the rehydrated symbols (fix #342) so
1206
+ // its file paths agree with index.root whatever spelling built it.
1198
1207
  const currentConfigHash = crypto.createHash('md5')
1199
1208
  .update(JSON.stringify(index.config || {})).digest('hex');
1200
1209
  if (currentConfigHash !== cacheData.configHash) {
1201
1210
  index.buildImportGraph();
1202
- index.buildInheritanceGraph();
1203
1211
  }
1212
+ index.buildInheritanceGraph();
1204
1213
 
1205
1214
  loadUsageCache(index, cacheFile);
1206
1215
 
package/core/callers.js CHANGED
@@ -592,10 +592,22 @@ function findCallers(index, name, options = {}) {
592
592
  // completion. Phase 2 still only enriches the first `maxResults` items —
593
593
  // file reads stay bounded, but the candidate count reflects the true total.
594
594
  const needsTotal = !!options.needsTotal;
595
- const localTypeCache = new Map(); // `${filePath}:${startLine}` -> localTypes Map or null
596
- const returnFlowCache = new Map(); // filePath -> return-type-flow map (see _buildReturnTypeFlowMap)
597
- const foldCtxCache = new Map(); // filePath -> chained-receiver fold context (fix #258)
598
- const pythonIndexedReceiverCache = new Map();
595
+ // Per-file query-time derivations that depend only on a file's immutable
596
+ // call records — never on the pinned target — so they live for the whole
597
+ // OPERATION, not one findCallers call (fix #340): stats --hot / repo run
598
+ // findCallers for hundreds of candidates over the same files, and
599
+ // rebuilding fold contexts (producer index, flow maps) and typed-local
600
+ // maps per candidate made grpc-go's `repo` cost 40s (1375 calls).
601
+ if (!index._opFindCallersCaches) {
602
+ index._opFindCallersCaches = {
603
+ localTypeCache: new Map(), // `${filePath}:${startLine}` -> localTypes Map or null
604
+ returnFlowCache: new Map(), // filePath -> return-type-flow map (see _buildReturnTypeFlowMap)
605
+ foldCtxCache: new Map(), // filePath -> chained-receiver fold context (fix #258)
606
+ pythonIndexedReceiverCache: new Map(),
607
+ };
608
+ }
609
+ const { localTypeCache, returnFlowCache, foldCtxCache, pythonIndexedReceiverCache } =
610
+ index._opFindCallersCaches;
599
611
 
600
612
  // Use inverted callee index to skip files that don't contain calls to this name
601
613
  let calleeFiles = index.getCalleeFiles(name);
@@ -1157,12 +1169,8 @@ function findCallers(index, name, options = {}) {
1157
1169
  receiverTypeFlowFile: path.join(index.root, project.rel),
1158
1170
  };
1159
1171
  } else {
1160
- const projectish = bindings.some(b => {
1161
- const mod = String(b.module || '');
1162
- const first = mod.split(/[./]/).filter(Boolean)[0];
1163
- return mod.startsWith('.') ||
1164
- (first && _projectTopLevelNames(index).has(first));
1165
- });
1172
+ const projectish = bindings.some(b =>
1173
+ _unresolvedModuleIsGap(index, b.module, b));
1166
1174
  const via = `${bindings[0].module}.${bindings[0].name}`;
1167
1175
  if (BUILTIN_RECEIVER_TYPES.has(call.receiverType)) {
1168
1176
  // Stable stdlib runtime classes (StringIO,
@@ -1657,10 +1665,7 @@ function findCallers(index, name, options = {}) {
1657
1665
  for (const binding of cbNameBindings) {
1658
1666
  const rel = fileEntry.moduleResolved?.[binding.module];
1659
1667
  if (!rel) {
1660
- const mod = String(binding.module || '');
1661
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
1662
- if (mod.startsWith('.') ||
1663
- (firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
1668
+ if (_unresolvedModuleIsGap(index, binding.module, binding)) {
1664
1669
  cbBindingUnknown = true;
1665
1670
  }
1666
1671
  continue;
@@ -2851,10 +2856,7 @@ function findCallers(index, name, options = {}) {
2851
2856
  // relative (project-internal by construction)
2852
2857
  // or its first segment names a project path
2853
2858
  // (resolution gap, not externality evidence)
2854
- const mod = String(b.module);
2855
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
2856
- if (mod.startsWith('.') ||
2857
- (firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
2859
+ if (_unresolvedModuleIsGap(index, b.module, b)) {
2858
2860
  undetermined = true;
2859
2861
  }
2860
2862
  continue;
@@ -3129,10 +3131,7 @@ function findCallers(index, name, options = {}) {
3129
3131
  const rel = recvSubmoduleRel ||
3130
3132
  (fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]);
3131
3133
  if (!rel) {
3132
- const mod = String(b.module);
3133
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
3134
- if (mod.startsWith('.') ||
3135
- (firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
3134
+ if (_unresolvedModuleIsGap(index, b.module, b)) {
3136
3135
  projectish = true;
3137
3136
  undetermined = true;
3138
3137
  }
@@ -7956,10 +7955,7 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7956
7955
  // project `info`) is not identity evidence. Same externality
7957
7956
  // test as #209 module ownership: relative or project-ish
7958
7957
  // modules are resolver gaps, never externality evidence.
7959
- const mod = String(binding.module);
7960
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
7961
- if (!mod.startsWith('.') &&
7962
- !(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
7958
+ if (!_unresolvedModuleIsGap(index, binding.module, binding)) {
7963
7959
  const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
7964
7960
  if (!map) map = new Map();
7965
7961
  const key = `${scope}:${call.assignedTo}`;
@@ -9103,9 +9099,7 @@ function _structuralQualifiedReceiverOrigin(index, fileEntry, qualifier, typeNam
9103
9099
  fromFile: path.join(index.root, rel),
9104
9100
  };
9105
9101
  }
9106
- const first = moduleName.split(/[./]/).filter(Boolean)[0];
9107
- if (moduleName.startsWith('.') ||
9108
- (first && _projectTopLevelNames(index).has(first))) {
9102
+ if (_unresolvedModuleIsGap(index, moduleName)) {
9109
9103
  projectish = true;
9110
9104
  }
9111
9105
  }
@@ -9296,10 +9290,7 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
9296
9290
  // Unresolved: relative or project-ish → resolver gap, not
9297
9291
  // a terminal; clearly external → that path pins outside
9298
9292
  // the project (dead end, consistent with #209c).
9299
- const mod = String(module);
9300
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
9301
- if (mod.startsWith('.') ||
9302
- (firstSeg && _projectTopLevelNames(index).has(firstSeg))) unknown = true;
9293
+ if (_unresolvedModuleIsGap(index, module)) unknown = true;
9303
9294
  return;
9304
9295
  }
9305
9296
  next.push([path.join(index.root, rel), nextAttr]);
@@ -9860,6 +9851,25 @@ function _projectTopLevelNames(index) {
9860
9851
  return names;
9861
9852
  }
9862
9853
 
9854
+ /**
9855
+ * Is an UNRESOLVED module specifier a resolver gap rather than externality
9856
+ * evidence? (fix #337b) Relative specifiers and first segments naming a
9857
+ * project top-level path were already gaps (#209); a NON-LITERAL specifier —
9858
+ * `require(path.join(__dirname, ...))`, `require(name)`, template paths — is
9859
+ * one too: the parser records the expression text as the module, which can
9860
+ * never match a package name, so judging it external excluded true callers as
9861
+ * `other-definition-import`. Statically composable `__dirname` paths are
9862
+ * resolved parser-side; whatever stays dynamic must route 'unknown'.
9863
+ */
9864
+ function _unresolvedModuleIsGap(index, module, binding) {
9865
+ const mod = String(module || '');
9866
+ if (binding && binding.dynamic) return true;
9867
+ if (mod.startsWith('.')) return true;
9868
+ if (/[()$`{}+\s]/.test(mod)) return true;
9869
+ const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
9870
+ return !!(firstSeg && _projectTopLevelNames(index).has(firstSeg));
9871
+ }
9872
+
9863
9873
  const IDENTITY_TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'enum']);
9864
9874
 
9865
9875
  /**
@@ -10740,10 +10750,9 @@ function _goQualifierNamesImport(index, fieldFile, qualifier) {
10740
10750
  function _iterExternalProducerVia(index, fileEntry, call) {
10741
10751
  if (!fileEntry || langTraits(fileEntry.language)?.typeSystem === 'nominal') return null;
10742
10752
  const externalModule = (mod) => {
10743
- if (!mod || mod.startsWith('.')) return false;
10753
+ if (!mod) return false;
10744
10754
  if (fileEntry.moduleResolved?.[mod]) return false;
10745
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
10746
- return !(firstSeg && _projectTopLevelNames(index).has(firstSeg));
10755
+ return !_unresolvedModuleIsGap(index, mod);
10747
10756
  };
10748
10757
  if (call.isMethod && call.receiverIsModule && call.receiver) {
10749
10758
  const binding = _structuralModuleBindings(fileEntry, call)[0];
@@ -10831,10 +10840,9 @@ function _structuralCompositeModuleOwnership(
10831
10840
 
10832
10841
  function _pythonBuiltinContractAllowed(index, fileEntry, moduleName) {
10833
10842
  const module = String(moduleName || '');
10834
- if (!module || module.startsWith('.')) return false;
10843
+ if (!module) return false;
10835
10844
  if (fileEntry.moduleResolved?.[module]) return false;
10836
- const first = module.split('.')[0];
10837
- return !first || !_projectTopLevelNames(index).has(first);
10845
+ return !_unresolvedModuleIsGap(index, module);
10838
10846
  }
10839
10847
 
10840
10848
  function _structuralImportedReceiverType(index, fileEntry, receiver) {
@@ -10957,10 +10965,7 @@ function _calleeStructuralBindingRoute(index, fileEntry, call, language, binding
10957
10965
  for (const binding of bindings) {
10958
10966
  const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
10959
10967
  if (!rel) {
10960
- const mod = String(binding.module || '');
10961
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
10962
- if (mod.startsWith('.') ||
10963
- (firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
10968
+ if (_unresolvedModuleIsGap(index, binding.module, binding)) {
10964
10969
  sawProjectish = true;
10965
10970
  sawUnknown = true;
10966
10971
  }
@@ -11008,10 +11013,7 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
11008
11013
  const enqueue = (module, nextAttr) => {
11009
11014
  const rel = fe.moduleResolved && fe.moduleResolved[module];
11010
11015
  if (!rel) {
11011
- const mod = String(module || '');
11012
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
11013
- if (mod.startsWith('.') ||
11014
- (firstSeg && _projectTopLevelNames(index).has(firstSeg))) unknown = true;
11016
+ if (_unresolvedModuleIsGap(index, module)) unknown = true;
11015
11017
  return;
11016
11018
  }
11017
11019
  next.push([path.join(index.root, rel), nextAttr]);
@@ -15600,10 +15602,7 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
15600
15602
  (binding && fileEntry.moduleResolved &&
15601
15603
  fileEntry.moduleResolved[binding.module]);
15602
15604
  if (binding && !rel) {
15603
- const mod = String(binding.module);
15604
- const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
15605
- if (!mod.startsWith('.') &&
15606
- !(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
15605
+ if (!_unresolvedModuleIsGap(index, binding.module, binding)) {
15607
15606
  return {
15608
15607
  externalVia: `${record.receiver}.${name}`,
15609
15608
  ...(/^[A-Z]/.test(name) && { externalConcrete: true }),
@@ -16129,4 +16128,4 @@ function findCallbackUsages(index, name) {
16129
16128
  return usages;
16130
16129
  }
16131
16130
 
16132
- module.exports = { getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };
16131
+ module.exports = { _unresolvedModuleIsGap, getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };
package/core/execute.js CHANGED
@@ -464,6 +464,14 @@ const PUBLIC_COMMAND_SET = new Set(CANONICAL_COMMANDS);
464
464
  * glob, file, and programmatic callers receive the same answer.
465
465
  */
466
466
  function validatePublicParams(command, p) {
467
+ if (p.raw && command !== 'source') return '--raw is supported only by source.';
468
+ if (p.lines && !['find', 'usages', 'search', 'show', 'impact'].includes(command)) {
469
+ return '--lines is supported by find, usages, search, show, and impact.';
470
+ }
471
+ if (p.lines && command === 'show' && p.sections) {
472
+ const parsed = parseSections(p.sections, ['callers'], new Set(['callers', 'callees']), 'show --lines');
473
+ if (parsed.error) return parsed.error;
474
+ }
467
475
  const hasName = typeof p.name === 'string' && p.name.trim() !== '';
468
476
  const gitScope = p.staged || (typeof p.base === 'string' && p.base.trim() !== '');
469
477
  if ((command === 'impact' || command === 'check') && hasName && gitScope) {
@@ -569,7 +577,7 @@ const HANDLERS = {
569
577
  } : query;
570
578
  const parsed = parseSections(
571
579
  p.sections,
572
- ['summary', 'callers', 'callees'],
580
+ p.lines ? ['callers'] : ['summary', 'callers', 'callees'],
573
581
  SHOW_SECTIONS,
574
582
  'show',
575
583
  );
@@ -693,6 +701,8 @@ const HANDLERS = {
693
701
  const notes = [
694
702
  ...(resolved.warnings || []).map(warning => warning.message),
695
703
  response.note,
704
+ ...(p.raw ? (response.result.entries || []).filter(entry => entry.truncated)
705
+ .map(entry => `Source truncated: ${entry.match.relativePath}:${entry.match.startLine}; showing ${entry.shownLines || entry.maxLines} of ${entry.totalLines} lines (--max-lines).`) : []),
696
706
  ].filter(Boolean);
697
707
  return notes.length > 0
698
708
  ? { ...response, note: combineNotes(notes) }
@@ -1174,6 +1184,9 @@ const HANDLERS = {
1174
1184
  file: p.file,
1175
1185
  className: p.className,
1176
1186
  exact: p.exact || false,
1187
+ // A shell definition listing renders no activity counts. Avoid a
1188
+ // full pinned caller query for every definition in a wildcard.
1189
+ skipCounts: !!p.lines,
1177
1190
  exclude,
1178
1191
  in: p.in,
1179
1192
  });
@@ -1430,7 +1443,7 @@ const HANDLERS = {
1430
1443
  exclude,
1431
1444
  in: p.in,
1432
1445
  file: p.file,
1433
- top: topVal || 50,
1446
+ top: topVal || (p.lines ? undefined : 50),
1434
1447
  });
1435
1448
  if (result.meta.error) return { ok: false, error: result.meta.error };
1436
1449
  const unsupported = (!p.regex && (p.term || p.name))
@@ -1456,7 +1469,7 @@ const HANDLERS = {
1456
1469
  // default at 500 and retain total/truncated metadata for controlled
1457
1470
  // expansion with an explicit top/limit.
1458
1471
  const topVal = num(p.top, undefined) || num(p.limit, undefined);
1459
- const effectiveLimit = topVal || 500;
1472
+ const effectiveLimit = topVal || (p.lines ? undefined : 500);
1460
1473
  const result = index.search(p.term, {
1461
1474
  codeOnly: p.codeOnly || false,
1462
1475
  context: num(p.context, 0),
@@ -2014,9 +2027,12 @@ const HANDLERS = {
2014
2027
 
2015
2028
  if (matches.length > 1 && p.all) {
2016
2029
  for (const m of matches) {
2017
- const code = readAndExtract(m);
2030
+ const fullCode = readAndExtract(m);
2018
2031
  const totalLines = m.endLine - m.startLine + 1;
2019
- entries.push({ match: m, code, totalLines, summaryMode: false, truncated: false });
2032
+ const truncated = !!maxLines && totalLines > maxLines;
2033
+ const code = truncated ? fullCode.split('\n').slice(0, maxLines).join('\n') : fullCode;
2034
+ entries.push({ match: m, code, totalLines, summaryMode: false, truncated,
2035
+ ...(truncated && { maxLines }) });
2020
2036
  }
2021
2037
  return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
2022
2038
  }
@@ -2035,7 +2051,7 @@ const HANDLERS = {
2035
2051
  const totalLines = match.endLine - match.startLine + 1;
2036
2052
 
2037
2053
  // Large class summary mode (>200 lines, no maxLines)
2038
- if (totalLines > 200 && !maxLines) {
2054
+ if (totalLines > 200 && !maxLines && !p.raw) {
2039
2055
  const methods = index.findMethodsForType(match.name, match);
2040
2056
  entries.push({ match, code: null, methods, totalLines, summaryMode: true, truncated: false });
2041
2057
  return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
@@ -2457,6 +2473,8 @@ function execute(index, command, params = {}) {
2457
2473
  // logged params object; a frozen object silently no-oped the
2458
2474
  // normalizers and returned wrong not-found answers).
2459
2475
  params = { ...params };
2476
+ // Listing completeness belongs to execution, including direct API calls.
2477
+ if (params.lines) params.all = true;
2460
2478
  try {
2461
2479
  if (PUBLIC_COMMAND_SET.has(command)) {
2462
2480
  const validationError = validatePublicParams(command, params);