@codeyam/codeyam-cli 0.1.21 → 0.1.22

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 (95) hide show
  1. package/analyzer-template/.build-info.json +7 -7
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +31 -8
  4. package/analyzer-template/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.ts +10 -3
  5. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +16 -6
  6. package/analyzer-template/packages/analyze/index.ts +4 -1
  7. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +28 -2
  8. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +5 -36
  9. package/analyzer-template/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.ts +21 -0
  10. package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +82 -10
  11. package/analyzer-template/packages/analyze/src/lib/files/analyzeNextRoute.ts +8 -3
  12. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +235 -58
  13. package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +170 -26
  14. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js +63 -0
  15. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js.map +1 -0
  16. package/codeyam-cli/src/commands/editor.js +412 -78
  17. package/codeyam-cli/src/commands/editor.js.map +1 -1
  18. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +354 -1
  19. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  20. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +11 -3
  21. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
  22. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +33 -1
  23. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  24. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
  25. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
  26. package/codeyam-cli/src/utils/__tests__/testRunner.test.js +217 -0
  27. package/codeyam-cli/src/utils/__tests__/testRunner.test.js.map +1 -0
  28. package/codeyam-cli/src/utils/analysisRunner.js +28 -1
  29. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  30. package/codeyam-cli/src/utils/analyzer.js +4 -2
  31. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  32. package/codeyam-cli/src/utils/editorAudit.js +98 -3
  33. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  34. package/codeyam-cli/src/utils/editorPreview.js +5 -3
  35. package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
  36. package/codeyam-cli/src/utils/entityChangeStatus.server.js +16 -0
  37. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
  38. package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
  39. package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
  40. package/codeyam-cli/src/utils/queue/job.js +20 -2
  41. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  42. package/codeyam-cli/src/utils/testRunner.js +199 -1
  43. package/codeyam-cli/src/utils/testRunner.js.map +1 -1
  44. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +35 -0
  45. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
  46. package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-CQENLSrF.js +36 -0
  47. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-Coe5NhbS.js +1 -0
  48. package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CJzc4vOH.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
  49. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-aIHKLB-m.js +96 -0
  50. package/codeyam-cli/src/webserver/build/client/assets/{editorPreview-NTuLi4Xg.js → editorPreview-CluPkvXJ.js} +6 -6
  51. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-Blfy9UlN.js → entity._sha._-ByHz6rAQ.js} +13 -12
  52. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-BA5L8bU-.js → entity._sha.scenarios._scenarioId.dev-CmLO432x.js} +1 -1
  53. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-D4dmRgvO.js → entity._sha.scenarios._scenarioId.fullscreen-Bz9sCUF_.js} +1 -1
  54. package/codeyam-cli/src/webserver/build/client/assets/globals-oyPmV37k.css +1 -0
  55. package/codeyam-cli/src/webserver/build/client/assets/{manifest-5025e428.js → manifest-bcbb3d49.js} +1 -1
  56. package/codeyam-cli/src/webserver/build/client/assets/{root-BCx1S8Z3.js → root-D2_tktnk.js} +6 -6
  57. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-DjF-soOH.js +16 -0
  58. package/codeyam-cli/src/webserver/build/server/assets/{index-C91yWWCI.js → index-nAvHGWbz.js} +1 -1
  59. package/codeyam-cli/src/webserver/build/server/assets/{init-Dkas-RUS.js → init-XhpIt-OT.js} +1 -1
  60. package/codeyam-cli/src/webserver/build/server/assets/server-build-DVwiibFu.js +644 -0
  61. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  62. package/codeyam-cli/src/webserver/build-info.json +5 -5
  63. package/codeyam-cli/src/webserver/idleDetector.js +15 -0
  64. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
  65. package/codeyam-cli/src/webserver/terminalServer.js +8 -2
  66. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  67. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +2 -2
  68. package/package.json +1 -1
  69. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +23 -9
  70. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  71. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
  72. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
  73. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
  74. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
  75. package/packages/analyze/index.js +1 -1
  76. package/packages/analyze/index.js.map +1 -1
  77. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
  78. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
  79. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
  80. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  81. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
  82. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
  83. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
  84. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
  85. package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
  86. package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
  87. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +116 -28
  88. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  89. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +139 -24
  90. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  91. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DODLxLcw.js +0 -1
  92. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Dx-h1rJK.js +0 -130
  93. package/codeyam-cli/src/webserver/build/client/assets/globals-BrPXT1iR.css +0 -1
  94. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-C1kjC9UJ.js +0 -13
  95. package/codeyam-cli/src/webserver/build/server/assets/server-build-pulXLTrG.js +0 -640
