ucn 5.4.2 → 5.5.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.
Files changed (85) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +40 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +457 -60
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +2113 -0
  62. package/core/rust-modules.js +185 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1161 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2237 -115
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
package/core/cache.js CHANGED
@@ -10,7 +10,7 @@ const path = require('path');
10
10
  const os = require('os');
11
11
  const crypto = require('crypto');
12
12
  const {
13
- expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, DEFAULT_IGNORES,
13
+ expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, gitListing, DEFAULT_IGNORES,
14
14
  classifyUnsupportedSourceFile,
15
15
  } = require('./discovery');
16
16
  const { codeUnitCompare } = require('./shared');
@@ -26,9 +26,21 @@ const CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
26
26
  const CACHE_MAX_PROJECTS = 128;
27
27
  const CACHE_MAX_BYTES = 1024 * 1024 * 1024;
28
28
 
29
- function discoveryRulesHash(root, patterns = null) {
30
- const rules = patterns || parseGitignore(root);
31
- return crypto.createHash('md5').update(rules.join('\0')).digest('hex');
29
+ /**
30
+ * Fingerprint of what decides the discovered file set besides the tree
31
+ * itself: the compiled .gitignore rules and whether git could list the
32
+ * tracked paths (a failed listing changes which files rules may exclude).
33
+ */
34
+ function discoveryFingerprint(rules, listing) {
35
+ return crypto.createHash('md5')
36
+ .update(rules.fingerprint())
37
+ .update(listing?.failure ? '\0git-listing-failed' : '')
38
+ .digest('hex');
39
+ }
40
+
41
+ function discoveryRulesHash(root) {
42
+ const listing = gitListing(root);
43
+ return discoveryFingerprint(parseGitignore(root, listing), listing);
32
44
  }
33
45
 
34
46
  /**
@@ -714,7 +726,199 @@ function clearAllCaches() {
714
726
  // v226: bundled/minified filename exclusions are disclosed in discoveryIssues.
715
727
  // v227: signature parameter/return text excludes AST comments in every language.
716
728
  // v228: Python keyword arguments retain the same callable-reference facts as positional arguments.
717
- const CACHE_FORMAT_VERSION = 228;
729
+ // v229 (fix #359): subscripts of declared containers type their element
730
+ // receivers (Python dict/list/Sequence, TS Array/Record/index signatures and
731
+ // `const c = xs[i]`, Java/C/C++ arrays, C# BCL indexers, C++ std containers,
732
+ // Rust Vec/HashMap/arrays).
733
+ // v230 (fix #360): Go embedded-field symbols carry `embedded`; Go files carry
734
+ // typeConversions (concrete values flowing into differently-typed slots);
735
+ // Rust `fn name(...)` inside macro token trees is no longer a call record.
736
+ // v231 (fix #361): C/C++ recovery keeps include-guarded bodies and
737
+ // access-specifier/statement macros inside class bodies; C++ macros carry
738
+ // namespaceScope, C++ files carry macroScopeMarkers and cppUsings, C++ path
739
+ // calls carry qualifierTemplateParam; angle-bracket project includes resolve
740
+ // through include directories and basename-resolved quoted includes are
741
+ // recorded in fileEntry.includeFallback.
742
+ // v232 (fix #362): C/C++ function-like macros carry their replacement-list
743
+ // template (ppParams/ppVariadic/ppBody); `#define`/`#include` directives the
744
+ // grammar places inside enumerator lists or initializers are indexed; the
745
+ // index cache carries per-file macro-expansion records (`macroExpansion`).
746
+ // v233 (fix #363): file entries carry `reflectionSites` (reflective member
747
+ // accesses: literal names, name patterns built from literal fragments,
748
+ // receiver facts), extracted from each file's own parse at index time.
749
+ // v234 (fix #364): Python functions whose own body yields and C# iterator
750
+ // methods (`yield return`) carry `isGenerator`, so audit-async classifies
751
+ // async generators as async iterators rather than coroutines.
752
+ // v235 (fix #366): JS/TS and Python call records carry `requestConfig`
753
+ // (object-literal / keyword `url`/`path` request configuration), C#
754
+ // attributes keep non-literal `args`, and Python absolute imports resolve
755
+ // against nested source roots (moduleResolved/importGraph content changed).
756
+ // v236 (fix #367): C/C++ definitions and calls inside `__cplusplus` blocks
757
+ // the file's own language skips are indexed with `languageBranch`.
758
+ // v238 (fix #369): Rust path calls carry `returnPosition` (the value the
759
+ // enclosing fn returns) and aliased `extern crate x as y;` declarations
760
+ // become import bindings.
761
+ // v239 (fix #370): Rust functions, methods and type aliases carry
762
+ // `futureReturn`; Rust qualifiers (async/unsafe/const/extern) come from the
763
+ // AST, and trait `async fn` members carry isAsync.
764
+ // v242 (fix #372): the index carries `cargoManifests` (the workspace crate
765
+ // registry's manifest snapshot, stat-validated for staleness).
766
+ // v243 (fix #372): Rust call records mark every value that flows on or is
767
+ // read from its local (`valueConsumed` 'flow'/'stored'), and a method taken
768
+ // on a stored value as `consumingMethod`; file entries carry `openCalls`
769
+ // (call shapes, replacing openCallNames); large directories' calls shards
770
+ // are split into path buckets.
771
+ // v244 (fix #374): Rust files invoking project macro_rules! carry
772
+ // `rustMacroExpansion` (expansion record, revalidated per build); their
773
+ // symbols and call records are those of the expanded file, and facts
774
+ // produced inside an invocation carry `macroExpansion`.
775
+ // v245 (fix #375): calls shards store each file's enclosing-function,
776
+ // macro-expansion and receiver-evidence objects once (`fx` / `mx` / `rx`
777
+ // tables, referenced by position).
778
+ // v246 (fix #377): Rust `mod` declarations resolve by the module layout
779
+ // rules (foo.rs + foo/ children, #[path], inline-module nesting); persisted
780
+ // moduleResolved/importGraph content changed.
781
+ // v247 (fix #378): Go generic instantiation calls (`f[T](x)`, `pkg.F[T](x)`)
782
+ // are call records, generic receivers/parameters are typed by their base
783
+ // type, Go aliases record `aliasOf` for generic bases and `aliasQualifier`
784
+ // for package-qualified ones, package-qualified function values are function
785
+ // references, and function-local Go types and JS classes carry their lexical
786
+ // scope.
787
+ // v248 (fix #379): C/C++ recovery blanks a decoration macro before a second
788
+ // type specifier and in `class EXPORT Name : bases` heads, member-list macro
789
+ // invocations carry their access effect (`accessAfterMacro`), and functions
790
+ // under a preprocessor conditional record `ppBranch`.
791
+ // v249 (fix #380): C# type definitions carry `typeArity` and their members
792
+ // `ownerTypeArity`; call records carry the written type-argument count of
793
+ // constructors (`typeArgs`), type-qualified and typed receivers
794
+ // (`receiverTypeArgs`, `receiverRootTypeArgs`); generic type receivers
795
+ // (`Outcome<int>.Create()`) are type-qualified; Java `implements` keeps
796
+ // qualified supertypes.
797
+ // v250 (fix #381): Python function-local classes carry their lexical scope,
798
+ // and Python/JS/Java/C#/Go/Rust call records on a one-hop local alias of a
799
+ // field path or typed local receive like the aliased expression.
800
+ // v251 (fix #383): route registrations carry `handlerArgs` (JS/TS, Go),
801
+ // Go method-first registrations (`Handle(method, path, h)`)
802
+ // `methodFirstRoute`, Fastify route-config objects their `handler`, Go request constructors
803
+ // `requestTarget` { method, url }, and axum `.route()` calls `methodRouter`;
804
+ // Go file entries carry `packageName` (the package clause).
805
+ // v252 (fix #384): JS/TS member-assigned functions carry `assignedObject`
806
+ // ('global' | 'object') and `selfNamed` (bare-name reachability).
807
+ // v253 (fix #385): C/C++ members inside preprocessor conditionals of a class
808
+ // body are indexed (with `ppBranch`), invocations of a file's own
809
+ // function-like macros read as declarations are recovered (members they
810
+ // declare carry `generatedByMacro`), and macro invocations read as functions
811
+ // are not indexed.
812
+ // v254 (fix #386): specs of a grouped Go `type ( ... )` declaration carry
813
+ // `nameLine` (the line naming the type).
814
+ // v255 (fix #387): C/C++ recovery blanks declaration-specifier macros, never
815
+ // the type, scope or keyword next to them (return types, members and line
816
+ // numbers of recovered declarations changed); C/C++ file entries carry
817
+ // `recoveryBlanks` (the byte ranges the recovery blanked).
818
+ // v256 (fix #389): Java annotation types (`@interface`, `annotationType`) with
819
+ // their elements and interface constants, Java type `nameLine`, Rust unions,
820
+ // struct/variant `valueShape` ('unit' | 'tuple'), C/C++ function-local class
821
+ // scopes, file-level `moduleValueAliases` (Python/JS/TS), Rust call records'
822
+ // `receiverValuePath` for unbound identifiers / struct expressions / one-hop
823
+ // locals (`receiverValueKind`), Java annotation element call records
824
+ // (`annotationElement`, `implicitName`), C# typeof reflection `arity`.
825
+ // v257 (fix #390): C# declarations carry `nameLine` when attributes start
826
+ // them and `generics` (type parameter names) on generic types, methods and
827
+ // local functions; C++ class templates carry `generics`; Java fields and
828
+ // TS/JS decorated fields carry `nameLine`; Java/C# call records read a `+`
829
+ // with a string operand as a `string` argument.
830
+ // v259 (fix #391): C++ friend functions are namespace-scope functions
831
+ // (`friendOf` names the befriending class) instead of members, and member
832
+ // templates carry their return type and `templateDependent`; namespace-scope
833
+ // macro invocations followed by a body are generated callables
834
+ // (`generatedByMacro`); C# files whose `#if` splits a declaration are read
835
+ // through configuration views, recorded per file as `conditionalViews`;
836
+ // C# explicit interface properties, indexers and events carry
837
+ // `explicitInterface`; large C/C++ files resolve the conditionals the
838
+ // grammar could not place; C#/Java call records carry explicit method type
839
+ // arguments (`methodTypeArgs`) and lambda parameter counts (`lambda:N`), and
840
+ // C# argument kinds type `params` parameters, fields of the enclosing type
841
+ // and array creations by rank.
842
+ // v260 (fix #392): Rust top-level const/static items carry their declared
843
+ // type (`valueType`); Python module aliases bound on several module-level
844
+ // paths carry `alternatives`, JS/TS `let`/`var` aliases never reassigned are
845
+ // aliases; Java enums record `implements` and their methods' `nameLine` /
846
+ // annotations; Rust call records on locals destructured from `self` carry
847
+ // the field-hop receiver; Python usage records classify default values as
848
+ // references and bare/lambda parameters as definitions (plan's usage mode
849
+ // adds each bare reference's lexical scope verdict).
850
+ // v261 (fix #393): C/C++ unnamed parameters carry `unnamed` with their type
851
+ // as the name; C++ `v->m()` call records carry `receiverArrow` (and the
852
+ // declared object type `receiverArrowObject` when `v` is an object); C++
853
+ // function templates carry `templateParams`, member templates with a body
854
+ // are definitions, members in skipped conditional branches are indexed,
855
+ // namespaces left as ERROR tokens or closed early by the parse scope their
856
+ // declarations, large files resolve brace-splitting conditionals, pointer
857
+ // arguments keep their pointer level in `type:` argument kinds; C#
858
+ // delegates carry `delegateParams` and target-typed `new(..)` records a
859
+ // constructor call (`targetTyped`).
860
+ // v262 (fix #394): Java argument kinds keep array dimensions
861
+ // (`type:java.lang.String[]`, `new:..[]`, `cast:..[]`) and receivers
862
+ // declared with a package-qualified type carry the package as
863
+ // `receiverTypeQualifier`.
864
+ // v263 (fix #395): C# declarations carry their full namespace (every
865
+ // enclosing `namespace` block), C# using directives record the namespace
866
+ // they sit in, `using static` and `global using`.
867
+ // v264 (fix #396): C/C++ anonymous typedef'd specifiers record
868
+ // `typedefName`, nested classes (in-class and `struct Outer::Inner`)
869
+ // `enclosingType`, generated callables their identifier arguments
870
+ // (`generatedByMacro.args`) and pasted member-list names `unspelled`; calls
871
+ // on `#define` continuation lines are replacement-list calls; recovery blanks
872
+ // decoration macros before parenthesized names and return-type macro
873
+ // invocations; files record the project macro names their recovery used
874
+ // (`externalMacroNames`, `externalMacroKey`, `recoveryCandidates`).
875
+ // v265 (fix #396): a replacement-list receiver declared with a macro
876
+ // parameter as its type carries no receiver type.
877
+ // v266 (fix #396): a class body the grammar closed early is never read as
878
+ // the return type of the next member (no decoration blank there).
879
+ // v267 (fix #397): JS/TS bare records of names an object pattern binds carry
880
+ // `destructured` (property key, position, source receiver facts); local
881
+ // shadows carry the shadowing declaration's line as `localShadow` (-1 for a
882
+ // parameter) and plain calls carry them too; stored methods record `memberValue` references;
883
+ // `require('m').y` binds y under the local name; object-literal members
884
+ // carry `objectLiteralLine` (accessors their get/set kind); usages mark
885
+ // shorthand properties and import aliases; the index persists the
886
+ // in-repository JS packages (`jsWorkspacePackages`); JS call records carry
887
+ // `uncertain` only when true.
888
+ // v268 (fix #398): repeated import source names retain every local binding;
889
+ // an alias record applies to one specifier, not all same-name specifiers.
890
+ // v269 (fix #398F): Python files record `configurationItems` (names bound in
891
+ // one scope only in exclusive `if`/`try`/`match` branches).
892
+ // v270 (fix #399): Go files record `packageVars` (package-level variables
893
+ // with their declared static types) and their `buildConstraint`; Go method
894
+ // calls on a receiver no scope of the file binds carry `receiverPackageScope`.
895
+ // v271 (fix #400): Go imports that write no local name are marked
896
+ // `implicitName` (a project import's local name is its package clause) and
897
+ // carry the name the Go tools suggest (`go-json` is `json`, `core/v1` `v1`);
898
+ // `packageVars` also record constants and `valueFrom` initializers, and
899
+ // untyped package-level variables of the file carry `receiverPackageScope`.
900
+ // C# target-typed `new(..)` arguments and member assignments are `new`
901
+ // records with a `targetSlot` (files list their `targetTypedKeys`), C# types
902
+ // record `primaryParams`; Python bound-method reads assigned to a name
903
+ // (`loads = registry.loads`) are method-value records.
904
+ // v272 (fix #401): Rust receivers typed through std deref wrappers carry the
905
+ // target type with the wrapper chain on their evidence (`derefVia`), statics
906
+ // record `valueDerefVia`, array literal receivers are 'array'; C++ subscript
907
+ // receivers of class objects record `receiverSubscriptObject`, macro-body
908
+ // receivers declared with a macro parameter `receiverMacroParam`, and
909
+ // `T v(X());` initializer calls are `declarationReading` records; C#
910
+ // target-typed `new(..)` collection elements are typed, argument slots of
911
+ // generic receivers carry `receiverTypeText` (a `~` targetTypedKeys entry);
912
+ // Java `super` calls in enum-constant bodies carry `receiverSuperType`.
913
+ // v273 (fix #402): Python defs under compound statements of a class body
914
+ // are methods of the class and `Union[X, None]` annotations name X; JS/TS
915
+ // anonymous default exports and member-assigned functions record `isAsync`,
916
+ // a default import beside named or namespace imports is its own `default`
917
+ // import record; JS/TS and Python member-call records mark `awaited` and
918
+ // `passedAsArgument`.
919
+ // v274 (fix #403): a Python reference inside a lambda whose enclosing def
920
+ // binds the name records that def as its `localShadowScope`.
921
+ const CACHE_FORMAT_VERSION = 274;
718
922
  const USAGE_CACHE_FILE = 'usage-results.json';
719
923
 
720
924
  /**
@@ -815,21 +1019,25 @@ function saveCache(index, cachePath) {
815
1019
 
816
1020
  const cacheFile = cachePath || path.join(cacheDir, 'index.json');
817
1021
 
1022
+ // A partially loaded (per-file) calls cache must be complete before it
1023
+ // is rewritten: the shard directory is replaced wholesale below.
1024
+ if (index._callsCachePrepared && !index._callsCacheLoaded) {
1025
+ ensureCallsCacheLoaded(index);
1026
+ }
1027
+
818
1028
  // Prepare callsCache for serialization (exclude content, use relative paths)
819
1029
  const callsCacheData = [];
820
1030
  for (const [filePath, entry] of index.callsCache) {
821
- callsCacheData.push([path.relative(index.root, filePath), {
822
- mtime: entry.mtime,
823
- hash: entry.hash,
824
- calls: entry.calls
825
- // content is not persisted - will be read on demand
826
- }]);
1031
+ callsCacheData.push([path.relative(index.root, filePath), entry]);
827
1032
  }
828
1033
 
829
1034
  // Hash config to detect when graph rebuild is needed on load
830
1035
  const configHash = crypto.createHash('md5')
831
1036
  .update(JSON.stringify(index.config || {})).digest('hex');
832
- const discoveryHash = discoveryRulesHash(index.root);
1037
+ // The rules the index was discovered with (fix #382: re-asking git at
1038
+ // save time cost a second listing and could describe newer rules).
1039
+ const discoveryHash = index._discoveryHash || index._loadedDiscoveryHash ||
1040
+ discoveryRulesHash(index.root);
833
1041
 
834
1042
  // Strip redundant fields from symbols and file entries to reduce cache size.
835
1043
  // v6: All paths stored as relative paths (saves ~60% on large codebases).
@@ -938,6 +1146,20 @@ function saveCache(index, cachePath) {
938
1146
  unsupportedFiles: Array.isArray(index.unsupportedFiles)
939
1147
  ? index.unsupportedFiles
940
1148
  : [],
1149
+ ...(Array.isArray(index.cargoManifests) && {
1150
+ cargoManifests: index.cargoManifests,
1151
+ cargoManifestsSeen: index.cargoManifestsSeen || null,
1152
+ }),
1153
+ // In-repository JS packages (fix #397), so a cache-loaded query
1154
+ // never re-reads the manifests.
1155
+ // Names and directories only; a manifest is read again when an
1156
+ // import resolves through it.
1157
+ ...(index._jsWorkspacePackages !== undefined && {
1158
+ jsWorkspacePackages: index._jsWorkspacePackages
1159
+ ? [...index._jsWorkspacePackages].map(([name, pkg]) =>
1160
+ [name, pkg.ambiguous ? null : path.relative(root, pkg.dir)])
1161
+ : null,
1162
+ }),
941
1163
  includeBundled: index.includeBundled === true,
942
1164
  discoveryIssues: Array.isArray(index.discoveryIssues)
943
1165
  ? index.discoveryIssues
@@ -947,6 +1169,12 @@ function saveCache(index, cachePath) {
947
1169
  reachableSymbols: reachableSymbolsRel,
948
1170
  reachableFingerprint,
949
1171
  }),
1172
+ // C/C++ macro-expansion records (fix #362), revalidated per file on use.
1173
+ ...(() => {
1174
+ const { persistableMacroExpansion } = require('./macro-expansion');
1175
+ const macroExpansion = persistableMacroExpansion(index);
1176
+ return macroExpansion ? { macroExpansion } : {};
1177
+ })(),
950
1178
  ...(index._computedDispatchBlindspots instanceof Map && {
951
1179
  computedDispatchBlindspots: [...index._computedDispatchBlindspots]
952
1180
  .map(([filePath, sites]) => [path.relative(root, filePath), sites]),
@@ -966,6 +1194,7 @@ function saveCache(index, cachePath) {
966
1194
  index.reachabilityDirty = false;
967
1195
  }
968
1196
  index.computedDispatchDirty = false;
1197
+ index.macroExpansionDirty = false;
969
1198
 
970
1199
  // Save callsCache sharded by directory for lazy loading.
971
1200
  // Write to a temp directory first, then atomic swap to avoid data loss on crash.
@@ -988,13 +1217,26 @@ function saveCache(index, cachePath) {
988
1217
  shards.get(dir).push([relPath, entry]);
989
1218
  }
990
1219
 
991
- // Write all shards to temp directory
1220
+ // Write all shards to temp directory. A large directory is split
1221
+ // into buckets by file path (fix #372), so a per-file consumer loads
1222
+ // roughly that file's records instead of its whole directory.
992
1223
  const shardManifest = [];
993
1224
  for (const [dir, entries] of shards) {
994
1225
  const hash = crypto.createHash('md5').update(dir).digest('hex').slice(0, 10);
995
- const shardFile = path.join(callsTmpDir, `${hash}.json`);
996
- fs.writeFileSync(shardFile, JSON.stringify(entries));
997
- shardManifest.push([dir, hash, entries.length]);
1226
+ const serialized = entries.map(([relPath, entry]) => serializeCallsEntry(relPath, entry));
1227
+ const bytes = serialized.reduce((total, text) => total + text.length + 1, 0);
1228
+ const buckets = Math.max(1, Math.min(entries.length, Math.ceil(bytes / CALLS_SHARD_TARGET_BYTES)));
1229
+ if (buckets === 1) {
1230
+ fs.writeFileSync(path.join(callsTmpDir, `${hash}.json`), `[${serialized.join(',')}]`);
1231
+ shardManifest.push([dir, hash, entries.length]);
1232
+ continue;
1233
+ }
1234
+ const parts = Array.from({ length: buckets }, () => []);
1235
+ entries.forEach(([relPath], i) => parts[callsShardBucket(relPath, buckets)].push(serialized[i]));
1236
+ parts.forEach((part, k) => {
1237
+ fs.writeFileSync(path.join(callsTmpDir, `${hash}.${k}.json`), `[${part.join(',')}]`);
1238
+ });
1239
+ shardManifest.push([dir, hash, entries.length, buckets]);
998
1240
  }
999
1241
 
1000
1242
  // Write manifest to temp directory
@@ -1084,6 +1326,14 @@ function loadCache(index, cachePath) {
1084
1326
  index._groundSetCacheLines = 0;
1085
1327
  index._nameBindingReachCache = new Map();
1086
1328
  index._returnTypeFlowCache = new Map();
1329
+ index._classIdentity = null;
1330
+ index._macroExpansion = null;
1331
+ index._macroSpellable = null; // fix #385: paste-spellability memo over the same symbols
1332
+ index._rustMacroState = null;
1333
+ index._cppScope = null;
1334
+ index._contractIndex = null;
1335
+ index._typeDenotation = null;
1336
+ index._languageFeatureMemo = null; // fix #380 protocol-type feature memo
1087
1337
 
1088
1338
  // Reconstruct files Map: relative key → absolute key, restore path and relativePath
1089
1339
  // Initialize symbols/bindings arrays (will be populated from top-level symbols)
@@ -1168,6 +1418,22 @@ function loadCache(index, cachePath) {
1168
1418
  index.unsupportedFiles = Array.isArray(cacheData.unsupportedFiles)
1169
1419
  ? cacheData.unsupportedFiles
1170
1420
  : [];
1421
+ // Cargo manifest snapshot (fix #372): seeds the workspace crate
1422
+ // registry so queries never walk the tree for it.
1423
+ index.cargoManifests = Array.isArray(cacheData.cargoManifests)
1424
+ ? cacheData.cargoManifests
1425
+ : null;
1426
+ index.cargoManifestsSeen = cacheData.cargoManifestsSeen || null;
1427
+ if (index.cargoManifests) {
1428
+ require('./imports').seedWorkspaceManifests(index.root, index.cargoManifests);
1429
+ }
1430
+ if (cacheData.jsWorkspacePackages !== undefined) {
1431
+ index._jsWorkspacePackages = Array.isArray(cacheData.jsWorkspacePackages)
1432
+ ? new Map(cacheData.jsWorkspacePackages.map(([name, dir]) => [name, typeof dir === 'string'
1433
+ ? { name, dir: path.join(index.root, dir), workspace: true, manifestPending: true }
1434
+ : { ambiguous: true }]))
1435
+ : null;
1436
+ }
1171
1437
  index.includeBundled = cacheData.includeBundled === true;
1172
1438
  index.discoveryIssues = Array.isArray(cacheData.discoveryIssues)
1173
1439
  ? cacheData.discoveryIssues
@@ -1175,6 +1441,11 @@ function loadCache(index, cachePath) {
1175
1441
  index.truncated = cacheData.truncated || null;
1176
1442
  index._loadedConfigHash = cacheData.configHash || null;
1177
1443
  index._loadedDiscoveryHash = cacheData.discoveryHash || null;
1444
+ index._discoveryHash = null;
1445
+ index._macroExpansionPersisted = cacheData.macroExpansion &&
1446
+ typeof cacheData.macroExpansion.signature === 'string'
1447
+ ? cacheData.macroExpansion : null;
1448
+ index.macroExpansionDirty = false;
1178
1449
  if (Array.isArray(cacheData.computedDispatchBlindspots)) {
1179
1450
  index._computedDispatchBlindspots = new Map(
1180
1451
  cacheData.computedDispatchBlindspots.map(([relPath, sites]) => [
@@ -1184,6 +1455,7 @@ function loadCache(index, cachePath) {
1184
1455
  );
1185
1456
  index.computedDispatchDirty = false;
1186
1457
  }
1458
+ index._reflectionPatternIndex = null;
1187
1459
 
1188
1460
  // Restore calleeIndex if persisted (v7 caches only; v8+ rebuilds lazily)
1189
1461
  if (Array.isArray(cacheData.calleeIndex)) {
@@ -1253,11 +1525,13 @@ function isCacheStale(index) {
1253
1525
  // new-file walk below. On a fresh cache these used to run two identical
1254
1526
  // `git ls-files` subprocesses per one-shot CLI invocation, accounting for
1255
1527
  // roughly a third of warm-start staleness time on measured repositories.
1256
- let gitignorePatterns = null;
1528
+ let gitignoreRules = null;
1529
+ // One git subprocess answers both questions discovery asks (fix #375).
1530
+ let listing;
1531
+ const gitListingOnce = () => (listing === undefined ? (listing = gitListing(index.root)) : listing);
1257
1532
  if (index._loadedDiscoveryHash) {
1258
- gitignorePatterns = parseGitignore(index.root);
1259
- if (discoveryRulesHash(index.root, gitignorePatterns) !==
1260
- index._loadedDiscoveryHash) {
1533
+ gitignoreRules = parseGitignore(index.root, gitListingOnce());
1534
+ if (discoveryFingerprint(gitignoreRules, listing) !== index._loadedDiscoveryHash) {
1261
1535
  return true;
1262
1536
  }
1263
1537
  }
@@ -1296,6 +1570,12 @@ function isCacheStale(index) {
1296
1570
  }
1297
1571
  }
1298
1572
 
1573
+ // Cargo manifests steer Rust import resolution (fix #372).
1574
+ if (index.cargoManifests &&
1575
+ !require('./imports').workspaceManifestsCurrent(index.root, index.cargoManifests)) {
1576
+ return true;
1577
+ }
1578
+
1299
1579
  // Ultra-fast skip for the SLOW path only: last confirmed-fresh < 2s ago
1300
1580
  // (covers MCP burst calls). Uses _lastFreshAt (set at the end of a
1301
1581
  // successful full check), never the cache save timestamp.
@@ -1308,6 +1588,7 @@ function isCacheStale(index) {
1308
1588
  const pattern = detectProjectPattern(index.root);
1309
1589
  const currentUnsupported = [];
1310
1590
  const currentBundled = [];
1591
+ const currentManifests = [];
1311
1592
  const globOpts = {
1312
1593
  root: index.root,
1313
1594
  includeBundled: index.includeBundled === true,
@@ -1315,6 +1596,9 @@ function isCacheStale(index) {
1315
1596
  if (issue.reason === 'bundled') currentBundled.push(path.relative(index.root, issue.path));
1316
1597
  },
1317
1598
  onSkippedFile: (filePath) => {
1599
+ if (path.basename(filePath) === 'Cargo.toml') {
1600
+ currentManifests.push(path.relative(index.root, filePath));
1601
+ }
1318
1602
  const kind = classifyUnsupportedSourceFile(filePath);
1319
1603
  if (!kind) return;
1320
1604
  currentUnsupported.push({
@@ -1323,9 +1607,9 @@ function isCacheStale(index) {
1323
1607
  });
1324
1608
  },
1325
1609
  };
1326
- if (!gitignorePatterns) gitignorePatterns = parseGitignore(index.root);
1327
- globOpts.gitignorePatterns = gitignorePatterns;
1328
- globOpts.trackedPaths = gitTrackedPaths(index.root);
1610
+ if (!gitignoreRules) gitignoreRules = parseGitignore(index.root, gitListingOnce());
1611
+ globOpts.gitignoreRules = gitignoreRules;
1612
+ globOpts.trackedPaths = gitTrackedPaths(index.root, gitListingOnce());
1329
1613
  const configExclude = index.config.exclude || [];
1330
1614
  if (configExclude.length > 0) {
1331
1615
  globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
@@ -1363,6 +1647,12 @@ function isCacheStale(index) {
1363
1647
  return true;
1364
1648
  }
1365
1649
 
1650
+ if (index.cargoManifests &&
1651
+ !require('./imports').workspaceManifestsCurrent(index.root, index.cargoManifests,
1652
+ currentManifests, index.cargoManifestsSeen)) {
1653
+ return true;
1654
+ }
1655
+
1366
1656
  // Record when we last confirmed the cache is fresh (enables 2s skip on burst calls)
1367
1657
  index._lastFreshAt = Date.now();
1368
1658
  return false;
@@ -1389,8 +1679,11 @@ function _prepareCallsCache(index, cacheFile) {
1389
1679
  try {
1390
1680
  const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
1391
1681
  index._callsManifest = new Map();
1392
- for (const [dir, hash, count] of manifest) {
1393
- index._callsManifest.set(dir, { hash, count, loaded: false });
1682
+ for (const [dir, hash, count, buckets] of manifest) {
1683
+ index._callsManifest.set(dir, {
1684
+ hash, count, buckets: buckets || 1,
1685
+ loaded: false, loadedBuckets: buckets > 1 ? new Set() : null,
1686
+ });
1394
1687
  }
1395
1688
  index._callsCachePrepared = true;
1396
1689
  return;
@@ -1418,10 +1711,19 @@ function loadCallsCache(index) {
1418
1711
  if (index._callsCacheLoaded) return index.callsCache.size > 0;
1419
1712
  index._callsCacheLoaded = true;
1420
1713
 
1421
- // If manifest was prepared lazily, load all shards now
1714
+ // If manifest was prepared lazily, load all shards now (a shard already
1715
+ // loaded for a single file is not parsed twice).
1422
1716
  if (index._callsManifest) {
1423
- for (const [, { hash }] of index._callsManifest) {
1424
- _loadCallsShard(index, hash);
1717
+ for (const [, shard] of index._callsManifest) {
1718
+ if (shard.loaded) continue;
1719
+ shard.loaded = true;
1720
+ if (shard.buckets > 1) {
1721
+ for (let k = 0; k < shard.buckets; k++) {
1722
+ if (!shard.loadedBuckets.has(k)) _loadCallsShard(index, `${shard.hash}.${k}`);
1723
+ }
1724
+ } else {
1725
+ _loadCallsShard(index, shard.hash);
1726
+ }
1425
1727
  }
1426
1728
  return index.callsCache.size > 0;
1427
1729
  }
@@ -1438,7 +1740,7 @@ function loadCallsCache(index) {
1438
1740
  if (!relPath || !entry) continue;
1439
1741
  const absPath = path.isAbsolute(relPath) ? relPath : path.join(index.root, relPath);
1440
1742
  if (!index.callsCache.has(absPath)) {
1441
- index.callsCache.set(absPath, entry);
1743
+ index.callsCache.set(absPath, decodeCallsEntry(entry));
1442
1744
  }
1443
1745
  }
1444
1746
  return index.callsCache.size > 0;
@@ -1460,6 +1762,194 @@ function ensureCallsCacheLoaded(index) {
1460
1762
  }
1461
1763
  }
1462
1764
 
1765
+ /**
1766
+ * Make one file's persisted call records available without loading every
1767
+ * shard (fix #365): a per-file consumer (audit-async's prefilter, single-file
1768
+ * queries) used to pay for parsing the whole sharded calls cache. Shards are
1769
+ * per directory, as saveCache writes them; whole-cache consumers still call
1770
+ * ensureCallsCacheLoaded, which loads the remaining shards.
1771
+ * @param {object} index - ProjectIndex instance
1772
+ * @param {string} filePath - Absolute file path
1773
+ */
1774
+ function ensureCallsShardForFile(index, filePath) {
1775
+ if (!index._callsCachePrepared || index._callsCacheLoaded) return;
1776
+ if (!index._callsManifest) {
1777
+ loadCallsCache(index);
1778
+ return;
1779
+ }
1780
+ const relPath = path.relative(index.root, filePath);
1781
+ const shard = index._callsManifest.get(path.dirname(relPath) || '.');
1782
+ if (!shard || shard.loaded) return;
1783
+ if (shard.buckets > 1) {
1784
+ const bucket = callsShardBucket(relPath, shard.buckets);
1785
+ if (shard.loadedBuckets.has(bucket)) return;
1786
+ shard.loadedBuckets.add(bucket);
1787
+ if (shard.loadedBuckets.size === shard.buckets) shard.loaded = true;
1788
+ _loadCallsShard(index, `${shard.hash}.${bucket}`);
1789
+ return;
1790
+ }
1791
+ shard.loaded = true;
1792
+ _loadCallsShard(index, shard.hash);
1793
+ }
1794
+
1795
+ // Calls shards are split into buckets of about this many serialized bytes
1796
+ // (fix #372); small directories keep one shard.
1797
+ const CALLS_SHARD_TARGET_BYTES = 64 * 1024;
1798
+
1799
+ /** Stable bucket of a project-relative path among `buckets` (FNV-1a). */
1800
+ function callsShardBucket(relPath, buckets) {
1801
+ const key = String(relPath).split(path.sep).join('/');
1802
+ let h = 0x811c9dc5;
1803
+ for (let i = 0; i < key.length; i++) {
1804
+ h ^= key.charCodeAt(i);
1805
+ h = Math.imul(h, 0x01000193) >>> 0;
1806
+ }
1807
+ return h % buckets;
1808
+ }
1809
+
1810
+ /**
1811
+ * Record fields whose objects repeat within a file (the records of one
1812
+ * function share their `enclosingFunction`, those of one macro invocation
1813
+ * their `macroExpansion`, those of one typed local its receiver evidence),
1814
+ * persisted once per file in a table and referenced by position (fix #375:
1815
+ * they were a third of a large project's calls cache).
1816
+ */
1817
+ const CALL_OBJECT_TABLES = [
1818
+ ['enclosingFunction', 'fx'],
1819
+ ['macroExpansion', 'mx'],
1820
+ ['receiverTypeEvidence', 'rx'],
1821
+ ['destructured', 'dx'],
1822
+ ];
1823
+
1824
+ /**
1825
+ * JSON text of `[relPath, entry]` for a calls shard, the repeated record
1826
+ * objects tabled (CALL_OBJECT_TABLES). The records are swapped in place
1827
+ * while serializing and restored.
1828
+ */
1829
+ function serializeCallsEntry(relPath, entry) {
1830
+ const calls = entry.calls;
1831
+ const out = {
1832
+ mtime: entry.mtime,
1833
+ hash: entry.hash,
1834
+ calls,
1835
+ // content is not persisted - will be read on demand
1836
+ };
1837
+ if (!Array.isArray(calls)) return JSON.stringify([relPath, out]);
1838
+ const tables = CALL_OBJECT_TABLES.map(() => new SlotTable());
1839
+ const swapped = [];
1840
+ try {
1841
+ for (const call of calls) {
1842
+ for (let t = 0; t < CALL_OBJECT_TABLES.length; t++) {
1843
+ const field = CALL_OBJECT_TABLES[t][0];
1844
+ const value = call[field];
1845
+ if (value === null || typeof value !== 'object') continue;
1846
+ swapped.push(call, field, value);
1847
+ call[field] = tables[t].slot(value);
1848
+ }
1849
+ }
1850
+ CALL_OBJECT_TABLES.forEach(([, key], t) => {
1851
+ if (tables[t].values.length > 0) out[key] = tables[t].values;
1852
+ });
1853
+ return JSON.stringify([relPath, out]);
1854
+ } finally {
1855
+ for (let i = 0; i < swapped.length; i += 3) swapped[i][swapped[i + 1]] = swapped[i + 2];
1856
+ }
1857
+ }
1858
+
1859
+ /**
1860
+ * Distinct JSON values in first-seen order. Equal values (same keys in the
1861
+ * same order, same values) share a slot. Candidates are bucketed by name and
1862
+ * lines; a crowded bucket (minified code puts every function on one line)
1863
+ * switches to serialized keys. Consecutive records usually repeat the
1864
+ * previous value, which is checked first.
1865
+ */
1866
+ class SlotTable {
1867
+ constructor() {
1868
+ this.values = [];
1869
+ this.buckets = new Map();
1870
+ this.last = undefined;
1871
+ this.lastKey = -1;
1872
+ }
1873
+
1874
+ slot(value) {
1875
+ if (this.last !== undefined && (value === this.last || sameJsonValue(this.last, value))) {
1876
+ this.last = value;
1877
+ return this.lastKey;
1878
+ }
1879
+ const key = this._find(value);
1880
+ this.last = value;
1881
+ this.lastKey = key;
1882
+ return key;
1883
+ }
1884
+
1885
+ _find(value) {
1886
+ const values = this.values;
1887
+ const probe = `${value.name ?? value.macro}\0${value.startLine ?? value.line}\0${value.endLine}`;
1888
+ let bucket = this.buckets.get(probe);
1889
+ if (!bucket) this.buckets.set(probe, bucket = { keys: [], byText: null });
1890
+ if (bucket.byText) {
1891
+ const text = JSON.stringify(value);
1892
+ let key = bucket.byText.get(text);
1893
+ if (key === undefined) {
1894
+ key = values.length;
1895
+ values.push(value);
1896
+ bucket.byText.set(text, key);
1897
+ }
1898
+ return key;
1899
+ }
1900
+ for (const key of bucket.keys) {
1901
+ if (sameJsonValue(values[key], value)) return key;
1902
+ }
1903
+ const key = values.length;
1904
+ values.push(value);
1905
+ bucket.keys.push(key);
1906
+ if (bucket.keys.length >= 32) {
1907
+ bucket.byText = new Map(bucket.keys.map(k => [JSON.stringify(values[k]), k]));
1908
+ }
1909
+ return key;
1910
+ }
1911
+ }
1912
+
1913
+ /** Whether two JSON values serialize identically (key order included). */
1914
+ function sameJsonValue(a, b) {
1915
+ if (a === b) return true;
1916
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
1917
+ if (Array.isArray(a)) {
1918
+ if (!Array.isArray(b) || a.length !== b.length) return false;
1919
+ for (let i = 0; i < a.length; i++) {
1920
+ if (!sameJsonValue(a[i], b[i])) return false;
1921
+ }
1922
+ return true;
1923
+ }
1924
+ if (Array.isArray(b)) return false;
1925
+ const keysA = Object.keys(a);
1926
+ const keysB = Object.keys(b);
1927
+ if (keysA.length !== keysB.length) return false;
1928
+ for (let i = 0; i < keysA.length; i++) {
1929
+ const key = keysA[i];
1930
+ if (key !== keysB[i] || !sameJsonValue(a[key], b[key])) return false;
1931
+ }
1932
+ return true;
1933
+ }
1934
+
1935
+ /** In-memory form of a persisted calls-cache entry (see serializeCallsEntry). */
1936
+ function decodeCallsEntry(entry) {
1937
+ let tabled = null;
1938
+ for (const [field, key] of CALL_OBJECT_TABLES) {
1939
+ if (!entry[key]) continue;
1940
+ (tabled ||= []).push(field, entry[key]);
1941
+ delete entry[key];
1942
+ }
1943
+ if (!tabled) return entry;
1944
+ for (const call of entry.calls || []) {
1945
+ for (let i = 0; i < tabled.length; i += 2) {
1946
+ const at = call[tabled[i]];
1947
+ if (typeof at === 'number') call[tabled[i]] = tabled[i + 1][at];
1948
+ }
1949
+ }
1950
+ return entry;
1951
+ }
1952
+
1463
1953
  /**
1464
1954
  * Load a single calls shard by hash.
1465
1955
  * @param {object} index - ProjectIndex instance
@@ -1483,7 +1973,7 @@ function _loadCallsShard(index, hash) {
1483
1973
  // parse of current disk content (or an earlier load) — never
1484
1974
  // clobber it with persisted shard data (fix #227).
1485
1975
  if (!index.callsCache.has(absPath)) {
1486
- index.callsCache.set(absPath, entry);
1976
+ index.callsCache.set(absPath, decodeCallsEntry(entry));
1487
1977
  }
1488
1978
  }
1489
1979
  } catch (e) {
@@ -1617,8 +2107,9 @@ function buildWithLock(index, buildOpts = {}, options = {}) {
1617
2107
  module.exports = {
1618
2108
  buildWithLock,
1619
2109
  saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
2110
+ ensureCallsShardForFile,
1620
2111
  getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
1621
2112
  getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
1622
2113
  clearAllCaches, pruneUserCache,
1623
- _computeReachabilityFingerprint, CACHE_FORMAT_VERSION,
2114
+ _computeReachabilityFingerprint, CACHE_FORMAT_VERSION, discoveryFingerprint,
1624
2115
  };