ucn 5.5.0 → 5.5.2

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.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +41 -27
  6. package/cli/index.js +6 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +106 -31
  9. package/core/analysis.js +266 -127
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +144 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +5111 -1024
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +228 -114
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +1202 -157
  63. package/core/rust-modules.js +21 -3
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +157 -46
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +548 -89
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  const { langTraits } = require('../languages');
28
+ const { sameNode } = require('../languages/utils');
28
29
 
29
30
  const WILD = '\u0000*';
30
31
  const MODULE_RECEIVER = Object.freeze({ kind: 'module' });
@@ -501,6 +502,224 @@ function classifyReceiver(node, language) {
501
502
  return null;
502
503
  }
503
504
 
505
+ // ---------------------------------------------------------------------------
506
+ // Names one call away (fix #428)
507
+ // ---------------------------------------------------------------------------
508
+
509
+ // Callables whose parameters a reflective name can be (grammar node types).
510
+ const PARAMETER_OWNERS = new Set([
511
+ 'function_declaration', 'generator_function_declaration', 'function_expression', 'generator_function',
512
+ 'arrow_function', 'method_definition', 'function_definition', 'method_declaration',
513
+ 'constructor_declaration', 'local_function_statement', 'func_literal',
514
+ ]);
515
+ const PARAMETER_LISTS = new Set(['formal_parameters', 'parameters', 'parameter_list']);
516
+ const VARIADIC_PARAMETERS = new Set(['rest_pattern', 'list_splat_pattern', 'dictionary_splat_pattern',
517
+ 'spread_parameter', 'variadic_parameter_declaration', 'variadic_parameter']);
518
+
519
+ /** The parameter names of a callable in declaration order (null if unknown). */
520
+ function _parameterNames(fn) {
521
+ const list = fn.childForFieldName('parameters') ||
522
+ (fn.namedChildren || []).find(child => PARAMETER_LISTS.has(child.type));
523
+ if (!list) {
524
+ // `x => ..`: a single bare parameter.
525
+ const single = fn.childForFieldName('parameter');
526
+ return single?.type === 'identifier' ? [single.text] : null;
527
+ }
528
+ const names = [];
529
+ for (const param of list.namedChildren) {
530
+ if (param.type === 'comment') continue;
531
+ if (param.type === 'identifier') { names.push(param.text); continue; }
532
+ // A Go declaration may name several parameters of one type.
533
+ if (param.type === 'parameter_declaration' || param.type === 'variadic_parameter_declaration') {
534
+ const ids = param.namedChildren.filter(child => child.type === 'identifier');
535
+ if (ids.length === 0) names.push(null);
536
+ for (const id of ids) names.push(id.text);
537
+ continue;
538
+ }
539
+ const named = param.childForFieldName('name') || param.childForFieldName('pattern') ||
540
+ param.childForFieldName('left') ||
541
+ (param.type === 'typed_parameter' ? param.namedChild(0) : null);
542
+ names.push(named?.type === 'identifier' ? named.text : null);
543
+ }
544
+ return names;
545
+ }
546
+
547
+ /** The name a callable is called by: its own, or the binding it is assigned to. */
548
+ function _callableName(fn) {
549
+ const own = fn.childForFieldName('name');
550
+ if (own) return own.text;
551
+ const parent = fn.parent;
552
+ if (parent?.type === 'variable_declarator') return parent.childForFieldName('name')?.text || null;
553
+ if (parent?.type === 'pair') return parent.childForFieldName('key')?.text?.replace(/^["']|["']$/g, '') || null;
554
+ if (parent?.type === 'assignment_expression') {
555
+ const left = parent.childForFieldName('left');
556
+ if (left?.type === 'identifier') return left.text;
557
+ if (left?.type === 'member_expression') return left.childForFieldName('property')?.text || null;
558
+ }
559
+ return null;
560
+ }
561
+
562
+ /** Does `body` assign the name anywhere (nested callables included)? */
563
+ function _assignsName(body, name) {
564
+ const stack = [body];
565
+ while (stack.length > 0) {
566
+ const node = stack.pop();
567
+ if (_bindingPairs(node).some(([bound]) => bound === name)) return true;
568
+ for (const child of node.namedChildren || []) stack.push(child);
569
+ }
570
+ return false;
571
+ }
572
+
573
+ /**
574
+ * A reflective name that is a parameter of an enclosing callable, never
575
+ * rebound: { name, line, index } (the call-site argument position), else
576
+ * null. Its value is the argument the callable's callers pass.
577
+ */
578
+ function _keyParameter(ident, language) {
579
+ if (ident?.type !== 'identifier') return null;
580
+ for (let fn = ident.parent; fn; fn = fn.parent) {
581
+ if (CLASS_NODES.has(fn.type)) return null;
582
+ if (!PARAMETER_OWNERS.has(fn.type)) continue;
583
+ const names = _parameterNames(fn);
584
+ if (!names) return null;
585
+ let position = names.indexOf(ident.text);
586
+ if (position < 0) continue; // captured from an outer callable
587
+ const name = _callableName(fn);
588
+ if (!name || _assignsName(fn.childForFieldName('body') || fn, ident.text)) return null;
589
+ // A method's receiver parameter (`self`, `cls`) is not a call
590
+ // argument of a bound call.
591
+ const selfNames = langTraits(language)?.selfParam || [];
592
+ let owner = fn.parent?.type === 'decorated_definition' ? fn.parent.parent : fn.parent;
593
+ if (fn.type === 'function_definition' && owner?.type === 'block' &&
594
+ owner.parent?.type === 'class_definition' && selfNames.includes(names[0])) {
595
+ if (position === 0) return null;
596
+ position--;
597
+ }
598
+ // The class a method belongs to: a call naming another class as its
599
+ // qualifier is no call of it. A C# extension's `this` parameter is
600
+ // the receiver of an extension call (`t.M(x)`).
601
+ const ownerClass = _enclosingClass(fn, false);
602
+ const ownerName = ownerClass ? _classInfo(ownerClass)?.name || null : null;
603
+ const params = (fn.childForFieldName('parameters')?.namedChildren || []).filter(child => child.type !== 'comment');
604
+ const firstParam = params.find(child => child.type === 'parameter');
605
+ const extension = !!firstParam?.children?.some(child => child.type === 'modifier' && child.text === 'this');
606
+ // A call passing more arguments than a fixed parameter list takes
607
+ // is not a call of it.
608
+ const variadic = params.some(param => VARIADIC_PARAMETERS.has(param.type) ||
609
+ param.children?.some(child => child.type === 'modifier' && child.text === 'params') ||
610
+ param.namedChildren?.some(child => VARIADIC_PARAMETERS.has(child.type)));
611
+ const maxArgs = variadic ? null : names.length - (names.length > 0 && selfNames.includes(names[0]) &&
612
+ fn.type === 'function_definition' && owner?.type === 'block' && owner.parent?.type === 'class_definition' ? 1 : 0);
613
+ return { name, line: fn.startPosition.row + 1, index: position,
614
+ ...(ownerName && { owner: ownerName }), ...(extension && { extension: true }),
615
+ ...(Number.isInteger(maxArgs) && { maxArgs }) };
616
+ }
617
+ return null;
618
+ }
619
+
620
+ const MAX_KEY_CALLS = 256;
621
+ // Characters after a call's name its arguments are looked for in, when the
622
+ // record keeps only the name's position.
623
+ const KEY_CALL_WINDOW = 4096;
624
+
625
+ /**
626
+ * The names a parameter-keyed site receives (fix #428): the string-shaped
627
+ * arguments at the parameter's position in the calls naming its callable,
628
+ * read from each call's own AST (one hop). Null when no call passes one.
629
+ */
630
+ function resolveKeyParameter(index, site, language, onlyFiles = null, onlyName = null) {
631
+ const key = site.keyParameter;
632
+ const { getCachedCalls } = require('./callers');
633
+ const { getParser, safeParse } = require('../languages');
634
+ const files = [...(onlyFiles || index.getCalleeFiles?.(key.name) || [])].sort();
635
+ const exact = new Set();
636
+ let seen = 0;
637
+ for (const file of files) {
638
+ const entry = index.files.get(file);
639
+ if (!entry || _family(entry.language) !== _family(language)) continue;
640
+ // A call qualified by another class's name never calls the method;
641
+ // an extension call passes the `this` argument as its receiver.
642
+ const shift = call => key.extension && call.isMethod && call.receiver && call.receiver !== key.owner ? 1 : 0;
643
+ const position = call => key.index - shift(call);
644
+ let calls = (getCachedCalls(index, file) || []).filter(call => call.name === key.name &&
645
+ !call.isFunctionReference && position(call) >= 0 &&
646
+ !(Number.isInteger(key.maxArgs) && Number.isInteger(call.argCount) && call.argCount > key.maxArgs - shift(call)) &&
647
+ !(Number.isInteger(call.argCount) && call.argCount <= position(call)) &&
648
+ !(key.owner && call.receiverIsTypeQualified && call.receiver && call.receiver !== key.owner));
649
+ if (calls.length === 0) continue;
650
+ seen += calls.length;
651
+ if (seen > MAX_KEY_CALLS) return null;
652
+ let content;
653
+ try { content = index._readFile(file); } catch { continue; }
654
+ // A literal argument lies in the call's text: a call whose text
655
+ // spells no quoted `onlyName` cannot pass it, and needs no parse.
656
+ if (onlyName) {
657
+ const quoted = ['"', "'", '`'].map(quote => `${quote}${onlyName}${quote}`);
658
+ calls = calls.filter(call => {
659
+ const start = call.callStart ?? call.callSite?.start;
660
+ if (!Number.isInteger(start)) return true;
661
+ const text = content.slice(start, call.callEnd ?? start + KEY_CALL_WINDOW);
662
+ return quoted.some(spelling => text.includes(spelling));
663
+ });
664
+ if (calls.length === 0) continue;
665
+ }
666
+ const tree = index._getParsedTree?.(file, content, entry.language) || safeParse(getParser(entry.language), content);
667
+ if (!tree) continue;
668
+ const api = langTraits(entry.language)?.reflectionApi || {};
669
+ const ctx = { formatCalls: api.formatCalls || null, resolve: makeResolver() };
670
+ for (const call of calls) {
671
+ const offset = call.callStart ?? call.callSite?.start;
672
+ if (!Number.isInteger(offset)) continue;
673
+ let node = tree.rootNode.descendantForIndex(offset);
674
+ for (let depth = 0; node && depth < 6 && !CALL_NODE_TYPES.includes(node.type); depth++) node = node.parent;
675
+ const args = node ? _callArgs(node) : null;
676
+ // Keyword, spread or missing arguments do not bind by position.
677
+ if (!args || args.slice(0, position(call) + 1).some(arg =>
678
+ /keyword_argument|spread|splat|named_argument/.test(arg.type) ||
679
+ (arg.type === 'argument' && arg.childForFieldName('name')))) continue;
680
+ let arg = args[position(call)];
681
+ if (arg?.type === 'argument') arg = arg.namedChildren.find(child => child.type !== 'comment') || arg;
682
+ if (!arg) continue;
683
+ const cls = classifyNameExpression(arg, ctx);
684
+ if (cls.name) exact.add(cls.name);
685
+ for (const pattern of cls.patterns || []) if (!pattern.includes('*')) exact.add(pattern);
686
+ }
687
+ }
688
+ return exact.size > 0 ? [...exact].sort() : null;
689
+ }
690
+
691
+ const QUOTED_IDENTIFIER = /(["'`])([A-Za-z_$][\w$]*)\1/g;
692
+
693
+ /**
694
+ * Could a parameter-keyed site receive `name` (fix #428)? Only when a file
695
+ * holding a call of its callable spells `name` as a string literal: the
696
+ * files' quoted identifiers are read once, and the AST resolution runs only
697
+ * for a name they hold.
698
+ */
699
+ function _keyParameterPasses(index, pidx, entry, name) {
700
+ const files = index.getCalleeFiles?.(entry.site.keyParameter.name);
701
+ if (!files || files.size === 0) return false;
702
+ const spelled = [];
703
+ for (const file of files) {
704
+ let quoted = pidx.quoted.get(file);
705
+ if (!quoted) {
706
+ quoted = new Set();
707
+ try {
708
+ for (const match of index._readFile(file).matchAll(QUOTED_IDENTIFIER)) quoted.add(match[2]);
709
+ } catch { /* unreadable: no literal */ }
710
+ pidx.quoted.set(file, quoted);
711
+ }
712
+ if (quoted.has(name)) spelled.push(file);
713
+ }
714
+ if (spelled.length === 0) return false;
715
+ let byName = pidx.keyNames.get(entry);
716
+ if (!byName) pidx.keyNames.set(entry, byName = new Map());
717
+ if (!byName.has(name)) {
718
+ byName.set(name, !!resolveKeyParameter(index, entry.site, entry.language, spelled, name)?.includes(name));
719
+ }
720
+ return byName.get(name);
721
+ }
722
+
504
723
  // ---------------------------------------------------------------------------
505
724
  // Site extraction
506
725
  // ---------------------------------------------------------------------------
@@ -558,7 +777,12 @@ function reflectionSitesInTree(tree, language, code = null) {
558
777
  const push = (node, kind, nameNode, receiverNode, receiverFact) => {
559
778
  const cls = classifyNameExpression(nameNode, ctx);
560
779
  const expression = String(node.text || '');
780
+ // A name that is the enclosing callable's parameter is decided one
781
+ // call away, by what its callers pass (fix #428).
782
+ const keyParameter = cls.dynamic && !cls.patterns && _unwrap(nameNode)?.type === 'identifier'
783
+ ? _keyParameter(_unwrap(nameNode), language) : null;
561
784
  sites.push({
785
+ ...(keyParameter && { keyParameter }),
562
786
  kind,
563
787
  line: node.startPosition.row + 1,
564
788
  expression: expression.length > EXPRESSION_CAP
@@ -595,6 +819,14 @@ function reflectionSitesInTree(tree, language, code = null) {
595
819
  // counts; opaque keys stay the computed-dispatch disclosure.
596
820
  const index = node.childForFieldName('index');
597
821
  const inner = _unwrap(index);
822
+ // `registry[type](..)` with `type` a parameter of the enclosing
823
+ // callable dispatches by the name its callers pass (fix #428).
824
+ if (inner?.type === 'identifier' && node.parent?.type === 'call_expression' &&
825
+ sameNode(node.parent.childForFieldName('function'), node) &&
826
+ _keyParameter(inner, language)) {
827
+ push(node, 'computed', index, node.childForFieldName('object'));
828
+ return;
829
+ }
598
830
  if (!inner || !COMPUTED_PATTERN_KEYS.has(inner.type)) return;
599
831
  const cls = classifyNameExpression(index, ctx);
600
832
  if (cls.patterns && cls.patterns.length > 0) {
@@ -696,6 +928,14 @@ function _longestFragment(pattern) {
696
928
  return best;
697
929
  }
698
930
 
931
+ /** Whether a pattern entry names members by some literal text (fix #433):
932
+ * a fully dynamic name (`eval(code)`, `globals()[key]` with no literal
933
+ * part) reaches every top-level name of its module alike and is attributed
934
+ * to none of them; deadcode still withholds what it may reach. */
935
+ function patternIsAttributable(pattern) {
936
+ return _longestFragment(String(pattern || '').split('\u0000').join('')) !== '';
937
+ }
938
+
699
939
  function _family(language) {
700
940
  return langTraits(language)?.reflectionApi?.family || language;
701
941
  }
@@ -713,6 +953,7 @@ function reflectionPatternIndex(index) {
713
953
  const lengths = new Set();
714
954
  let entryCount = 0;
715
955
  const moduleScoped = new Map();
956
+ const keyParameterSites = [];
716
957
  for (const [file, sites] of byFile) {
717
958
  const fe = index.files.get(file);
718
959
  if (!fe) continue;
@@ -728,6 +969,11 @@ function reflectionPatternIndex(index) {
728
969
  }
729
970
  continue;
730
971
  }
972
+ if (site.keyParameter && !site.patterns) {
973
+ keyParameterSites.push({ file, relativePath: fe.relativePath,
974
+ language: fe.language, family: _family(fe.language), site });
975
+ continue;
976
+ }
731
977
  if (!site.patterns) continue;
732
978
  for (const pattern of site.patterns) {
733
979
  const frag = _longestFragment(pattern);
@@ -740,7 +986,8 @@ function reflectionPatternIndex(index) {
740
986
  }
741
987
  }
742
988
  }
743
- const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped };
989
+ const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped,
990
+ keyParameterSites, quoted: new Map(), keyNames: new Map() };
744
991
  index._reflectionPatternIndex = result;
745
992
  return result;
746
993
  }
@@ -755,8 +1002,12 @@ function patternEntriesFor(index, name, language, file = null) {
755
1002
  if (globMatches(entry.pattern, name)) out.push(entry);
756
1003
  }
757
1004
  }
758
- if (pidx.entryCount === 0) return out;
759
1005
  const family = language ? _family(language) : null;
1006
+ for (const entry of pidx.keyParameterSites) {
1007
+ if (family && entry.family !== family) continue;
1008
+ if (_keyParameterPasses(index, pidx, entry, name)) out.push({ ...entry, pattern: name });
1009
+ }
1010
+ if (pidx.entryCount === 0) return out;
760
1011
  const seenFrag = new Set();
761
1012
  for (const len of pidx.lengths) {
762
1013
  if (len > name.length) break;
@@ -873,5 +1124,7 @@ module.exports = {
873
1124
  reflectionEntryReaches,
874
1125
  reflectionPatternReferences,
875
1126
  reflectionPatternIndex,
1127
+ patternIsAttributable,
876
1128
  resetReflectionIndex,
1129
+ resolveKeyParameter,
877
1130
  };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const cache = require('./cache');
5
+ const { indexedContent, parseViewParts, syntaxMode, SYNTAX_FAMILIES } = require('./source-memo');
6
+ const { langTraits } = require('../languages');
7
+
8
+ /** Cache AST-proved positions only; the caller resolves each binding anew. */
9
+ function callTokenSyntax(index, file, entry, content, range, name, line, derive) {
10
+ if (index.isProvenanceFactIndex || !entry?.hash ||
11
+ !langTraits(entry.language)?.physicalCallTokenSyntax ||
12
+ !Number.isInteger(range.start) || !Number.isInteger(range.end) ||
13
+ range.start < 0 || range.end <= range.start || !Number.isInteger(line) || line <= 0) return derive();
14
+ const tree = index._peekParsedTree?.(file, entry?.language);
15
+ const state = sourceState(index, file, entry, content, tree);
16
+ if (!state?.mode) return derive();
17
+ let facts = state.names.get(name);
18
+ if (!facts) {
19
+ const value = cache.getUsageResult(index, file, name, state.mode);
20
+ const rows = Array.isArray(value) && value.every(row => Array.isArray(row) &&
21
+ row.length === 4 && row.every(Number.isInteger) &&
22
+ row[0] >= 0 && row[1] > row[0] && row[2] > 0 && row[3] >= 0)
23
+ ? value.slice() : [];
24
+ const positions = new Map();
25
+ let duplicate = false;
26
+ for (const row of rows) {
27
+ const key = `${row[0]}:${row[1]}:${row[2]}`;
28
+ if (positions.has(key)) duplicate = true;
29
+ positions.set(key, row[3]);
30
+ }
31
+ facts = duplicate ? { rows: [], positions: new Map() } : { rows, positions };
32
+ state.names.set(name, facts);
33
+ }
34
+ const key = `${range.start}:${range.end}:${line}`;
35
+ const stored = facts.positions.get(key);
36
+ state.lines ||= content.split('\n');
37
+ const lineText = state.lines[line - 1] || '';
38
+ if (!state.lineStarts) {
39
+ let at = 0;
40
+ state.lineStarts = state.lines.map(text => { const start = at; at += text.length + 1; return start; });
41
+ }
42
+ const lineStart = state.lineStarts[line - 1];
43
+ const inRange = column => Number.isInteger(lineStart) && lineStart + column >= range.start &&
44
+ lineStart + column + name.length <= range.end;
45
+ if (!tree && stored !== undefined && inRange(stored) &&
46
+ lineText.slice(stored, stored + name.length) === name) return stored;
47
+ const column = derive();
48
+ if (column === stored) return column;
49
+ if (Number.isInteger(column) && column >= 0 && inRange(column) && Number.isInteger(range.start) &&
50
+ Number.isInteger(range.end) && range.end > range.start && Number.isInteger(line) &&
51
+ line > 0 && lineText.slice(column, column + name.length) === name) {
52
+ const rows = facts.rows.filter(row =>
53
+ row[0] !== range.start || row[1] !== range.end || row[2] !== line);
54
+ rows.push([range.start, range.end, line, column]);
55
+ facts.rows = rows;
56
+ facts.positions.set(key, column);
57
+ cache.storeUsageResult(index, file, name, state.mode, rows);
58
+ }
59
+ return column;
60
+ }
61
+
62
+ /** The selected declaration is a fresh input; only its physical name column is saved. */
63
+ function declarationTokenSyntax(index, file, entry, content, line, name, definition, allIdentifiers = true) {
64
+ if (index.isProvenanceFactIndex || !entry?.hash ||
65
+ !langTraits(entry.language)?.physicalCallTokenSyntax) return null;
66
+ const tree = index._peekParsedTree?.(file, entry?.language);
67
+ const state = sourceState(index, file, entry, content, tree);
68
+ if (!state?.mode) return null;
69
+ let identity;
70
+ try {
71
+ const description = JSON.stringify([allIdentifiers, definition || null], (_key, value) =>
72
+ value && typeof value === 'object' && !Array.isArray(value)
73
+ ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value);
74
+ identity = crypto.createHash('sha256').update(description).digest('hex');
75
+ }
76
+ catch { return null; }
77
+ const mode = state.mode.replace(`${SYNTAX_FAMILIES.callToken.prefix}:`, `${SYNTAX_FAMILIES.declarationToken.prefix}:`) +
78
+ `:${line}:${identity}`;
79
+ const saved = cache.getUsageResult(index, file, name, mode);
80
+ state.lines ||= content.split('\n');
81
+ const text = state.lines[line - 1] || '';
82
+ const valid = value => Array.isArray(value) && value.length === 1 &&
83
+ Number.isInteger(value[0]) && value[0] >= 0 &&
84
+ text.slice(value[0], value[0] + name.length) === name;
85
+ return {
86
+ columns: !tree && valid(saved) ? saved.slice() : null,
87
+ save(columns) {
88
+ if (valid(columns) && (!valid(saved) || saved[0] !== columns[0])) {
89
+ cache.storeUsageResult(index, file, name, mode, columns.slice());
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ function sourceState(index, file, entry, content, tree) {
96
+ if (index.isProvenanceFactIndex || !entry?.hash || !index._usageResultCache ||
97
+ !langTraits(entry.language)?.physicalCallTokenSyntax) return null;
98
+ const memo = index._opMemo?.('callTokenSyntax', () => new Map());
99
+ let state = memo?.get(file);
100
+ // Preserve the existing query-tree LRU and avoid rehashing a hot tree.
101
+ if (!state && tree) return null;
102
+ if (!state || state.content !== content || state.hash !== entry.hash) {
103
+ const eligible = indexedContent(index, file, entry, content);
104
+ const mode = eligible ? syntaxMode('callToken', parseViewParts(entry)) : null;
105
+ state = { content, hash: entry.hash, mode, names: new Map(), lines: null };
106
+ memo?.set(file, state);
107
+ }
108
+ return state;
109
+ }
110
+
111
+ module.exports = { callTokenSyntax, declarationTokenSyntax };
package/core/reporting.js CHANGED
@@ -15,6 +15,11 @@ const { summarizeCommandTrust } = require('./trust-matrix');
15
15
  const { projectComputedDispatch } = require('./ast-analysis');
16
16
  const { langTraits } = require('../languages');
17
17
 
18
+ // Modules read lazily (load cycles), each resolved once: a require() per
19
+ // call in hot paths was measurable (fix #436).
20
+ let lazySharedModuleCache = null;
21
+ const lazySharedModule = () => lazySharedModuleCache || (lazySharedModuleCache = require('./shared'));
22
+
18
23
  function matchesReportingScope(index, relativePath, options = {}) {
19
24
  if (!relativePath) return false;
20
25
  if (options.file && !relativePath.includes(options.file)) return false;
@@ -124,6 +129,7 @@ function getStats(index, options = {}) {
124
129
  // Ensure the calls cache is fully populated before counting.
125
130
  // First-time stats --hot may need to parse files to extract calls;
126
131
  // subsequent runs use the persisted calls cache.
132
+ require('./cache').ensureCallsCacheLoaded(index);
127
133
  if (typeof index.buildCalleeIndex === 'function' && !index.calleeIndex) {
128
134
  index.buildCalleeIndex();
129
135
  }
@@ -209,7 +215,7 @@ function getStats(index, options = {}) {
209
215
  // candidate set, the exact count, AND the upper bound (a bound that
210
216
  // still counted them stopped the early exit from firing, so the
211
217
  // orientation fell back to its approximate refinement budget).
212
- const { inlineTestRanges, lineInRanges } = require('./shared');
218
+ const { inlineTestRanges, lineInRanges } = lazySharedModule();
213
219
  const inlineTestRangesByFile = new Map();
214
220
  const inInlineTest = (file, line) => {
215
221
  if (!file || !line) return false;
@@ -227,7 +233,7 @@ function getStats(index, options = {}) {
227
233
  // the exact passes never scan them (every per-site verdict is
228
234
  // file-local). Orientation on django spent most of its time
229
235
  // resolving the test suite's calls, which it then discarded.
230
- const { isTestPath } = require('./shared');
236
+ const { isTestPath } = lazySharedModule();
231
237
  const countableFile = (filePath) => {
232
238
  if (!scopedPaths.has(filePath)) return false;
233
239
  const fileEntry = index.files.get(filePath);
@@ -307,7 +313,7 @@ function getStats(index, options = {}) {
307
313
  if (upper === 0) continue;
308
314
  if (index.files.get(symbol.file)?.isBundled) continue;
309
315
  if (options.productionCallsOnly &&
310
- (require('./shared').isTestPath(symbol.relativePath) ||
316
+ (lazySharedModule().isTestPath(symbol.relativePath) ||
311
317
  inInlineTest(symbol.file, symbol.startLine))) continue;
312
318
  const identity = `${symbol.file}:${symbol.startLine}:${name}:` +
313
319
  `${symbol.className || symbol.receiver || ''}:${symbol.params || ''}`;
@@ -418,7 +424,7 @@ function getStats(index, options = {}) {
418
424
  const testPath = (file) => {
419
425
  let verdict = testPathMemo.get(file);
420
426
  if (verdict === undefined) {
421
- verdict = require('./shared').isTestPath(file);
427
+ verdict = lazySharedModule().isTestPath(file);
422
428
  testPathMemo.set(file, verdict);
423
429
  }
424
430
  return verdict;
@@ -744,7 +750,7 @@ function doctor(index, options = {}) {
744
750
  // Reflection/eval signals come from the shared text-blind-spot counter
745
751
  // (core/shared.js) — the SAME routine detectCompleteness uses for the about
746
752
  // footer, so the two never drift (field-report #2). Occurrence counts.
747
- const { hasTextBlindspots, countTextBlindspots } = require('./shared');
753
+ const { hasTextBlindspots, countTextBlindspots } = lazySharedModule();
748
754
 
749
755
  const computedDispatch = projectComputedDispatch(index);
750
756
  for (const [filePath, fe] of index.files) {
@@ -1130,7 +1136,7 @@ function orient(index, options = {}) {
1130
1136
  in: options.in,
1131
1137
  exclude: options.exclude,
1132
1138
  };
1133
- const { isTestPath } = require('./shared');
1139
+ const { isTestPath } = lazySharedModule();
1134
1140
  const hasProductionFiles = [...index.files.values()].some(fileEntry => {
1135
1141
  const relativePath = fileEntry.relativePath;
1136
1142
  return relativePath &&
@@ -1172,7 +1178,7 @@ function orient(index, options = {}) {
1172
1178
  let entrypoints = null;
1173
1179
  try {
1174
1180
  const { detectEntrypoints } = require('./entrypoints');
1175
- const { addTestExclusions } = require('./shared');
1181
+ const { addTestExclusions } = lazySharedModule();
1176
1182
  const orientExclude = Array.isArray(options.exclude)
1177
1183
  ? options.exclude
1178
1184
  : (options.exclude