@codeyam/codeyam-cli 0.1.0-staging.323686 → 0.1.0-staging.483fdc2

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 (171) hide show
  1. package/analyzer-template/.build-info.json +7 -7
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/package.json +2 -2
  4. package/analyzer-template/packages/ai/index.ts +6 -1
  5. package/analyzer-template/packages/ai/src/lib/analyzeScope.ts +39 -17
  6. package/analyzer-template/packages/ai/src/lib/astScopes/astScopeAnalyzer.ts +67 -9
  7. package/analyzer-template/packages/ai/src/lib/astScopes/processExpression.ts +308 -50
  8. package/analyzer-template/packages/ai/src/lib/astScopes/types.ts +15 -6
  9. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +664 -242
  10. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/BatchSchemaProcessor.ts +16 -3
  11. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/ScopeTreeManager.ts +6 -4
  12. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +20 -1
  13. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanNonObjectFunctions.ts +35 -13
  14. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/convertTypeAnnotationsToValues.ts +160 -0
  15. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/deduplicateFunctionSchemas.ts +40 -30
  16. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/fillInSchemaGapsAndUnknowns.ts +289 -83
  17. package/analyzer-template/packages/ai/src/lib/generateEntityScenarioData.ts +269 -1
  18. package/analyzer-template/packages/ai/src/lib/generateEntityScenarios.ts +9 -5
  19. package/analyzer-template/packages/ai/src/lib/generateExecutionFlows.ts +11 -3
  20. package/analyzer-template/packages/ai/src/lib/generateExecutionFlowsFromConditionalEffects.ts +1 -1
  21. package/analyzer-template/packages/ai/src/lib/generateExecutionFlowsFromConditionals.ts +297 -7
  22. package/analyzer-template/packages/ai/src/lib/generateExecutionFlowsFromJsxUsages.ts +1 -1
  23. package/analyzer-template/packages/ai/src/lib/mergeStatements.ts +90 -96
  24. package/analyzer-template/packages/ai/src/lib/promptGenerators/gatherAttributesMap.ts +10 -7
  25. package/analyzer-template/packages/ai/src/lib/resolvePathToControllable.ts +25 -13
  26. package/analyzer-template/packages/ai/src/lib/worker/SerializableDataStructure.ts +4 -3
  27. package/analyzer-template/packages/analyze/src/lib/FileAnalyzer.ts +65 -59
  28. package/analyzer-template/packages/analyze/src/lib/ProjectAnalyzer.ts +113 -26
  29. package/analyzer-template/packages/analyze/src/lib/asts/sourceFiles/getAllDeclaredEntityNodes.ts +19 -0
  30. package/analyzer-template/packages/analyze/src/lib/asts/sourceFiles/getAllEntityNodes.ts +19 -0
  31. package/analyzer-template/packages/analyze/src/lib/asts/sourceFiles/getAllExports.ts +11 -0
  32. package/analyzer-template/packages/analyze/src/lib/asts/sourceFiles/getImportsAnalysis.ts +8 -0
  33. package/analyzer-template/packages/analyze/src/lib/asts/sourceFiles/getResolvedModule.ts +49 -1
  34. package/analyzer-template/packages/analyze/src/lib/asts/sourceFiles/getSourceFilesForAllImports.ts +2 -1
  35. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +20 -6
  36. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +14 -4
  37. package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +4 -2
  38. package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +0 -3
  39. package/analyzer-template/packages/analyze/src/lib/files/analyzeRemixRoute.ts +4 -5
  40. package/analyzer-template/packages/analyze/src/lib/files/getImportedExports.ts +14 -12
  41. package/analyzer-template/packages/analyze/src/lib/files/scenarios/enrichArrayTypesFromChildSignatures.ts +57 -13
  42. package/analyzer-template/packages/analyze/src/lib/files/scenarios/gatherDataForMocks.ts +29 -0
  43. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +35 -4
  44. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.ts +117 -9
  45. package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +199 -17
  46. package/analyzer-template/packages/analyze/src/lib/files/scenarios/propagateArrayItemSchemas.ts +474 -0
  47. package/analyzer-template/packages/analyze/src/lib/files/setImportedExports.ts +2 -1
  48. package/analyzer-template/packages/analyze/src/lib/utils/getFileByPath.ts +19 -0
  49. package/analyzer-template/packages/aws/package.json +1 -1
  50. package/analyzer-template/packages/github/dist/types/src/types/ScenariosDataStructure.d.ts +5 -5
  51. package/analyzer-template/packages/github/dist/types/src/types/ScenariosDataStructure.d.ts.map +1 -1
  52. package/analyzer-template/packages/github/dist/types/src/types/ScopeAnalysis.d.ts +6 -1
  53. package/analyzer-template/packages/github/dist/types/src/types/ScopeAnalysis.d.ts.map +1 -1
  54. package/analyzer-template/packages/github/package.json +1 -1
  55. package/analyzer-template/packages/types/src/types/ScenariosDataStructure.ts +6 -5
  56. package/analyzer-template/packages/types/src/types/ScopeAnalysis.ts +6 -1
  57. package/analyzer-template/packages/utils/dist/types/src/types/ScenariosDataStructure.d.ts +5 -5
  58. package/analyzer-template/packages/utils/dist/types/src/types/ScenariosDataStructure.d.ts.map +1 -1
  59. package/analyzer-template/packages/utils/dist/types/src/types/ScopeAnalysis.d.ts +6 -1
  60. package/analyzer-template/packages/utils/dist/types/src/types/ScopeAnalysis.d.ts.map +1 -1
  61. package/analyzer-template/project/constructMockCode.ts +54 -9
  62. package/analyzer-template/project/writeMockDataTsx.ts +73 -2
  63. package/background/src/lib/virtualized/project/constructMockCode.js +45 -3
  64. package/background/src/lib/virtualized/project/constructMockCode.js.map +1 -1
  65. package/background/src/lib/virtualized/project/writeMockDataTsx.js +71 -2
  66. package/background/src/lib/virtualized/project/writeMockDataTsx.js.map +1 -1
  67. package/codeyam-cli/scripts/apply-setup.js +146 -0
  68. package/codeyam-cli/scripts/apply-setup.js.map +1 -1
  69. package/codeyam-cli/src/commands/debug.js +7 -5
  70. package/codeyam-cli/src/commands/debug.js.map +1 -1
  71. package/codeyam-cli/src/utils/install-skills.js +22 -0
  72. package/codeyam-cli/src/utils/install-skills.js.map +1 -1
  73. package/codeyam-cli/src/utils/reviewedRules.js +92 -0
  74. package/codeyam-cli/src/utils/reviewedRules.js.map +1 -0
  75. package/codeyam-cli/src/webserver/build/client/assets/globals-CX9f-5xM.css +1 -0
  76. package/codeyam-cli/src/webserver/build/client/assets/{manifest-7522edd4.js → manifest-bba56ec1.js} +1 -1
  77. package/codeyam-cli/src/webserver/build/client/assets/memory-DuTFSyJ2.js +92 -0
  78. package/codeyam-cli/src/webserver/build/client/assets/{root-eVAaavTS.js → root-DTfSQARG.js} +6 -6
  79. package/codeyam-cli/src/webserver/build/server/assets/{index-DVzYx8PN.js → index-TD1f-DHV.js} +1 -1
  80. package/codeyam-cli/src/webserver/build/server/assets/server-build-BQ-1XyEa.js +258 -0
  81. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  82. package/codeyam-cli/src/webserver/build-info.json +5 -5
  83. package/codeyam-cli/templates/codeyam:memory.md +174 -233
  84. package/codeyam-cli/templates/codeyam:new-rule.md +41 -2
  85. package/codeyam-cli/templates/rule-reflection-hook.py +161 -0
  86. package/codeyam-cli/templates/rules-instructions.md +126 -0
  87. package/package.json +1 -1
  88. package/packages/ai/index.js +2 -1
  89. package/packages/ai/index.js.map +1 -1
  90. package/packages/ai/src/lib/analyzeScope.js +29 -12
  91. package/packages/ai/src/lib/analyzeScope.js.map +1 -1
  92. package/packages/ai/src/lib/astScopes/astScopeAnalyzer.js +54 -8
  93. package/packages/ai/src/lib/astScopes/astScopeAnalyzer.js.map +1 -1
  94. package/packages/ai/src/lib/astScopes/processExpression.js +239 -43
  95. package/packages/ai/src/lib/astScopes/processExpression.js.map +1 -1
  96. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +503 -165
  97. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  98. package/packages/ai/src/lib/dataStructure/helpers/BatchSchemaProcessor.js +13 -3
  99. package/packages/ai/src/lib/dataStructure/helpers/BatchSchemaProcessor.js.map +1 -1
  100. package/packages/ai/src/lib/dataStructure/helpers/ScopeTreeManager.js +6 -4
  101. package/packages/ai/src/lib/dataStructure/helpers/ScopeTreeManager.js.map +1 -1
  102. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +22 -1
  103. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
  104. package/packages/ai/src/lib/dataStructure/helpers/cleanNonObjectFunctions.js +34 -9
  105. package/packages/ai/src/lib/dataStructure/helpers/cleanNonObjectFunctions.js.map +1 -1
  106. package/packages/ai/src/lib/dataStructure/helpers/convertTypeAnnotationsToValues.js +159 -0
  107. package/packages/ai/src/lib/dataStructure/helpers/convertTypeAnnotationsToValues.js.map +1 -0
  108. package/packages/ai/src/lib/dataStructure/helpers/deduplicateFunctionSchemas.js +37 -20
  109. package/packages/ai/src/lib/dataStructure/helpers/deduplicateFunctionSchemas.js.map +1 -1
  110. package/packages/ai/src/lib/dataStructure/helpers/fillInSchemaGapsAndUnknowns.js +237 -73
  111. package/packages/ai/src/lib/dataStructure/helpers/fillInSchemaGapsAndUnknowns.js.map +1 -1
  112. package/packages/ai/src/lib/generateEntityScenarioData.js +195 -1
  113. package/packages/ai/src/lib/generateEntityScenarioData.js.map +1 -1
  114. package/packages/ai/src/lib/generateEntityScenarios.js +7 -1
  115. package/packages/ai/src/lib/generateEntityScenarios.js.map +1 -1
  116. package/packages/ai/src/lib/generateExecutionFlows.js +10 -2
  117. package/packages/ai/src/lib/generateExecutionFlows.js.map +1 -1
  118. package/packages/ai/src/lib/generateExecutionFlowsFromConditionals.js +209 -3
  119. package/packages/ai/src/lib/generateExecutionFlowsFromConditionals.js.map +1 -1
  120. package/packages/ai/src/lib/mergeStatements.js +70 -51
  121. package/packages/ai/src/lib/mergeStatements.js.map +1 -1
  122. package/packages/ai/src/lib/promptGenerators/gatherAttributesMap.js +10 -4
  123. package/packages/ai/src/lib/promptGenerators/gatherAttributesMap.js.map +1 -1
  124. package/packages/ai/src/lib/resolvePathToControllable.js +24 -14
  125. package/packages/ai/src/lib/resolvePathToControllable.js.map +1 -1
  126. package/packages/ai/src/lib/worker/SerializableDataStructure.js.map +1 -1
  127. package/packages/analyze/src/lib/FileAnalyzer.js +60 -36
  128. package/packages/analyze/src/lib/FileAnalyzer.js.map +1 -1
  129. package/packages/analyze/src/lib/ProjectAnalyzer.js +96 -26
  130. package/packages/analyze/src/lib/ProjectAnalyzer.js.map +1 -1
  131. package/packages/analyze/src/lib/asts/sourceFiles/getAllDeclaredEntityNodes.js +14 -0
  132. package/packages/analyze/src/lib/asts/sourceFiles/getAllDeclaredEntityNodes.js.map +1 -1
  133. package/packages/analyze/src/lib/asts/sourceFiles/getAllEntityNodes.js +14 -0
  134. package/packages/analyze/src/lib/asts/sourceFiles/getAllEntityNodes.js.map +1 -1
  135. package/packages/analyze/src/lib/asts/sourceFiles/getAllExports.js +6 -0
  136. package/packages/analyze/src/lib/asts/sourceFiles/getAllExports.js.map +1 -1
  137. package/packages/analyze/src/lib/asts/sourceFiles/getImportsAnalysis.js +6 -0
  138. package/packages/analyze/src/lib/asts/sourceFiles/getImportsAnalysis.js.map +1 -1
  139. package/packages/analyze/src/lib/asts/sourceFiles/getResolvedModule.js +39 -1
  140. package/packages/analyze/src/lib/asts/sourceFiles/getResolvedModule.js.map +1 -1
  141. package/packages/analyze/src/lib/asts/sourceFiles/getSourceFilesForAllImports.js +2 -1
  142. package/packages/analyze/src/lib/asts/sourceFiles/getSourceFilesForAllImports.js.map +1 -1
  143. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +13 -5
  144. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
  145. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +14 -4
  146. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  147. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +2 -1
  148. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
  149. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +0 -3
  150. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
  151. package/packages/analyze/src/lib/files/analyzeRemixRoute.js +3 -2
  152. package/packages/analyze/src/lib/files/analyzeRemixRoute.js.map +1 -1
  153. package/packages/analyze/src/lib/files/getImportedExports.js +11 -7
  154. package/packages/analyze/src/lib/files/getImportedExports.js.map +1 -1
  155. package/packages/analyze/src/lib/files/scenarios/enrichArrayTypesFromChildSignatures.js +52 -10
  156. package/packages/analyze/src/lib/files/scenarios/enrichArrayTypesFromChildSignatures.js.map +1 -1
  157. package/packages/analyze/src/lib/files/scenarios/gatherDataForMocks.js +25 -8
  158. package/packages/analyze/src/lib/files/scenarios/gatherDataForMocks.js.map +1 -1
  159. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +34 -4
  160. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  161. package/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.js +56 -8
  162. package/packages/analyze/src/lib/files/scenarios/generateExecutionFlows.js.map +1 -1
  163. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +168 -9
  164. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  165. package/packages/analyze/src/lib/files/setImportedExports.js +2 -1
  166. package/packages/analyze/src/lib/files/setImportedExports.js.map +1 -1
  167. package/packages/analyze/src/lib/utils/getFileByPath.js +12 -0
  168. package/packages/analyze/src/lib/utils/getFileByPath.js.map +1 -0
  169. package/codeyam-cli/src/webserver/build/client/assets/globals-D3yhhV8x.css +0 -1
  170. package/codeyam-cli/src/webserver/build/client/assets/memory-yxFcrxBX.js +0 -92
  171. package/codeyam-cli/src/webserver/build/server/assets/server-build-4Cr0uToj.js +0 -257