@@ -41,6 +41,19 @@ function getTypeParameter(functionName: string): string | null {
41
41
  return null;
42
42
  }
43
43
 
44
+ /**
45
+ * Check if a schema path contains a Set/Map collection method call.
46
+ * Paths like `.has(articleId)`, `.delete(articleId)`, `.add(articleId)` represent
47
+ * membership checks on Sets/Maps, not meaningful data flow for schema generation.
48
+ * These create massive combinatorial explosions when every filter field (filterRatings,
49
+ * filterPublications, filterAuthors, etc.) × every method (has, delete, add) gets
50
+ * tracked as a separate equivalency.
51
+ */
52
+ const COLLECTION_METHOD_PATTERN = /\.(?:has|delete|add|clear|get|set)\(/;
53
+ function isCollectionMethodPath(path: string): boolean {
54
+ return COLLECTION_METHOD_PATTERN.test(path);
55
+ }
56
+
44
57
  // Primitive types that should not have child paths
45
58
  const PRIMITIVE_TYPES = new Set([
46
59
  'number',
@@ -142,12 +155,26 @@ function bestValueFromOptions(options: Array<string | undefined>) {
142
155
  return options[0] ?? 'unknown';
143
156
  }
144
157
 
158
+ /** Timeout (ms) for the merge operation. Throws DataStructureTimeoutError if exceeded.
159
+ * All successful merges complete in <300ms. Anything exceeding 2s is pathological. */
160
+ const MERGE_TIMEOUT_MS = 2_000;
161
+
162
+ export class DataStructureTimeoutError extends Error {
163
+ constructor(entityName: string, elapsedMs: number) {
164
+ super(
165
+ `Data structure merge timed out for ${entityName} after ${Math.round(elapsedMs / 1000)}s (limit: ${MERGE_TIMEOUT_MS / 1000}s)`,
166
+ );
167
+ this.name = 'DataStructureTimeoutError';
168
+ }
169
+ }
170
+
145
171
  export default function mergeInDependentDataStructure({
146
172
  importedExports,
147
173
  dependentAnalyses,
148
174
  rootScopeName,
149
175
  dataStructure,
150
176
  dependencySchemas,
177
+ timeoutMs = MERGE_TIMEOUT_MS,
151
178
  }: {
152
179
  importedExports: Pick<
153
180
  Entity['metadata']['importedExports'][0],
@@ -169,7 +196,23 @@ export default function mergeInDependentDataStructure({
169
196
  [name: string]: DataStructureInfo;
170
197
  };
171
198
  };
199
+ /** Override the default timeout (ms). Set to 0 to disable. */
200
+ timeoutMs?: number;
172
201
  }) {
202
+ const mergeStartTime = Date.now();
203
+ const mergeDeadline = timeoutMs > 0 ? mergeStartTime + timeoutMs : 0;
204
+
205
+ /** Call in hot loops. Throws DataStructureTimeoutError if deadline exceeded.
206
+ * Date.now() is ~20ns — negligible vs the ms-scale string ops in each iteration. */
207
+ const checkDeadline = () => {
208
+ if (!mergeDeadline) return;
209
+ if (Date.now() > mergeDeadline) {
210
+ throw new DataStructureTimeoutError(
211
+ rootScopeName,
212
+ Date.now() - mergeStartTime,
213
+ );
214
+ }
215
+ };
173
216
  const mergedDataStructure: Omit<
174
217
  DataStructure,
175
218
  'equivalentSignatureVariables'
@@ -310,7 +353,17 @@ export default function mergeInDependentDataStructure({
310
353
  );
311
354
  };
312
355
 
356
+ // Cache translatePath results — the same path is often translated multiple times
357
+ // (once per equivalency entry that references it). Avoids redundant
358
+ // splitOutsideParenthesesAndArrays calls on long paths.
359
+ const translatePathCache = new Map<string, string>();
360
+
313
361
  const translatePath = (path: string, dependencyName: string) => {
362
+ const cacheKey = `${dependencyName}\0${path}`;
363
+ const cached = translatePathCache.get(cacheKey);
364
+ if (cached !== undefined) return cached;
365
+
366
+ let result = path;
314
367
  if (path.startsWith(dependencyName)) {
315
368
  const pathParts = splitOutsideParenthesesAndArrays(path);
316
369
  if (pathParts.length > 1) {
@@ -318,33 +371,26 @@ export default function mergeInDependentDataStructure({
318
371
  // Check if this function has multiple DIFFERENT type parameters.
319
372
  // If so, DON'T normalize to returnValue - keep the full path to avoid
320
373
  // merging different type-parameterized variants together.
321
- // e.g., useFetcher<{ data: UserData }>().functionCallReturnValue.data
322
- // should NOT be merged with useFetcher<{ data: ConfigData }>().functionCallReturnValue.data
323
374
  const baseName = cleanFunctionName(pathParts[0]);
324
- if (functionsWithMultipleTypeParams.has(baseName)) {
325
- return path; // Keep the original path with type parameters
375
+ if (!functionsWithMultipleTypeParams.has(baseName)) {
376
+ // functionCallReturnValue immediately follows - normalize to returnValue
377
+ result = joinParenthesesAndArrays([
378
+ 'returnValue',
379
+ ...pathParts.slice(2),
380
+ ]);
326
381
  }
327
- // functionCallReturnValue immediately follows - normalize to returnValue
328
- // e.g., useAuth().functionCallReturnValue.user -> returnValue.user
329
- return joinParenthesesAndArrays([
330
- 'returnValue',
331
- ...pathParts.slice(2),
332
- ]);
333
382
  } else if (
334
383
  pathParts[0].endsWith(')') &&
335
384
  pathParts[1].startsWith('signature[')
336
385
  ) {
337
- // Hook-style with signature access (e.g., BranchChangesTab().signature[0]...)
338
- // Strip the function name for signature equivalency matching
339
- return joinParenthesesAndArrays(pathParts.slice(1));
386
+ // Hook-style with signature access
387
+ result = joinParenthesesAndArrays(pathParts.slice(1));
340
388
  }
341
- // For all other cases (object-style APIs like getSupabase().auth and
342
- // direct object references like supabase.from), preserve the path as-is.
343
- // The prefix must be kept for proper schema lookups in constructMockCode
344
- // and gatherDataForMocks.
345
389
  }
346
390
  }
347
- return path;
391
+
392
+ translatePathCache.set(cacheKey, result);
393
+ return result;
348
394
  };
349
395
 
350
396
  const gatherAllEquivalentSchemaPaths = (
@@ -358,6 +404,7 @@ export default function mergeInDependentDataStructure({
358
404
  'signatureSchema' | 'returnValueSchema'
359
405
  >,
360
406
  ) => {
407
+ checkDeadline();
361
408
  if (!sourceAndUsageEquivalencies) return;
362
409
 
363
410
  // Pre-computed normalized schema index cache.
@@ -380,6 +427,7 @@ export default function mergeInDependentDataStructure({
380
427
  if (cached) return cached;
381
428
  const byFirstPart = new Map<string, NormalizedEntry[]>();
382
429
  for (const path in schema) {
430
+ checkDeadline();
383
431
  let parts = splitOutsideParenthesesAndArrays(path);
384
432
  if (parts[0].startsWith(functionName)) {
385
433
  const baseName = cleanFunctionName(parts[0]);
@@ -441,6 +489,7 @@ export default function mergeInDependentDataStructure({
441
489
  // Use espIndex Map for O(1) lookup instead of O(E) linear search.
442
490
  // Falls back to linear search only when Map hit has a signature index conflict.
443
491
  for (const pathInfo of allPaths) {
492
+ checkDeadline();
444
493
  if (equivalentSchemaPathsEntry) break;
445
494
  const candidate = espIndex.get(
446
495
  espIndexKey(pathInfo.path, pathInfo.functionName),
@@ -572,20 +621,43 @@ export default function mergeInDependentDataStructure({
572
621
  for (const equivalencies of allEquivalencies) {
573
622
  const schemaPathEntries = Object.entries(equivalencies);
574
623
  for (const [schemaPath, usages] of schemaPathEntries) {
624
+ checkDeadline();
625
+
626
+ // Skip equivalency entries whose source path is a Set/Map membership operation.
627
+ // Patterns like `.has(articleId)`, `.delete(articleId)`, `.add(articleId)` on
628
+ // Sets/Maps represent membership checks, not meaningful data flow for schema generation.
629
+ // In the Margo LibraryPage case, these account for 74% of all equivalency targets
630
+ // (19,444 of 26,340) and cause a combinatorial explosion in the merge.
631
+ if (isCollectionMethodPath(schemaPath)) continue;
632
+
575
633
  // First, check if the raw schemaPath starts with a function call to a dependency.
576
634
  // If so, use that dependency name for translation (so translatePath can strip the prefix).
577
635
  const extractedFuncName = extractFunctionNameFromPath(schemaPath);
578
636
  const effectiveFunctionName = extractedFuncName || functionName;
579
637
  const translatedPath = translatePath(schemaPath, effectiveFunctionName);
580
638
 
581
- const allPaths: { path: string; functionName?: string }[] = [
639
+ const allPathsRaw: { path: string; functionName?: string }[] = [
582
640
  { path: translatedPath, functionName: effectiveFunctionName },
583
- ...usages.map((u) => ({
584
- path: translatePath(u.schemaPath, u.scopeNodeName),
585
- functionName: u.scopeNodeName,
586
- })),
641
+ ...usages
642
+ .filter((u) => !isCollectionMethodPath(u.schemaPath))
643
+ .map((u) => ({
644
+ path: translatePath(u.schemaPath, u.scopeNodeName),
645
+ functionName: u.scopeNodeName,
646
+ })),
587
647
  ].filter((pathInfo) => !pathInfo.path.includes('.map('));
588
648
 
649
+ // Deduplicate by translated path + function name.
650
+ // Multiple call variants (e.g., loadView(viewKey(null,null)) vs loadView(viewKey(newTag,newCol)))
651
+ // translate to the same path after stripping arguments. Processing duplicates
652
+ // creates O(n²) work in the schema matching loops below.
653
+ const seenPathKeys = new Set<string>();
654
+ const allPaths = allPathsRaw.filter((p) => {
655
+ const key = `${p.functionName ?? ''}::${p.path}`;
656
+ if (seenPathKeys.has(key)) return false;
657
+ seenPathKeys.add(key);
658
+ return true;
659
+ });
660
+
589
661
  // Fix 38: Derive base paths from property access paths.
590
662
  // When we have equivalent paths like:
591
663
  // Parent: signature[0].scenarios[].name
@@ -617,6 +689,7 @@ export default function mergeInDependentDataStructure({
617
689
 
618
690
  // For each child path, find its equivalent parent path and derive bases
619
691
  for (const childPathInfo of childPaths) {
692
+ checkDeadline();
620
693
  const childParts = splitOutsideParenthesesAndArrays(
621
694
  childPathInfo.path,
622
695
  );
@@ -743,6 +816,7 @@ export default function mergeInDependentDataStructure({
743
816
  });
744
817
  }
745
818
  for (const equivalentRoot of entry.equivalentRoots) {
819
+ checkDeadline();
746
820
  const dataStructures =
747
821
  equivalentRoot.function &&
748
822
  equivalentRoot.function.name !== rootScopeName
@@ -803,6 +877,7 @@ export default function mergeInDependentDataStructure({
803
877
  path: schemaPath,
804
878
  parts: schemaPathParts,
805
879
  } of candidates) {
880
+ checkDeadline();
806
881
  if (schemaPathParts.length < pathParts.length) continue;
807
882
 
808
883
  // Check if all path parts match (allowing function call variants)
@@ -910,6 +985,7 @@ export default function mergeInDependentDataStructure({
910
985
  // where we want both X().functionCallReturnValue and Y().functionCallReturnValue as bases
911
986
  const allBasePaths = new Set<string>();
912
987
  for (const path of Object.keys(dataStructure.returnValueSchema)) {
988
+ checkDeadline();
913
989
  const parts = splitOutsideParenthesesAndArrays(path);
914
990
  // Find all positions of functionCallReturnValue and create base paths for each
915
991
  for (let i = 0; i < parts.length; i++) {
@@ -944,6 +1020,7 @@ export default function mergeInDependentDataStructure({
944
1020
 
945
1021
  const basePathParts = splitOutsideParenthesesAndArrays(basePath);
946
1022
  for (const schemaPath in dataStructure.returnValueSchema) {
1023
+ checkDeadline();
947
1024
  const schemaPathParts = splitOutsideParenthesesAndArrays(schemaPath);
948
1025
  if (schemaPathParts.length < basePathParts.length) continue;
949
1026
 
@@ -984,6 +1061,7 @@ export default function mergeInDependentDataStructure({
984
1061
  // We do this before the main merge to ensure the connection happens regardless
985
1062
  // of processing order.
986
1063
  for (const esp of equivalentSchemaPaths) {
1064
+ checkDeadline();
987
1065
  for (const root of esp.equivalentRoots) {
988
1066
  if (root.schemaRootPath.endsWith('[]')) {
989
1067
  // Find a matching parent entry with the base array path
@@ -1004,6 +1082,7 @@ export default function mergeInDependentDataStructure({
1004
1082
  for (const [postfixPath, postfixValue] of Object.entries(
1005
1083
  esp.equivalentPostfixes,
1006
1084
  )) {
1085
+ checkDeadline();
1007
1086
  const transformedPostfix = joinParenthesesAndArrays(
1008
1087
  ['[]', postfixPath].filter(Boolean),
1009
1088
  );
@@ -1093,6 +1172,7 @@ export default function mergeInDependentDataStructure({
1093
1172
  );
1094
1173
 
1095
1174
  for (const esp of sortedEquivalentSchemaPaths) {
1175
+ checkDeadline();
1096
1176
  if (esp.equivalentRoots.length === 0) continue;
1097
1177
  let bestCandidateLength: number | undefined;
1098
1178
  let bestCandidate: (typeof equivalentSchemaPaths)[0] | undefined;
@@ -1174,6 +1254,7 @@ export default function mergeInDependentDataStructure({
1174
1254
  // dependencySchemas contains usage information (how dependencies are called),
1175
1255
  // not internal implementation, so we want this for mocked dependencies too
1176
1256
  for (const dependency of importedExports) {
1257
+ checkDeadline();
1177
1258
  const dependentDataStructure =
1178
1259
  dependencySchemas?.[dependency.filePath]?.[dependency.name];
1179
1260
  if (!dependentDataStructure) continue;
@@ -1198,18 +1279,36 @@ export default function mergeInDependentDataStructure({
1198
1279
  }
1199
1280
  }
1200
1281
 
1282
+ const gatherElapsed = Date.now() - mergeStartTime;
1283
+
1201
1284
  equivalentSchemaPaths = mergeAllEquivalentSchemaPaths();
1202
1285
 
1286
+ const mergeEspElapsed = Date.now() - mergeStartTime;
1287
+
1203
1288
  // Collect schemas that need cleaning — batch the calls for the end instead of
1204
1289
  // calling cleanSchema inside the inner root loop (which was O(roots * schemaSize)).
1205
1290
  const schemasToClean = new Set<{ [key: string]: string }>();
1206
1291
 
1207
1292
  for (const esp of equivalentSchemaPaths) {
1293
+ checkDeadline();
1208
1294
  // Pre-compute which postfixes have children to avoid O(n²) lookups in the inner loop.
1209
1295
  // A postfix "has children" if there are other postfixes that extend it.
1210
1296
  const postfixesWithChildren = new Set<string>();
1211
1297
  const postfixKeys = Object.keys(esp.equivalentPostfixes);
1212
1298
 
1299
+ // Pre-parse ALL postfix paths once. These parsed parts are reused in:
1300
+ // 1. The children detection loop below
1301
+ // 2. The inner postfix application loop (lines that split postfixPath and equivalentRoot.postfix)
1302
+ // This eliminates thousands of redundant splitOutsideParenthesesAndArrays calls.
1303
+ const postfixPartsCache = new Map<string, string[]>();
1304
+ for (const postfixPath of postfixKeys) {
1305
+ if (!postfixPath) continue;
1306
+ postfixPartsCache.set(
1307
+ postfixPath,
1308
+ splitOutsideParenthesesAndArrays(postfixPath),
1309
+ );
1310
+ }
1311
+
1213
1312
  // Check for empty postfix having children (any other postfixes exist)
1214
1313
  if (postfixKeys.length > 1 && '' in esp.equivalentPostfixes) {
1215
1314
  postfixesWithChildren.add('');
@@ -1221,7 +1320,7 @@ export default function mergeInDependentDataStructure({
1221
1320
  const postfixPrefixSet = new Set<string>();
1222
1321
  for (const postfixPath of postfixKeys) {
1223
1322
  if (!postfixPath) continue;
1224
- const parts = splitOutsideParenthesesAndArrays(postfixPath);
1323
+ const parts = postfixPartsCache.get(postfixPath)!;
1225
1324
  for (let i = 1; i < parts.length; i++) {
1226
1325
  postfixPrefixSet.add(joinParenthesesAndArrays(parts.slice(0, i)));
1227
1326
  }
@@ -1242,7 +1341,13 @@ export default function mergeInDependentDataStructure({
1242
1341
  return true;
1243
1342
  });
1244
1343
 
1344
+ // Cap schema size to prevent combinatorial explosion.
1345
+ // Successful merges produce <3K ret keys. Beyond 5K, further postfixes
1346
+ // add noise but no useful data — they're cross-products of unrelated equivalencies.
1347
+ const SCHEMA_KEY_CAP = 5_000;
1348
+
1245
1349
  for (const equivalentRoot of uniqueRoots) {
1350
+ checkDeadline();
1246
1351
  let merged:
1247
1352
  | {
1248
1353
  signatureSchema: { [key: string]: string };
@@ -1262,9 +1367,13 @@ export default function mergeInDependentDataStructure({
1262
1367
  ? merged.signatureSchema
1263
1368
  : merged.returnValueSchema;
1264
1369
 
1370
+ // Skip if this schema has already grown past the cap
1371
+ if (Object.keys(schema).length > SCHEMA_KEY_CAP) continue;
1372
+
1265
1373
  for (const [postfixPath, postfixValue] of Object.entries(
1266
1374
  esp.equivalentPostfixes,
1267
1375
  )) {
1376
+ checkDeadline();
1268
1377
  let relevantPostfix = postfixPath;
1269
1378
  if (equivalentRoot.postfix) {
1270
1379
  // Check if postfixPath starts with equivalentRoot.postfix at a path boundary.
@@ -1284,10 +1393,18 @@ export default function mergeInDependentDataStructure({
1284
1393
  }
1285
1394
 
1286
1395
  const postFixPathParts =
1396
+ postfixPartsCache.get(postfixPath) ??
1287
1397
  splitOutsideParenthesesAndArrays(postfixPath);
1288
- const equivalentRootPostFixParts = splitOutsideParenthesesAndArrays(
1398
+ // Cache equivalentRoot.postfix parts — same root reused across all postfixes
1399
+ if (!postfixPartsCache.has(equivalentRoot.postfix)) {
1400
+ postfixPartsCache.set(
1401
+ equivalentRoot.postfix,
1402
+ splitOutsideParenthesesAndArrays(equivalentRoot.postfix),
1403
+ );
1404
+ }
1405
+ const equivalentRootPostFixParts = postfixPartsCache.get(
1289
1406
  equivalentRoot.postfix,
1290
- );
1407
+ )!;
1291
1408
  relevantPostfix = joinParenthesesAndArrays(
1292
1409
  postFixPathParts.slice(equivalentRootPostFixParts.length),
1293
1410
  );
@@ -1385,11 +1502,15 @@ export default function mergeInDependentDataStructure({
1385
1502
  }
1386
1503
  }
1387
1504
 
1505
+ const postfixElapsed = Date.now() - mergeStartTime;
1506
+
1388
1507
  // Batch-clean all modified schemas once (instead of once per root per ESP entry)
1389
1508
  for (const schema of schemasToClean) {
1390
1509
  cleanSchema(schema, { stage: 'afterMergePostfix' });
1391
1510
  }
1392
1511
 
1512
+ const cleanElapsed = Date.now() - mergeStartTime;
1513
+
1393
1514
  // Propagate equivalency-derived attributes to generic function call variants.
1394
1515
  // When attributes are traced via equivalencies (e.g., fileComparisons from buildDataMap.signature[2]),
1395
1516
  // they get written to non-generic paths (returnValue.data.x or funcName().functionCallReturnValue.data.x).
@@ -1415,6 +1536,7 @@ export default function mergeInDependentDataStructure({
1415
1536
  );
1416
1537
 
1417
1538
  for (const path in schemaToSearchForGenericVariants) {
1539
+ checkDeadline();
1418
1540
  const match = path.match(genericRegex);
1419
1541
  if (match) {
1420
1542
  genericVariants.add(match[0]);
@@ -1428,6 +1550,7 @@ export default function mergeInDependentDataStructure({
1428
1550
  const pathsToAdd: [string, string][] = [];
1429
1551
 
1430
1552
  for (const path in returnValueSchema) {
1553
+ checkDeadline();
1431
1554
  const value = returnValueSchema[path];
1432
1555
 
1433
1556
  // Handle returnValue. paths
@@ -1481,6 +1604,7 @@ export default function mergeInDependentDataStructure({
1481
1604
  // This includes both returnValue. (dot) and returnValue[ (array) paths.
1482
1605
  const pathsToNormalize: [string, string][] = [];
1483
1606
  for (const path in depSchema.returnValueSchema) {
1607
+ checkDeadline();
1484
1608
  if (
1485
1609
  path === 'returnValue' ||
1486
1610
  path.startsWith('returnValue.') ||
@@ -1521,6 +1645,7 @@ export default function mergeInDependentDataStructure({
1521
1645
 
1522
1646
  // Now copy paths from the source schema (dependencySchemas)
1523
1647
  for (const path in srcSchema.returnValueSchema) {
1648
+ checkDeadline();
1524
1649
  const value = srcSchema.returnValueSchema[path];
1525
1650
 
1526
1651
  // Normalize paths starting with 'returnValue' to use the standard format:
@@ -1714,9 +1839,11 @@ export default function mergeInDependentDataStructure({
1714
1839
  if (!existingSchema) {
1715
1840
  const depSchema = findOrCreateDependentSchemas({ filePath, name });
1716
1841
  for (const path in srcSchema.returnValueSchema) {
1842
+ checkDeadline();
1717
1843
  depSchema.returnValueSchema[path] = srcSchema.returnValueSchema[path];
1718
1844
  }
1719
1845
  for (const path in srcSchema.signatureSchema) {
1846
+ checkDeadline();
1720
1847
  depSchema.signatureSchema[path] = srcSchema.signatureSchema[path];
1721
1848
  }
1722
1849
 
@@ -1746,6 +1873,7 @@ export default function mergeInDependentDataStructure({
1746
1873
 
1747
1874
  if (childSignatureSchema) {
1748
1875
  for (const path in depSchema.signatureSchema) {
1876
+ checkDeadline();
1749
1877
  const parentType = depSchema.signatureSchema[path];
1750
1878
  const childType = childSignatureSchema[path];
1751
1879
 
@@ -1794,6 +1922,7 @@ export default function mergeInDependentDataStructure({
1794
1922
 
1795
1923
  // Copy only paths that belong to this variant
1796
1924
  for (const path in srcSchema.returnValueSchema) {
1925
+ checkDeadline();
1797
1926
  if (path.startsWith(variant)) {
1798
1927
  variantSchema.returnValueSchema[path] =
1799
1928
  srcSchema.returnValueSchema[path];
@@ -1816,6 +1945,7 @@ export default function mergeInDependentDataStructure({
1816
1945
  // EXCEPT: Skip mocked dependencies - we don't want their internal implementation details.
1817
1946
  for (const filePath in dependentAnalyses) {
1818
1947
  for (const name in dependentAnalyses[filePath]) {
1948
+ checkDeadline();
1819
1949
  const dependentMergedDataStructure =
1820
1950
  dependentAnalyses[filePath][name].metadata?.mergedDataStructure;
1821
1951
 
@@ -1832,6 +1962,7 @@ export default function mergeInDependentDataStructure({
1832
1962
  // Copy over all paths from the dependent's returnValueSchema
1833
1963
  // Only add paths that don't already exist (don't overwrite values set by equivalencies)
1834
1964
  for (const path in dependentMergedDataStructure.returnValueSchema) {
1965
+ checkDeadline();
1835
1966
  const translatedPath = translatePath(path, name);
1836
1967
  if (!(translatedPath in depSchema.returnValueSchema)) {
1837
1968
  depSchema.returnValueSchema[translatedPath] =
@@ -1841,6 +1972,7 @@ export default function mergeInDependentDataStructure({
1841
1972
 
1842
1973
  // Copy over signature schema as well
1843
1974
  for (const path in dependentMergedDataStructure.signatureSchema) {
1975
+ checkDeadline();
1844
1976
  const translatedPath = translatePath(path, name);
1845
1977
  if (!(translatedPath in depSchema.signatureSchema)) {
1846
1978
  depSchema.signatureSchema[translatedPath] =
@@ -1867,6 +1999,7 @@ export default function mergeInDependentDataStructure({
1867
1999
 
1868
2000
  // Merge in the nested dependency schemas
1869
2001
  for (const path in nestedDepSchema.returnValueSchema) {
2002
+ checkDeadline();
1870
2003
  if (!(path in targetDepSchema.returnValueSchema)) {
1871
2004
  const value = nestedDepSchema.returnValueSchema[path];
1872
2005
  targetDepSchema.returnValueSchema[path] = value;
@@ -1874,6 +2007,7 @@ export default function mergeInDependentDataStructure({
1874
2007
  }
1875
2008
 
1876
2009
  for (const path in nestedDepSchema.signatureSchema) {
2010
+ checkDeadline();
1877
2011
  if (!(path in targetDepSchema.signatureSchema)) {
1878
2012
  targetDepSchema.signatureSchema[path] =
1879
2013
  nestedDepSchema.signatureSchema[path];
@@ -1885,5 +2019,15 @@ export default function mergeInDependentDataStructure({
1885
2019
  }
1886
2020
  }
1887
2021
 
2022
+ const totalElapsed = Date.now() - mergeStartTime;
2023
+ const retKeys = Object.keys(mergedDataStructure.returnValueSchema).length;
2024
+
2025
+ // Only log phase breakdown for slow merges (>2s)
2026
+ if (totalElapsed > 2000) {
2027
+ console.log(
2028
+ `CodeYam Log Level 2: ${rootScopeName} merge phases: gather=${gatherElapsed}ms mergeESP=${mergeEspElapsed - gatherElapsed}ms postfix=${postfixElapsed - mergeEspElapsed}ms clean=${cleanElapsed - postfixElapsed}ms depCopy=${totalElapsed - cleanElapsed}ms total=${totalElapsed}ms ret=${retKeys}`,
2029
+ );
2030
+ }
2031
+
1888
2032
  return mergedDataStructure;
1889
2033
  }
@@ -0,0 +1,63 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * Structural test: handleAudit must NOT call runAnalysisForEntities.
5
+ *
6
+ * Previously, handleAudit auto-ran targeted analysis for incomplete entities,
7
+ * which was slow (analyzer startup + AST scan) and contradicted the design
8
+ * intent (comment at line ~6255 says "We do NOT run handleAnalyzeImports here").
9
+ *
10
+ * This test ensures the auto-analysis doesn't creep back in.
11
+ */
12
+ describe('handleAudit - no auto-analysis', () => {
13
+ it('should not call runAnalysisForEntities inside handleAudit', () => {
14
+ const editorTsPath = path.join(__dirname, '..', 'editor.ts');
15
+ const source = fs.readFileSync(editorTsPath, 'utf8');
16
+ // Extract the handleAudit function body.
17
+ // It starts with "async function handleAudit" and ends at the next
18
+ // top-level "async function" or "function" declaration (unindented).
19
+ const handleAuditStart = source.indexOf('async function handleAudit');
20
+ expect(handleAuditStart).toBeGreaterThan(-1);
21
+ // Find the next top-level function declaration after handleAudit starts.
22
+ // Top-level functions start at the beginning of a line (no indentation).
23
+ const afterStart = source.slice(handleAuditStart + 1);
24
+ const nextFnMatch = afterStart.match(/\n(?:async )?function [a-zA-Z]/);
25
+ const handleAuditEnd = nextFnMatch
26
+ ? handleAuditStart + 1 + nextFnMatch.index
27
+ : source.length;
28
+ const handleAuditBody = source.slice(handleAuditStart, handleAuditEnd);
29
+ // The function body must NOT contain runAnalysisForEntities
30
+ expect(handleAuditBody).not.toContain('runAnalysisForEntities');
31
+ });
32
+ it('should report incomplete entities with guidance instead of auto-fixing', () => {
33
+ const editorTsPath = path.join(__dirname, '..', 'editor.ts');
34
+ const source = fs.readFileSync(editorTsPath, 'utf8');
35
+ const handleAuditStart = source.indexOf('async function handleAudit');
36
+ const afterStart = source.slice(handleAuditStart + 1);
37
+ const nextFnMatch = afterStart.match(/\n(?:async )?function [a-zA-Z]/);
38
+ const handleAuditEnd = nextFnMatch
39
+ ? handleAuditStart + 1 + nextFnMatch.index
40
+ : source.length;
41
+ const handleAuditBody = source.slice(handleAuditStart, handleAuditEnd);
42
+ // Should still report incomplete entities
43
+ expect(handleAuditBody).toContain('incompleteEntities');
44
+ expect(handleAuditBody).toContain('formatIncompleteEntityGuidance');
45
+ });
46
+ it('should not call handleAnalyzeImports inside handleAudit', () => {
47
+ const editorTsPath = path.join(__dirname, '..', 'editor.ts');
48
+ const source = fs.readFileSync(editorTsPath, 'utf8');
49
+ const handleAuditStart = source.indexOf('async function handleAudit');
50
+ expect(handleAuditStart).toBeGreaterThan(-1);
51
+ const afterStart = source.slice(handleAuditStart + 1);
52
+ const nextFnMatch = afterStart.match(/\n(?:async )?function [a-zA-Z]/);
53
+ const handleAuditEnd = nextFnMatch
54
+ ? handleAuditStart + 1 + nextFnMatch.index
55
+ : source.length;
56
+ const handleAuditBody = source.slice(handleAuditStart, handleAuditEnd);
57
+ // handleAnalyzeImports calls runAnalysisForEntities internally —
58
+ // it takes minutes on large projects and contradicts the audit's
59
+ // read-only design intent.
60
+ expect(handleAuditBody).not.toContain('handleAnalyzeImports');
61
+ });
62
+ });
63
+ //# sourceMappingURL=editor.auditNoAutoAnalysis.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.auditNoAutoAnalysis.test.js","sourceRoot":"","sources":["../../../../../src/commands/__tests__/editor.auditNoAutoAnalysis.test.ts"],"names":[],"mappings":"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"}