@@ -152,7 +152,7 @@ export interface ScopeInfo {
152
152
  [childComponentName: string]: Array<{
153
153
  path: string;
154
154
  conditionType: 'truthiness' | 'comparison';
155
- location: 'if' | 'ternary' | 'logical-and' | 'switch';
155
+ location: 'if' | 'ternary' | 'logical-and' | 'switch' | 'unconditional';
156
156
  isNegated?: boolean;
157
157
  }>;
158
158
  };
@@ -424,7 +424,7 @@ export class ScopeDataStructure {
424
424
  path: string;
425
425
  conditionType: 'truthiness' | 'comparison' | 'switch';
426
426
  comparedValues?: string[];
427
- location: 'if' | 'ternary' | 'logical-and' | 'switch';
427
+ location: 'if' | 'ternary' | 'logical-and' | 'switch' | 'unconditional';
428
428
  }>
429
429
  > = {};
430
430
 
@@ -1650,17 +1650,26 @@ export class ScopeDataStructure {
1650
1650
  private setInstantiatedVariables(scopeNode: ScopeNode) {
1651
1651
  let instantiatedVariables = scopeNode.analysis?.instantiatedVariables ?? [];
1652
1652
 
1653
- for (const [path, equivalentPath] of Object.entries(
1653
+ for (const [path, rawEquivalentPath] of Object.entries(
1654
1654
  scopeNode.analysis.isolatedEquivalentVariables ?? {},
1655
1655
  )) {
1656
- if (typeof equivalentPath !== 'string') {
1657
- continue;
1658
- }
1656
+ // Normalize to array for consistent handling (supports both string and string[])
1657
+ const equivalentPaths = Array.isArray(rawEquivalentPath)
1658
+ ? rawEquivalentPath
1659
+ : rawEquivalentPath
1660
+ ? [rawEquivalentPath]
1661
+ : [];
1662
+
1663
+ for (const equivalentPath of equivalentPaths) {
1664
+ if (typeof equivalentPath !== 'string') {
1665
+ continue;
1666
+ }
1659
1667
 
1660
- if (equivalentPath.startsWith('signature[')) {
1661
- const equivalentPathParts = this.splitPath(equivalentPath);
1662
- instantiatedVariables.push(equivalentPathParts[0]);
1663
- instantiatedVariables.push(path);
1668
+ if (equivalentPath.startsWith('signature[')) {
1669
+ const equivalentPathParts = this.splitPath(equivalentPath);
1670
+ instantiatedVariables.push(equivalentPathParts[0]);
1671
+ instantiatedVariables.push(path);
1672
+ }
1664
1673
  }
1665
1674
 
1666
1675
  const duplicateInstantiated = instantiatedVariables.find(
@@ -1673,9 +1682,14 @@ export class ScopeDataStructure {
1673
1682
  }
1674
1683
  }
1675
1684
 
1676
- instantiatedVariables = instantiatedVariables.filter(
1677
- (varName, index, self) => self.indexOf(varName) === index,
1678
- );
1685
+ const instantiatedSeen = new Set<string>();
1686
+ instantiatedVariables = instantiatedVariables.filter((varName) => {
1687
+ if (instantiatedSeen.has(varName)) {
1688
+ return false;
1689
+ }
1690
+ instantiatedSeen.add(varName);
1691
+ return true;
1692
+ });
1679
1693
 
1680
1694
  scopeNode.instantiatedVariables = instantiatedVariables;
1681
1695
 
@@ -1696,13 +1710,19 @@ export class ScopeDataStructure {
1696
1710
  ...parentScopeNode.instantiatedVariables.filter(
1697
1711
  (v) => !v.startsWith('signature[') && !v.startsWith('returnValue'),
1698
1712
  ),
1699
- ].filter(
1700
- (varName, index, self) =>
1701
- !instantiatedVariables.includes(varName) &&
1702
- self.indexOf(varName) === index,
1703
- );
1713
+ ].filter((varName) => !instantiatedSeen.has(varName));
1704
1714
 
1705
- scopeNode.parentInstantiatedVariables = parentInstantiatedVariables;
1715
+ const parentInstantiatedSeen = new Set<string>();
1716
+ const dedupedParentInstantiatedVariables =
1717
+ parentInstantiatedVariables.filter((varName) => {
1718
+ if (parentInstantiatedSeen.has(varName)) {
1719
+ return false;
1720
+ }
1721
+ parentInstantiatedSeen.add(varName);
1722
+ return true;
1723
+ });
1724
+
1725
+ scopeNode.parentInstantiatedVariables = dedupedParentInstantiatedVariables;
1706
1726
  }
1707
1727
 
1708
1728
  private trackFunctionCalls(scopeNode: ScopeNode) {
@@ -1718,172 +1738,198 @@ export class ScopeDataStructure {
1718
1738
  const { isolatedStructure, isolatedEquivalentVariables } =
1719
1739
  scopeNode.analysis;
1720
1740
 
1741
+ // Flatten isolatedEquivalentVariables values for allPaths (handles both string and string[])
1742
+ const flattenedEquivValues = Object.values(
1743
+ isolatedEquivalentVariables || {},
1744
+ ).flatMap((v) => (Array.isArray(v) ? v : [v]));
1745
+
1721
1746
  const allPaths = Array.from(
1722
1747
  new Set([
1723
1748
  ...Object.keys(isolatedStructure || {}),
1724
1749
  ...Object.keys(isolatedEquivalentVariables || {}),
1725
- ...Object.values(isolatedEquivalentVariables || {}),
1750
+ ...flattenedEquivValues,
1726
1751
  ]),
1727
1752
  );
1728
1753
 
1729
1754
  for (let path in isolatedEquivalentVariables) {
1730
- let equivalentValue = isolatedEquivalentVariables?.[path];
1731
-
1732
- if (equivalentValue && this.isValidPath(equivalentValue)) {
1733
- // IMPORTANT: DO NOT strip ::cyDuplicateKey:: markers from equivalencies.
1734
- // These markers are critical for distinguishing variable reassignments.
1735
- // For example, with:
1736
- // let fetcher = useFetcher<ConfigData>();
1737
- // const configData = fetcher.data?.data;
1738
- // fetcher = useFetcher<SettingsData>();
1739
- // const settingsData = fetcher.data?.data;
1740
- //
1741
- // mergeStatements creates:
1742
- // fetcher useFetcher<ConfigData>()...
1743
- // fetcher::cyDuplicateKey1:: useFetcher<SettingsData>()...
1744
- // configData fetcher.data.data
1745
- // settingsData → fetcher::cyDuplicateKey1::.data.data
1746
- //
1747
- // If we strip ::cyDuplicateKey::, settingsData would incorrectly trace
1748
- // to useFetcher<ConfigData>() instead of useFetcher<SettingsData>().
1749
- path = cleanPath(path, allPaths);
1750
- equivalentValue = cleanPath(equivalentValue, allPaths);
1751
-
1752
- this.addEquivalency(
1753
- path,
1754
- equivalentValue,
1755
- scopeNode.name,
1756
- scopeNode,
1757
- 'original equivalency',
1758
- );
1755
+ const rawEquivalentValue = isolatedEquivalentVariables?.[path];
1756
+ // Normalize to array for consistent handling
1757
+ const equivalentValues = Array.isArray(rawEquivalentValue)
1758
+ ? rawEquivalentValue
1759
+ : [rawEquivalentValue];
1760
+
1761
+ for (let equivalentValue of equivalentValues) {
1762
+ if (equivalentValue && this.isValidPath(equivalentValue)) {
1763
+ // IMPORTANT: DO NOT strip ::cyDuplicateKey:: markers from equivalencies.
1764
+ // These markers are critical for distinguishing variable reassignments.
1765
+ // For example, with:
1766
+ // let fetcher = useFetcher<ConfigData>();
1767
+ // const configData = fetcher.data?.data;
1768
+ // fetcher = useFetcher<SettingsData>();
1769
+ // const settingsData = fetcher.data?.data;
1770
+ //
1771
+ // mergeStatements creates:
1772
+ // fetcher useFetcher<ConfigData>()...
1773
+ // fetcher::cyDuplicateKey1:: useFetcher<SettingsData>()...
1774
+ // configData fetcher.data.data
1775
+ // settingsData fetcher::cyDuplicateKey1::.data.data
1776
+ //
1777
+ // If we strip ::cyDuplicateKey::, settingsData would incorrectly trace
1778
+ // to useFetcher<ConfigData>() instead of useFetcher<SettingsData>().
1779
+ path = cleanPath(path, allPaths);
1780
+ equivalentValue = cleanPath(equivalentValue, allPaths);
1781
+
1782
+ this.addEquivalency(
1783
+ path,
1784
+ equivalentValue,
1785
+ scopeNode.name,
1786
+ scopeNode,
1787
+ 'original equivalency',
1788
+ );
1759
1789
 
1760
- // Propagate equivalencies involving parent-scope variables to those parent scopes.
1761
- // This handles patterns like: collected.push({...entity}) where 'collected' is defined
1762
- // in a parent scope. The equivalency collected[] -> push().signature[0] needs to be
1763
- // visible when tracing from the parent scope.
1764
- const rootVariable = this.extractRootVariable(path);
1765
- const equivalentRootVariable =
1766
- this.extractRootVariable(equivalentValue);
1767
-
1768
- // Skip propagation for self-referential reassignment patterns like:
1769
- // x = x.method().functionCallReturnValue
1770
- // where the path IS the variable itself (not a sub-path like x[] or x.prop).
1771
- // These create circular references since both sides reference the same variable.
1772
- //
1773
- // But DO propagate for patterns like collected[] -> collected.push(...).signature[0]
1774
- // where the path has additional segments beyond the root variable.
1775
- const pathIsJustRootVariable = path === rootVariable;
1776
- const isSelfReferentialReassignment =
1777
- pathIsJustRootVariable && rootVariable === equivalentRootVariable;
1790
+ // Propagate equivalencies involving parent-scope variables to those parent scopes.
1791
+ // This handles patterns like: collected.push({...entity}) where 'collected' is defined
1792
+ // in a parent scope. The equivalency collected[] -> push().signature[0] needs to be
1793
+ // visible when tracing from the parent scope.
1794
+ const rootVariable = this.extractRootVariable(path);
1795
+ const equivalentRootVariable =
1796
+ this.extractRootVariable(equivalentValue);
1797
+
1798
+ // Skip propagation for self-referential reassignment patterns like:
1799
+ // x = x.method().functionCallReturnValue
1800
+ // where the path IS the variable itself (not a sub-path like x[] or x.prop).
1801
+ // These create circular references since both sides reference the same variable.
1802
+ //
1803
+ // But DO propagate for patterns like collected[] -> collected.push(...).signature[0]
1804
+ // where the path has additional segments beyond the root variable.
1805
+ const pathIsJustRootVariable = path === rootVariable;
1806
+ const isSelfReferentialReassignment =
1807
+ pathIsJustRootVariable && rootVariable === equivalentRootVariable;
1778
1808
 
1779
- if (
1780
- rootVariable &&
1781
- !isSelfReferentialReassignment &&
1782
- scopeNode.parentInstantiatedVariables?.includes(rootVariable)
1783
- ) {
1784
- // Find the parent scope where this variable is defined
1785
- for (const parentScopeName of scopeNode.tree || []) {
1786
- const parentScope = this.scopeNodes[parentScopeName];
1787
- if (parentScope?.instantiatedVariables?.includes(rootVariable)) {
1788
- // Add the equivalency to the parent scope as well
1789
- this.addEquivalency(
1790
- path,
1791
- equivalentValue,
1792
- scopeNode.name, // The equivalent path's scope remains the child scope
1793
- parentScope, // But store it in the parent scope's equivalencies
1794
- 'propagated parent-variable equivalency',
1795
- );
1796
- break;
1809
+ if (
1810
+ rootVariable &&
1811
+ !isSelfReferentialReassignment &&
1812
+ scopeNode.parentInstantiatedVariables?.includes(rootVariable)
1813
+ ) {
1814
+ // Find the parent scope where this variable is defined
1815
+ for (const parentScopeName of scopeNode.tree || []) {
1816
+ const parentScope = this.scopeNodes[parentScopeName];
1817
+ if (parentScope?.instantiatedVariables?.includes(rootVariable)) {
1818
+ // Add the equivalency to the parent scope as well
1819
+ this.addEquivalency(
1820
+ path,
1821
+ equivalentValue,
1822
+ scopeNode.name, // The equivalent path's scope remains the child scope
1823
+ parentScope, // But store it in the parent scope's equivalencies
1824
+ 'propagated parent-variable equivalency',
1825
+ );
1826
+ break;
1827
+ }
1797
1828
  }
1798
1829
  }
1799
- }
1800
1830
 
1801
- // Propagate sub-property equivalencies when the equivalentValue is a simple variable
1802
- // that has sub-properties defined in the isolatedEquivalentVariables.
1803
- // This handles cases like: dataItem={{ structure: completeDataStructure }}
1804
- // where completeDataStructure has sub-properties like completeDataStructure['Function Arguments']
1805
- // We need to propagate these to create: dataItem.structure['Function Arguments'] equivalencies
1806
- const isSimpleVariable =
1807
- !equivalentValue.startsWith('signature[') &&
1808
- !equivalentValue.includes('functionCallReturnValue') &&
1809
- !equivalentValue.includes('.') &&
1810
- !equivalentValue.includes('[');
1811
-
1812
- if (isSimpleVariable) {
1813
- // Look in current scope and all parent scopes for sub-properties
1814
- const scopesToCheck = [scopeNode.name, ...scopeNode.tree];
1815
- for (const scopeName of scopesToCheck) {
1816
- const checkScope = this.scopeNodes[scopeName];
1817
- if (!checkScope?.analysis?.isolatedEquivalentVariables) continue;
1818
-
1819
- for (const [subPath, subValue] of Object.entries(
1820
- checkScope.analysis.isolatedEquivalentVariables,
1821
- )) {
1822
- // Check if this is a sub-property of the equivalentValue variable
1823
- // e.g., completeDataStructure['Function Arguments'] or completeDataStructure.foo
1824
- const matchesDot = subPath.startsWith(equivalentValue + '.');
1825
- const matchesBracket = subPath.startsWith(equivalentValue + '[');
1826
- if (matchesDot || matchesBracket) {
1827
- const subPropertyPath = subPath.substring(
1828
- equivalentValue.length,
1829
- );
1830
- const newPath = cleanPath(path + subPropertyPath, allPaths);
1831
- const newEquivalentValue = cleanPath(
1832
- (subValue as string).replace(/::cyDuplicateKey\d+::/g, ''),
1833
- allPaths,
1831
+ // Propagate sub-property equivalencies when the equivalentValue is a simple variable
1832
+ // that has sub-properties defined in the isolatedEquivalentVariables.
1833
+ // This handles cases like: dataItem={{ structure: completeDataStructure }}
1834
+ // where completeDataStructure has sub-properties like completeDataStructure['Function Arguments']
1835
+ // We need to propagate these to create: dataItem.structure['Function Arguments'] equivalencies
1836
+ const isSimpleVariable =
1837
+ !equivalentValue.startsWith('signature[') &&
1838
+ !equivalentValue.includes('functionCallReturnValue') &&
1839
+ !equivalentValue.includes('.') &&
1840
+ !equivalentValue.includes('[');
1841
+
1842
+ if (isSimpleVariable) {
1843
+ // Look in current scope and all parent scopes for sub-properties
1844
+ const scopesToCheck = [scopeNode.name, ...scopeNode.tree];
1845
+ for (const scopeName of scopesToCheck) {
1846
+ const checkScope = this.scopeNodes[scopeName];
1847
+ if (!checkScope?.analysis?.isolatedEquivalentVariables) continue;
1848
+
1849
+ for (const [subPath, rawSubValue] of Object.entries(
1850
+ checkScope.analysis.isolatedEquivalentVariables,
1851
+ )) {
1852
+ // Normalize to array for consistent handling
1853
+ const subValues = Array.isArray(rawSubValue)
1854
+ ? rawSubValue
1855
+ : rawSubValue
1856
+ ? [rawSubValue]
1857
+ : [];
1858
+
1859
+ // Check if this is a sub-property of the equivalentValue variable
1860
+ // e.g., completeDataStructure['Function Arguments'] or completeDataStructure.foo
1861
+ const matchesDot = subPath.startsWith(equivalentValue + '.');
1862
+ const matchesBracket = subPath.startsWith(
1863
+ equivalentValue + '[',
1834
1864
  );
1835
-
1836
- if (
1837
- newEquivalentValue &&
1838
- this.isValidPath(newEquivalentValue)
1839
- ) {
1840
- this.addEquivalency(
1841
- newPath,
1842
- newEquivalentValue,
1843
- checkScope.name, // Use the scope where the sub-property was found
1844
- scopeNode,
1845
- 'propagated sub-property equivalency',
1865
+ if (matchesDot || matchesBracket) {
1866
+ const subPropertyPath = subPath.substring(
1867
+ equivalentValue.length,
1846
1868
  );
1869
+ const newPath = cleanPath(path + subPropertyPath, allPaths);
1870
+
1871
+ for (const subValue of subValues) {
1872
+ if (typeof subValue !== 'string') continue;
1873
+ const newEquivalentValue = cleanPath(
1874
+ subValue.replace(/::cyDuplicateKey\d+::/g, ''),
1875
+ allPaths,
1876
+ );
1877
+
1878
+ if (
1879
+ newEquivalentValue &&
1880
+ this.isValidPath(newEquivalentValue)
1881
+ ) {
1882
+ this.addEquivalency(
1883
+ newPath,
1884
+ newEquivalentValue,
1885
+ checkScope.name, // Use the scope where the sub-property was found
1886
+ scopeNode,
1887
+ 'propagated sub-property equivalency',
1888
+ );
1889
+ }
1890
+ }
1847
1891
  }
1848
- }
1849
1892
 
1850
- // Also check if equivalentValue itself maps to a functionCallReturnValue
1851
- // e.g., result = useMemo(...).functionCallReturnValue
1852
- if (
1853
- subPath === equivalentValue &&
1854
- typeof subValue === 'string' &&
1855
- subValue.endsWith('.functionCallReturnValue')
1856
- ) {
1857
- this.propagateFunctionCallReturnSubProperties(
1858
- path,
1859
- subValue,
1860
- scopeNode,
1861
- allPaths,
1862
- );
1893
+ // Also check if equivalentValue itself maps to a functionCallReturnValue
1894
+ // e.g., result = useMemo(...).functionCallReturnValue
1895
+ for (const subValue of subValues) {
1896
+ if (
1897
+ subPath === equivalentValue &&
1898
+ typeof subValue === 'string' &&
1899
+ subValue.endsWith('.functionCallReturnValue')
1900
+ ) {
1901
+ this.propagateFunctionCallReturnSubProperties(
1902
+ path,
1903
+ subValue,
1904
+ scopeNode,
1905
+ allPaths,
1906
+ );
1907
+ }
1908
+ }
1863
1909
  }
1864
1910
  }
1865
1911
  }
1866
- }
1867
1912
 
1868
- // Handle function call return values by propagating returnValue.* sub-properties
1869
- // from the callback scope to the usage path
1870
- if (equivalentValue.endsWith('.functionCallReturnValue')) {
1871
- this.propagateFunctionCallReturnSubProperties(
1872
- path,
1873
- equivalentValue,
1874
- scopeNode,
1875
- allPaths,
1876
- );
1913
+ // Handle function call return values by propagating returnValue.* sub-properties
1914
+ // from the callback scope to the usage path
1915
+ if (equivalentValue.endsWith('.functionCallReturnValue')) {
1916
+ this.propagateFunctionCallReturnSubProperties(
1917
+ path,
1918
+ equivalentValue,
1919
+ scopeNode,
1920
+ allPaths,
1921
+ );
1877
1922
 
1878
- // Track which variable receives the return value of each function call
1879
- // This enables generating separate mock data for each call site
1880
- this.trackReceivingVariable(path, equivalentValue);
1881
- }
1923
+ // Track which variable receives the return value of each function call
1924
+ // This enables generating separate mock data for each call site
1925
+ this.trackReceivingVariable(path, equivalentValue);
1926
+ }
1882
1927
 
1883
- // Also track variables that receive destructured properties from function call return values
1884
- // e.g., "userData" -> "db.query('users').functionCallReturnValue.data"
1885
- if (equivalentValue.includes('.functionCallReturnValue.')) {
1886
- this.trackReceivingVariable(path, equivalentValue);
1928
+ // Also track variables that receive destructured properties from function call return values
1929
+ // e.g., "userData" -> "db.query('users').functionCallReturnValue.data"
1930
+ if (equivalentValue.includes('.functionCallReturnValue.')) {
1931
+ this.trackReceivingVariable(path, equivalentValue);
1932
+ }
1887
1933
  }
1888
1934
  }
1889
1935
  }
@@ -2064,9 +2110,18 @@ export class ScopeDataStructure {
2064
2110
  const checkScope = this.scopeNodes[scopeName];
2065
2111
  if (!checkScope?.analysis?.isolatedEquivalentVariables) continue;
2066
2112
 
2067
- const functionRef =
2113
+ const rawFunctionRef =
2068
2114
  checkScope.analysis.isolatedEquivalentVariables[functionName];
2069
- if (typeof functionRef === 'string' && functionRef.endsWith('F')) {
2115
+ // Normalize to array and find first string ending with 'F'
2116
+ const functionRefs = Array.isArray(rawFunctionRef)
2117
+ ? rawFunctionRef
2118
+ : rawFunctionRef
2119
+ ? [rawFunctionRef]
2120
+ : [];
2121
+ const functionRef = functionRefs.find(
2122
+ (r) => typeof r === 'string' && r.endsWith('F'),
2123
+ );
2124
+ if (typeof functionRef === 'string') {
2070
2125
  callbackScopeName = functionRef.slice(0, -1);
2071
2126
  break;
2072
2127
  }
@@ -2094,19 +2149,24 @@ export class ScopeDataStructure {
2094
2149
 
2095
2150
  const isolatedVars = callbackScope.analysis.isolatedEquivalentVariables;
2096
2151
 
2152
+ // Get the first returnValue equivalency (normalize array to single value for these checks)
2153
+ const rawReturnValue = isolatedVars.returnValue;
2154
+ const firstReturnValue = Array.isArray(rawReturnValue)
2155
+ ? rawReturnValue[0]
2156
+ : rawReturnValue;
2157
+
2097
2158
  // First, check if returnValue is an alias to another variable (e.g., returnValue = intermediate)
2098
2159
  // If so, we need to look for that variable's sub-properties too
2099
2160
  const returnValueAlias =
2100
- typeof isolatedVars.returnValue === 'string' &&
2101
- !isolatedVars.returnValue.includes('.')
2102
- ? isolatedVars.returnValue
2161
+ typeof firstReturnValue === 'string' && !firstReturnValue.includes('.')
2162
+ ? firstReturnValue
2103
2163
  : undefined;
2104
2164
 
2105
2165
  // Pattern 3: Object.keys(X).reduce() - the reduce result has the same sub-properties as X
2106
2166
  // When returnValue = "Object.keys(source).reduce(...).functionCallReturnValue", look for source.* sub-properties
2107
2167
  let reduceSourceVar: string | undefined;
2108
- if (typeof isolatedVars.returnValue === 'string') {
2109
- const reduceMatch = isolatedVars.returnValue.match(
2168
+ if (typeof firstReturnValue === 'string') {
2169
+ const reduceMatch = firstReturnValue.match(
2110
2170
  /^Object\.keys\((\w+)\)\.reduce\(.*\)\.functionCallReturnValue$/,
2111
2171
  );
2112
2172
  if (reduceMatch) {
@@ -2114,7 +2174,14 @@ export class ScopeDataStructure {
2114
2174
  }
2115
2175
  }
2116
2176
 
2117
- for (const [subPath, subValue] of Object.entries(isolatedVars)) {
2177
+ for (const [subPath, rawSubValue] of Object.entries(isolatedVars)) {
2178
+ // Normalize to array for consistent handling
2179
+ const subValues = Array.isArray(rawSubValue)
2180
+ ? rawSubValue
2181
+ : rawSubValue
2182
+ ? [rawSubValue]
2183
+ : [];
2184
+
2118
2185
  // Check for direct returnValue.* sub-properties
2119
2186
  const isReturnValueSub =
2120
2187
  subPath.startsWith('returnValue.') ||
@@ -2132,57 +2199,59 @@ export class ScopeDataStructure {
2132
2199
  (subPath.startsWith(reduceSourceVar + '.') ||
2133
2200
  subPath.startsWith(reduceSourceVar + '['));
2134
2201
 
2135
- if (
2136
- typeof subValue !== 'string' ||
2137
- (!isReturnValueSub && !isAliasSub && !isReduceSourceSub)
2138
- )
2139
- continue;
2140
-
2141
- // Convert alias/reduceSource paths to returnValue paths
2142
- let effectiveSubPath = subPath;
2143
- if (isAliasSub && !isReturnValueSub) {
2144
- // Replace the alias prefix with returnValue
2145
- effectiveSubPath =
2146
- 'returnValue' + subPath.substring(returnValueAlias!.length);
2147
- } else if (isReduceSourceSub && !isReturnValueSub && !isAliasSub) {
2148
- // Replace the reduce source prefix with returnValue
2149
- effectiveSubPath =
2150
- 'returnValue' + subPath.substring(reduceSourceVar!.length);
2151
- }
2152
- const subPropertyPath = effectiveSubPath.substring('returnValue'.length);
2153
- const newPath = cleanPath(path + subPropertyPath, allPaths);
2154
- let newEquivalentValue = cleanPath(
2155
- subValue.replace(/::cyDuplicateKey\d+::/g, ''),
2156
- allPaths,
2157
- );
2202
+ if (!isReturnValueSub && !isAliasSub && !isReduceSourceSub) continue;
2203
+
2204
+ for (const subValue of subValues) {
2205
+ if (typeof subValue !== 'string') continue;
2206
+
2207
+ // Convert alias/reduceSource paths to returnValue paths
2208
+ let effectiveSubPath = subPath;
2209
+ if (isAliasSub && !isReturnValueSub) {
2210
+ // Replace the alias prefix with returnValue
2211
+ effectiveSubPath =
2212
+ 'returnValue' + subPath.substring(returnValueAlias!.length);
2213
+ } else if (isReduceSourceSub && !isReturnValueSub && !isAliasSub) {
2214
+ // Replace the reduce source prefix with returnValue
2215
+ effectiveSubPath =
2216
+ 'returnValue' + subPath.substring(reduceSourceVar!.length);
2217
+ }
2218
+ const subPropertyPath = effectiveSubPath.substring(
2219
+ 'returnValue'.length,
2220
+ );
2221
+ const newPath = cleanPath(path + subPropertyPath, allPaths);
2222
+ let newEquivalentValue = cleanPath(
2223
+ subValue.replace(/::cyDuplicateKey\d+::/g, ''),
2224
+ allPaths,
2225
+ );
2158
2226
 
2159
- // Resolve variable references through parent scope equivalencies
2160
- const resolved = this.resolveVariableThroughParentScopes(
2161
- newEquivalentValue,
2162
- callbackScope,
2163
- allPaths,
2164
- );
2165
- newEquivalentValue = resolved.resolvedPath;
2166
- const equivalentScopeName = resolved.scopeName;
2227
+ // Resolve variable references through parent scope equivalencies
2228
+ const resolved = this.resolveVariableThroughParentScopes(
2229
+ newEquivalentValue,
2230
+ callbackScope,
2231
+ allPaths,
2232
+ );
2233
+ newEquivalentValue = resolved.resolvedPath;
2234
+ const equivalentScopeName = resolved.scopeName;
2167
2235
 
2168
- if (!newEquivalentValue || !this.isValidPath(newEquivalentValue))
2169
- continue;
2236
+ if (!newEquivalentValue || !this.isValidPath(newEquivalentValue))
2237
+ continue;
2170
2238
 
2171
- this.addEquivalency(
2172
- newPath,
2173
- newEquivalentValue,
2174
- equivalentScopeName,
2175
- scopeNode,
2176
- 'propagated function call return sub-property equivalency',
2177
- );
2239
+ this.addEquivalency(
2240
+ newPath,
2241
+ newEquivalentValue,
2242
+ equivalentScopeName,
2243
+ scopeNode,
2244
+ 'propagated function call return sub-property equivalency',
2245
+ );
2178
2246
 
2179
- // Ensure the database entry has the usage path
2180
- this.addUsageToEquivalencyDatabaseEntry(
2181
- newPath,
2182
- newEquivalentValue,
2183
- equivalentScopeName,
2184
- scopeNode.name,
2185
- );
2247
+ // Ensure the database entry has the usage path
2248
+ this.addUsageToEquivalencyDatabaseEntry(
2249
+ newPath,
2250
+ newEquivalentValue,
2251
+ equivalentScopeName,
2252
+ scopeNode.name,
2253
+ );
2254
+ }
2186
2255
  }
2187
2256
  }
2188
2257
 
@@ -2222,8 +2291,15 @@ export class ScopeDataStructure {
2222
2291
  const parentScope = this.scopeNodes[parentScopeName];
2223
2292
  if (!parentScope?.analysis?.isolatedEquivalentVariables) continue;
2224
2293
 
2225
- const rootEquiv =
2294
+ const rawRootEquiv =
2226
2295
  parentScope.analysis.isolatedEquivalentVariables[rootVar];
2296
+ // Normalize to array and use first string value
2297
+ const rootEquivs = Array.isArray(rawRootEquiv)
2298
+ ? rawRootEquiv
2299
+ : rawRootEquiv
2300
+ ? [rawRootEquiv]
2301
+ : [];
2302
+ const rootEquiv = rootEquivs.find((r) => typeof r === 'string');
2227
2303
  if (typeof rootEquiv === 'string') {
2228
2304
  return {
2229
2305
  resolvedPath: cleanPath(rootEquiv + restOfPath, allPaths),
@@ -2889,10 +2965,105 @@ export class ScopeDataStructure {
2889
2965
  this.intermediatesOrderIndex.set(pathId, databaseEntry);
2890
2966
 
2891
2967
  if (intermediateIndex === 0) {
2892
- const isValidSourceCandidate =
2968
+ let isValidSourceCandidate =
2893
2969
  pathInfo.schemaPath.startsWith('signature[') ||
2894
2970
  pathInfo.schemaPath.includes('functionCallReturnValue');
2895
- if (isValidSourceCandidate) {
2971
+
2972
+ // Check if path STARTS with a spread pattern like [...var]
2973
+ // This handles cases like [...files][][0] or [...files].sort(...).functionCallReturnValue[][0]
2974
+ // where the spread source variable needs to be resolved to a signature path.
2975
+ // We do this REGARDLESS of isValidSourceCandidate because even paths containing
2976
+ // functionCallReturnValue may need spread resolution to trace back to the signature.
2977
+ const spreadMatch = pathInfo.schemaPath.match(/^\[\.\.\.(\w+)\]/);
2978
+ if (spreadMatch) {
2979
+ const spreadVar = spreadMatch[1];
2980
+ const spreadPattern = spreadMatch[0]; // The full [...var] match
2981
+ const scopeNode = this.scopeNodes[pathInfo.scopeNodeName];
2982
+
2983
+ if (scopeNode?.equivalencies) {
2984
+ // Follow the equivalency chain to find a signature path
2985
+ // e.g., files (cyScope1) → files (root) → signature[0].files
2986
+ const resolveToSignature = (
2987
+ varName: string,
2988
+ currentScopeName: string,
2989
+ visited: Set<string>,
2990
+ ): { schemaPath: string; scopeNodeName: string } | null => {
2991
+ const visitKey = `${currentScopeName}::${varName}`;
2992
+ if (visited.has(visitKey)) return null;
2993
+ visited.add(visitKey);
2994
+
2995
+ const currentScope = this.scopeNodes[currentScopeName];
2996
+ if (!currentScope?.equivalencies) return null;
2997
+
2998
+ const varEquivs = currentScope.equivalencies[varName];
2999
+ if (!varEquivs) return null;
3000
+
3001
+ // First check if any equivalency directly points to a signature path
3002
+ const signatureEquiv = varEquivs.find((eq) =>
3003
+ eq.schemaPath.startsWith('signature['),
3004
+ );
3005
+ if (signatureEquiv) {
3006
+ return signatureEquiv;
3007
+ }
3008
+
3009
+ // Otherwise, follow the chain to other scopes
3010
+ for (const equiv of varEquivs) {
3011
+ // If the equivalency points to the same variable in a different scope,
3012
+ // follow the chain
3013
+ if (
3014
+ equiv.schemaPath === varName &&
3015
+ equiv.scopeNodeName !== currentScopeName
3016
+ ) {
3017
+ const result = resolveToSignature(
3018
+ varName,
3019
+ equiv.scopeNodeName,
3020
+ visited,
3021
+ );
3022
+ if (result) return result;
3023
+ }
3024
+ }
3025
+
3026
+ return null;
3027
+ };
3028
+
3029
+ const signatureEquiv = resolveToSignature(
3030
+ spreadVar,
3031
+ pathInfo.scopeNodeName,
3032
+ new Set(),
3033
+ );
3034
+ if (signatureEquiv) {
3035
+ // Replace ONLY the [...var] part with the resolved signature path
3036
+ // This preserves any suffix like .sort(...).functionCallReturnValue[][0]
3037
+ const resolvedPath = pathInfo.schemaPath.replace(
3038
+ spreadPattern,
3039
+ signatureEquiv.schemaPath,
3040
+ );
3041
+ // Add the resolved path as a source candidate
3042
+ if (
3043
+ !databaseEntry.sourceCandidates.some(
3044
+ (sc) =>
3045
+ sc.schemaPath === resolvedPath &&
3046
+ sc.scopeNodeName === pathInfo.scopeNodeName,
3047
+ )
3048
+ ) {
3049
+ databaseEntry.sourceCandidates.push({
3050
+ scopeNodeName: pathInfo.scopeNodeName,
3051
+ schemaPath: resolvedPath,
3052
+ });
3053
+ }
3054
+ isValidSourceCandidate = true;
3055
+ }
3056
+ }
3057
+ }
3058
+
3059
+ if (
3060
+ isValidSourceCandidate &&
3061
+ !databaseEntry.sourceCandidates.some(
3062
+ (sc) =>
3063
+ sc.schemaPath === pathInfo.schemaPath &&
3064
+ sc.scopeNodeName === pathInfo.scopeNodeName,
3065
+ )
3066
+ ) {
2896
3067
  databaseEntry.sourceCandidates.push(pathInfo);
2897
3068
  }
2898
3069
  } else {
@@ -3479,18 +3650,171 @@ export class ScopeDataStructure {
3479
3650
  return {};
3480
3651
  }
3481
3652
 
3653
+ // Collect all descendant scope names (including the scope itself)
3654
+ // This ensures we include external calls from nested scopes like cyScope2
3655
+ const getAllDescendantScopeNames = (
3656
+ node: import('./helpers/ScopeTreeManager').ScopeTreeNode,
3657
+ ): Set<string> => {
3658
+ const names = new Set<string>([node.name]);
3659
+ for (const child of node.children) {
3660
+ for (const name of getAllDescendantScopeNames(child)) {
3661
+ names.add(name);
3662
+ }
3663
+ }
3664
+ return names;
3665
+ };
3666
+
3667
+ const treeNode = this.scopeTreeManager.findNode(scopeNode.name);
3668
+ const descendantScopeNames = treeNode
3669
+ ? getAllDescendantScopeNames(treeNode)
3670
+ : new Set<string>([scopeNode.name]);
3671
+
3672
+ // Get all external function calls made from this scope or any descendant scope
3673
+ // This allows us to include prop equivalencies from JSX components
3674
+ // that were rendered in nested scopes (e.g., FileTableRow called from cyScope2)
3675
+ const externalCallsFromScope = this.externalFunctionCalls.filter((efc) =>
3676
+ descendantScopeNames.has(efc.callScope),
3677
+ );
3678
+ const externalCallNames = new Set(
3679
+ externalCallsFromScope.map((efc) => efc.name),
3680
+ );
3681
+
3682
+ // Helper to check if a usage belongs to this scope (directly, via descendant, or via external call)
3683
+ const usageMatchesScope = (usage: { scopeNodeName: string }) =>
3684
+ descendantScopeNames.has(usage.scopeNodeName) ||
3685
+ externalCallNames.has(usage.scopeNodeName);
3686
+
3482
3687
  const entries = this.equivalencyDatabase.filter((entry) =>
3483
- entry.usages.some((usage) => usage.scopeNodeName === scopeNode.name),
3688
+ entry.usages.some(usageMatchesScope),
3484
3689
  );
3690
+
3691
+ // Helper to resolve a source candidate through equivalency chains to find signature paths
3692
+ const resolveToSignature = (
3693
+ source: Pick<ScopeVariable, 'scopeNodeName' | 'schemaPath'>,
3694
+ visited: Set<string>,
3695
+ ): Pick<ScopeVariable, 'scopeNodeName' | 'schemaPath'>[] => {
3696
+ const visitKey = `${source.scopeNodeName}::${source.schemaPath}`;
3697
+ if (visited.has(visitKey)) return [];
3698
+ visited.add(visitKey);
3699
+
3700
+ // If already a signature path, return as-is
3701
+ if (source.schemaPath.startsWith('signature[')) {
3702
+ return [source];
3703
+ }
3704
+
3705
+ const currentScope = this.scopeNodes[source.scopeNodeName];
3706
+ if (!currentScope?.equivalencies) return [source];
3707
+
3708
+ // Check for direct equivalencies FIRST (full path match)
3709
+ // This ensures paths like "useMemo(...).functionCallReturnValue" follow to "cyScope1::returnValue"
3710
+ // before prefix matching tries "useMemo(...)" which goes to the useMemo scope
3711
+ const directEquivs = currentScope.equivalencies[source.schemaPath];
3712
+ if (directEquivs?.length > 0) {
3713
+ const results: Pick<ScopeVariable, 'scopeNodeName' | 'schemaPath'>[] =
3714
+ [];
3715
+ for (const equiv of directEquivs) {
3716
+ const resolved = resolveToSignature(
3717
+ {
3718
+ scopeNodeName: equiv.scopeNodeName,
3719
+ schemaPath: equiv.schemaPath,
3720
+ },
3721
+ visited,
3722
+ );
3723
+ results.push(...resolved);
3724
+ }
3725
+ if (results.length > 0) return results;
3726
+ }
3727
+
3728
+ // Handle spread patterns like [...items].sort().functionCallReturnValue
3729
+ // Extract the spread variable and resolve it through the equivalency chain
3730
+ const spreadMatch = source.schemaPath.match(/^\[\.\.\.(\w+)\]/);
3731
+ if (spreadMatch) {
3732
+ const spreadVar = spreadMatch[1];
3733
+ const spreadPattern = spreadMatch[0];
3734
+ const varEquivs = currentScope.equivalencies[spreadVar];
3735
+
3736
+ if (varEquivs?.length > 0) {
3737
+ const results: Pick<ScopeVariable, 'scopeNodeName' | 'schemaPath'>[] =
3738
+ [];
3739
+ for (const equiv of varEquivs) {
3740
+ // Follow the variable equivalency and then resolve from there
3741
+ const resolvedVar = resolveToSignature(
3742
+ {
3743
+ scopeNodeName: equiv.scopeNodeName,
3744
+ schemaPath: equiv.schemaPath,
3745
+ },
3746
+ visited,
3747
+ );
3748
+ // For each resolved variable path, create the full path with array element suffix
3749
+ for (const rv of resolvedVar) {
3750
+ if (rv.schemaPath.startsWith('signature[')) {
3751
+ // Get the suffix after the spread pattern
3752
+ let suffix = source.schemaPath.slice(spreadPattern.length);
3753
+
3754
+ // Clean the suffix: strip array method chains like .sort(...).functionCallReturnValue[]
3755
+ // These don't change the data identity, just transform it.
3756
+ // Keep only the final element access parts like [0], [1], etc.
3757
+ // Pattern: strip everything from a method call up through functionCallReturnValue[]
3758
+ suffix = suffix.replace(
3759
+ /\.\w+\([^)]*\)\.functionCallReturnValue\[\]/g,
3760
+ '',
3761
+ );
3762
+ // Also handle simpler case without nested parens
3763
+ suffix = suffix.replace(
3764
+ /\.sort\(\w*\(\)\)\.functionCallReturnValue\[\]/g,
3765
+ '',
3766
+ );
3767
+
3768
+ // Add [] to indicate array element access from the spread
3769
+ const resolvedPath = rv.schemaPath + '[]' + suffix;
3770
+ results.push({
3771
+ scopeNodeName: rv.scopeNodeName,
3772
+ schemaPath: resolvedPath,
3773
+ });
3774
+ }
3775
+ }
3776
+ }
3777
+ if (results.length > 0) return results;
3778
+ }
3779
+ }
3780
+
3781
+ // Try to find prefix equivalencies that can resolve this path
3782
+ // For path like "cyScope3().signature[0][0]", check "cyScope3().signature[0]", etc.
3783
+ const pathParts = this.splitPath(source.schemaPath);
3784
+ for (let i = pathParts.length - 1; i > 0; i--) {
3785
+ const prefix = this.joinPathParts(pathParts.slice(0, i));
3786
+ const suffix = this.joinPathParts(pathParts.slice(i));
3787
+ const prefixEquivs = currentScope.equivalencies[prefix];
3788
+
3789
+ if (prefixEquivs?.length > 0) {
3790
+ const results: Pick<ScopeVariable, 'scopeNodeName' | 'schemaPath'>[] =
3791
+ [];
3792
+ for (const equiv of prefixEquivs) {
3793
+ const newPath = this.joinPathParts([equiv.schemaPath, suffix]);
3794
+ const resolved = resolveToSignature(
3795
+ { scopeNodeName: equiv.scopeNodeName, schemaPath: newPath },
3796
+ visited,
3797
+ );
3798
+ results.push(...resolved);
3799
+ }
3800
+ if (results.length > 0) return results;
3801
+ }
3802
+ }
3803
+
3804
+ return [source];
3805
+ };
3806
+
3485
3807
  return entries.reduce(
3486
3808
  (acc, entry) => {
3487
3809
  if (entry.sourceCandidates.length === 0) return acc;
3488
- const usages = entry.usages.filter(
3489
- (u) => u.scopeNodeName === scopeNode.name,
3490
- );
3810
+ const usages = entry.usages.filter(usageMatchesScope);
3491
3811
  for (const usage of usages) {
3492
3812
  acc[usage.schemaPath] ||= [];
3493
- acc[usage.schemaPath].push(...entry.sourceCandidates);
3813
+ // Resolve each source candidate through the equivalency chain
3814
+ for (const source of entry.sourceCandidates) {
3815
+ const resolvedSources = resolveToSignature(source, new Set());
3816
+ acc[usage.schemaPath].push(...resolvedSources);
3817
+ }
3494
3818
  }
3495
3819
  return acc;
3496
3820
  },
@@ -3875,10 +4199,32 @@ export class ScopeDataStructure {
3875
4199
  return scopeText;
3876
4200
  }
3877
4201
 
3878
- getEquivalentSignatureVariables() {
4202
+ getEquivalentSignatureVariables(): Record<string, string | string[]> {
3879
4203
  const scopeNode = this.scopeNodes[this.scopeTreeManager.getRootName()];
3880
4204
 
3881
- const equivalentSignatureVariables: Record<string, string> = {};
4205
+ const equivalentSignatureVariables: Record<string, string | string[]> = {};
4206
+
4207
+ // Helper to add equivalencies - accumulates into array if multiple values for same key
4208
+ // This is critical for OR expressions like `x = a || b` where x should map to both a and b
4209
+ const addEquivalency = (key: string, value: string) => {
4210
+ const existing = equivalentSignatureVariables[key];
4211
+ if (existing === undefined) {
4212
+ // First value - store as string
4213
+ equivalentSignatureVariables[key] = value;
4214
+ } else if (typeof existing === 'string') {
4215
+ if (existing !== value) {
4216
+ // Second different value - convert to array
4217
+ equivalentSignatureVariables[key] = [existing, value];
4218
+ }
4219
+ // Same value - no change needed
4220
+ } else {
4221
+ // Already an array - add if not already present
4222
+ if (!existing.includes(value)) {
4223
+ existing.push(value);
4224
+ }
4225
+ }
4226
+ };
4227
+
3882
4228
  for (const [path, equivalentValues] of Object.entries(
3883
4229
  scopeNode.equivalencies,
3884
4230
  )) {
@@ -3887,7 +4233,7 @@ export class ScopeDataStructure {
3887
4233
  // Maps local variable names to their signature paths
3888
4234
  // e.g., "propValue" -> "signature[0].prop"
3889
4235
  if (path.startsWith('signature[')) {
3890
- equivalentSignatureVariables[equivalentValue.schemaPath] = path;
4236
+ addEquivalency(equivalentValue.schemaPath, path);
3891
4237
  }
3892
4238
 
3893
4239
  // Case 2: Hook variable equivalencies (new behavior)
@@ -3921,7 +4267,7 @@ export class ScopeDataStructure {
3921
4267
  hookCallPath = dbEntry.sourceCandidates[0].schemaPath;
3922
4268
  }
3923
4269
  }
3924
- equivalentSignatureVariables[path] = hookCallPath;
4270
+ addEquivalency(path, hookCallPath);
3925
4271
  }
3926
4272
  }
3927
4273
 
@@ -3935,10 +4281,8 @@ export class ScopeDataStructure {
3935
4281
  !equivalentValue.schemaPath.startsWith('signature[') && // not a signature path
3936
4282
  !equivalentValue.schemaPath.endsWith('.functionCallReturnValue') // not already handled above
3937
4283
  ) {
3938
- // Only add if we haven't already captured this variable in Case 1 or 2
3939
- if (!(path in equivalentSignatureVariables)) {
3940
- equivalentSignatureVariables[path] = equivalentValue.schemaPath;
3941
- }
4284
+ // Add equivalency (will accumulate if multiple values for OR expressions)
4285
+ addEquivalency(path, equivalentValue.schemaPath);
3942
4286
  }
3943
4287
 
3944
4288
  // Case 4: Child component prop mappings (Fix 22)
@@ -3951,7 +4295,7 @@ export class ScopeDataStructure {
3951
4295
  path.includes('().signature[') &&
3952
4296
  !equivalentValue.schemaPath.includes('()') // schemaPath is a simple variable, not a function call
3953
4297
  ) {
3954
- equivalentSignatureVariables[path] = equivalentValue.schemaPath;
4298
+ addEquivalency(path, equivalentValue.schemaPath);
3955
4299
  }
3956
4300
 
3957
4301
  // Case 5: Destructured function parameters (Fix 25)
@@ -3966,7 +4310,7 @@ export class ScopeDataStructure {
3966
4310
  !path.includes('.') && // path is a simple identifier (destructured prop name)
3967
4311
  equivalentValue.schemaPath.startsWith('signature[') // schemaPath IS a signature path
3968
4312
  ) {
3969
- equivalentSignatureVariables[path] = equivalentValue.schemaPath;
4313
+ addEquivalency(path, equivalentValue.schemaPath);
3970
4314
  }
3971
4315
 
3972
4316
  // Case 7: Method calls on variables that result in .functionCallReturnValue (Fix 33)
@@ -3980,8 +4324,7 @@ export class ScopeDataStructure {
3980
4324
  if (
3981
4325
  !path.includes('.') && // path is a simple identifier
3982
4326
  equivalentValue.schemaPath.endsWith('.functionCallReturnValue') && // ends with function return
3983
- equivalentValue.schemaPath.includes('.') && // has property access (method call)
3984
- !(path in equivalentSignatureVariables) // not already captured
4327
+ equivalentValue.schemaPath.includes('.') // has property access (method call)
3985
4328
  ) {
3986
4329
  // Check if this looks like a method call on a variable (not a hook call)
3987
4330
  // Hook calls look like: hookName() or hookName<T>()
@@ -3995,7 +4338,7 @@ export class ScopeDataStructure {
3995
4338
  const parenPos = hookCallPath.indexOf('(');
3996
4339
  if (dotBeforeParen !== -1 && dotBeforeParen < parenPos) {
3997
4340
  // This is a method call like "splat.split('/')", not a hook call
3998
- equivalentSignatureVariables[path] = equivalentValue.schemaPath;
4341
+ addEquivalency(path, equivalentValue.schemaPath);
3999
4342
  }
4000
4343
  }
4001
4344
  }
@@ -4026,8 +4369,9 @@ export class ScopeDataStructure {
4026
4369
  !equivalentValue.schemaPath.includes('()') // schemaPath is a simple variable
4027
4370
  ) {
4028
4371
  // Only add if not already present from the root scope
4372
+ // Root scope values take precedence over child scope values
4029
4373
  if (!(path in equivalentSignatureVariables)) {
4030
- equivalentSignatureVariables[path] = equivalentValue.schemaPath;
4374
+ addEquivalency(path, equivalentValue.schemaPath);
4031
4375
  }
4032
4376
  }
4033
4377
  }
@@ -4039,12 +4383,83 @@ export class ScopeDataStructure {
4039
4383
  // We need multiple passes because resolutions can depend on each other
4040
4384
  const maxIterations = 5; // Prevent infinite loops
4041
4385
 
4386
+ // Helper function to resolve a single source path using equivalencies
4387
+ const resolveSourcePath = (
4388
+ sourcePath: string,
4389
+ equivMap: Record<string, string | string[]>,
4390
+ ): string | null => {
4391
+ // Extract base variable from the path
4392
+ const dotIndex = sourcePath.indexOf('.');
4393
+ const bracketIndex = sourcePath.indexOf('[');
4394
+
4395
+ let baseVar: string;
4396
+ let rest: string;
4397
+
4398
+ if (dotIndex === -1 && bracketIndex === -1) {
4399
+ baseVar = sourcePath;
4400
+ rest = '';
4401
+ } else if (dotIndex === -1) {
4402
+ baseVar = sourcePath.slice(0, bracketIndex);
4403
+ rest = sourcePath.slice(bracketIndex);
4404
+ } else if (bracketIndex === -1) {
4405
+ baseVar = sourcePath.slice(0, dotIndex);
4406
+ rest = sourcePath.slice(dotIndex);
4407
+ } else {
4408
+ const firstIndex = Math.min(dotIndex, bracketIndex);
4409
+ baseVar = sourcePath.slice(0, firstIndex);
4410
+ rest = sourcePath.slice(firstIndex);
4411
+ }
4412
+
4413
+ // Look up the base variable in equivalencies
4414
+ if (baseVar in equivMap && equivMap[baseVar] !== sourcePath) {
4415
+ const baseResolved = equivMap[baseVar];
4416
+ // Skip if baseResolved is an array (handle later)
4417
+ if (Array.isArray(baseResolved)) return null;
4418
+ // If it resolves to a signature path, build the full resolved path
4419
+ if (
4420
+ baseResolved.startsWith('signature[') ||
4421
+ baseResolved.includes('()')
4422
+ ) {
4423
+ if (baseResolved.endsWith('()')) {
4424
+ return baseResolved + '.functionCallReturnValue' + rest;
4425
+ }
4426
+ return baseResolved + rest;
4427
+ }
4428
+ }
4429
+ return null;
4430
+ };
4431
+
4042
4432
  for (let iteration = 0; iteration < maxIterations; iteration++) {
4043
4433
  let changed = false;
4044
4434
 
4045
- for (const [varName, sourcePath] of Object.entries(
4435
+ for (const [varName, sourcePathOrArray] of Object.entries(
4046
4436
  equivalentSignatureVariables,
4047
4437
  )) {
4438
+ // Handle arrays (OR expressions) by resolving each element
4439
+ if (Array.isArray(sourcePathOrArray)) {
4440
+ const resolvedArray: string[] = [];
4441
+ let arrayChanged = false;
4442
+ for (const sourcePath of sourcePathOrArray) {
4443
+ // Try to resolve this path using transitive resolution
4444
+ const resolved = resolveSourcePath(
4445
+ sourcePath,
4446
+ equivalentSignatureVariables,
4447
+ );
4448
+ if (resolved && resolved !== sourcePath) {
4449
+ resolvedArray.push(resolved);
4450
+ arrayChanged = true;
4451
+ } else {
4452
+ resolvedArray.push(sourcePath);
4453
+ }
4454
+ }
4455
+ if (arrayChanged) {
4456
+ equivalentSignatureVariables[varName] = resolvedArray;
4457
+ changed = true;
4458
+ }
4459
+ continue;
4460
+ }
4461
+ const sourcePath = sourcePathOrArray;
4462
+
4048
4463
  // Skip if already fully resolved (contains function call syntax)
4049
4464
  // BUT first check for computed value patterns that need resolution (Fix 28)
4050
4465
  // AND method call patterns that need base variable resolution (Fix 33)
@@ -4106,6 +4521,8 @@ export class ScopeDataStructure {
4106
4521
  baseVar !== varName
4107
4522
  ) {
4108
4523
  const baseResolved = equivalentSignatureVariables[baseVar];
4524
+ // Skip if baseResolved is an array (OR expression)
4525
+ if (Array.isArray(baseResolved)) continue;
4109
4526
  // Only resolve if the base resolved to something useful (contains () or .)
4110
4527
  if (baseResolved.includes('()') || baseResolved.includes('.')) {
4111
4528
  const newPath = baseResolved + rest;
@@ -4170,7 +4587,12 @@ export class ScopeDataStructure {
4170
4587
  }
4171
4588
 
4172
4589
  if (baseVar in equivalentSignatureVariables && baseVar !== varName) {
4173
- const baseResolved = equivalentSignatureVariables[baseVar];
4590
+ // Handle array case (OR expressions) - use first element
4591
+ const rawBaseResolved = equivalentSignatureVariables[baseVar];
4592
+ const baseResolved = Array.isArray(rawBaseResolved)
4593
+ ? rawBaseResolved[0]
4594
+ : rawBaseResolved;
4595
+ if (!baseResolved) continue;
4174
4596
  // If the base resolves to a hook call, add .functionCallReturnValue
4175
4597
  if (baseResolved.endsWith('()')) {
4176
4598
  const newPath = baseResolved + '.functionCallReturnValue' + rest;
@@ -4390,7 +4812,7 @@ export class ScopeDataStructure {
4390
4812
  path: string;
4391
4813
  conditionType: 'truthiness' | 'comparison' | 'switch';
4392
4814
  comparedValues?: string[];
4393
- location: 'if' | 'ternary' | 'logical-and' | 'switch';
4815
+ location: 'if' | 'ternary' | 'logical-and' | 'switch' | 'unconditional';
4394
4816
  }>
4395
4817
  >,
4396
4818
  ): void {