Skip to content

Commit 4150c6d

Browse files
prosdevclaude
andcommitted
fix(core,cli): resolve workspace callees and fix flaky CI test
- Resolve workspace package symlinks to source paths in callee extraction. pnpm workspace links (node_modules/@scope/pkg → packages/pkg) are now followed via realpathSync, producing cross-package edges in the graph. - Extract normalizeAndRelativize as a pure function for dist/ → src/, .d.ts → .ts, and absolute → relative path normalization. - Filter resolved paths that are still inside node_modules (e.g. @types/node) to prevent external dependencies from appearing in hot paths. - Bump flaky CI test timeout from 30s to 60s (ts-morph init is slow on GitHub runners). - Clean up scratchpad: remove resolved flaky test entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 173cb7f commit 4150c6d

4 files changed

Lines changed: 104 additions & 18 deletions

File tree

.claude/scratchpad.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
## Flaky Tests
2424

25-
- **`packages/cli/src/commands/commands.test.ts:119` — "should display indexing summary without storage size"** times out at 30s on GitHub CI runners. The test indexes files and the slower CI runner can't finish in time. Needs either a higher timeout, a smaller test fixture, or mocking the indexer. Seen on PR #17 CI run.
25+
(none currently tracked)
2626

2727
## Test Gaps
2828

packages/cli/src/commands/commands.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,6 @@ export class Calculator {
162162
// Verify storage size is NOT shown (deferred to `dev stats`)
163163
const hasStorageSize = loggedMessages.some((msg) => msg.includes('Storage:'));
164164
expect(hasStorageSize).toBe(false);
165-
}, 30000); // 30s timeout for indexing
165+
}, 60000); // 60s timeout — ts-morph project init is slow on CI runners
166166
});
167167
});

packages/core/src/scanner/__tests__/scanner.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as path from 'node:path';
22
import { describe, expect, it } from 'vitest';
33
import { MarkdownScanner } from '../markdown';
44
import { ScannerRegistry } from '../registry';
5-
import { TypeScriptScanner } from '../typescript';
5+
import { normalizeAndRelativize, TypeScriptScanner } from '../typescript';
66

77
// Helper to create registry
88
function createDefaultRegistry(): ScannerRegistry {
@@ -1006,3 +1006,52 @@ describe('Scanner', () => {
10061006
});
10071007
});
10081008
});
1009+
1010+
describe('normalizeAndRelativize', () => {
1011+
const repoRoot = '/Users/dev/project';
1012+
1013+
it('should replace dist/ with src/', () => {
1014+
expect(
1015+
normalizeAndRelativize('/Users/dev/project/packages/core/dist/map/index.ts', repoRoot)
1016+
).toBe('packages/core/src/map/index.ts');
1017+
});
1018+
1019+
it('should replace .d.ts with .ts', () => {
1020+
expect(
1021+
normalizeAndRelativize('/Users/dev/project/packages/core/dist/types.d.ts', repoRoot)
1022+
).toBe('packages/core/src/types.ts');
1023+
});
1024+
1025+
it('should replace .js with .ts', () => {
1026+
expect(normalizeAndRelativize('/Users/dev/project/packages/core/dist/index.js', repoRoot)).toBe(
1027+
'packages/core/src/index.ts'
1028+
);
1029+
});
1030+
1031+
it('should handle multiple dist/ segments', () => {
1032+
expect(
1033+
normalizeAndRelativize(
1034+
'/Users/dev/project/packages/core/dist/formatters/dist/utils.js',
1035+
repoRoot
1036+
)
1037+
).toBe('packages/core/src/formatters/src/utils.ts');
1038+
});
1039+
1040+
it('should make paths relative to repoRoot', () => {
1041+
expect(normalizeAndRelativize('/Users/dev/project/packages/cli/src/cli.ts', repoRoot)).toBe(
1042+
'packages/cli/src/cli.ts'
1043+
);
1044+
});
1045+
1046+
it('should return absolute path when repoRoot is empty', () => {
1047+
expect(normalizeAndRelativize('/abs/packages/core/src/index.ts', '')).toBe(
1048+
'/abs/packages/core/src/index.ts'
1049+
);
1050+
});
1051+
1052+
it('should handle path without dist/', () => {
1053+
expect(normalizeAndRelativize('/Users/dev/project/packages/core/src/index.ts', repoRoot)).toBe(
1054+
'packages/core/src/index.ts'
1055+
);
1056+
});
1057+
});

packages/core/src/scanner/typescript.ts

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from 'node:fs';
12
import * as path from 'node:path';
23
import type { Logger } from '@prosdevlab/kero';
34
import {
@@ -19,6 +20,21 @@ import {
1920
import { getCurrentSystemResources, getOptimalConcurrency } from '../utils/concurrency';
2021
import type { CalleeInfo, Document, Scanner, ScannerCapabilities } from './types';
2122

23+
/**
24+
* Normalize a resolved file path: dist/ → src/, .d.ts → .ts, absolute → relative.
25+
* Pure function — no I/O.
26+
*/
27+
export function normalizeAndRelativize(filePath: string, repoRoot: string): string {
28+
let normalized = filePath
29+
.replaceAll('/dist/', '/src/')
30+
.replace(/\.d\.ts$/, '.ts')
31+
.replace(/\.js$/, '.ts');
32+
if (repoRoot && normalized.startsWith(repoRoot)) {
33+
normalized = path.relative(repoRoot, normalized);
34+
}
35+
return normalized;
36+
}
37+
2238
/**
2339
* Enhanced TypeScript scanner using ts-morph
2440
* Provides type information and cross-file references
@@ -987,21 +1003,7 @@ export class TypeScriptScanner implements Scanner {
9871003
const declSourceFile = firstDecl.getSourceFile();
9881004
if (declSourceFile) {
9891005
const rawPath = declSourceFile.getFilePath() as string;
990-
// Only include if it's within the project (not node_modules)
991-
if (rawPath && !rawPath.includes('node_modules')) {
992-
// Normalize: dist/ → src/, .d.ts → .ts, then make relative to repo root.
993-
// ts-morph resolves imports to absolute dist output paths
994-
// (e.g. /abs/packages/logger/dist/types.d.ts) but we store
995-
// relative source paths (packages/logger/src/types.ts).
996-
let normalized = rawPath
997-
.replaceAll('/dist/', '/src/')
998-
.replace(/\.d\.ts$/, '.ts')
999-
.replace(/\.js$/, '.ts');
1000-
if (this.repoRoot && normalized.startsWith(this.repoRoot)) {
1001-
normalized = path.relative(this.repoRoot, normalized);
1002-
}
1003-
file = normalized;
1004-
}
1006+
file = this.normalizeCalleePath(rawPath);
10051007
}
10061008
}
10071009
}
@@ -1016,4 +1018,39 @@ export class TypeScriptScanner implements Scanner {
10161018
file,
10171019
};
10181020
}
1021+
1022+
/**
1023+
* Normalize a callee file path to a relative source path.
1024+
*
1025+
* Handles three cases:
1026+
* 1. Direct project files (not in node_modules) — normalize dist/ → src/
1027+
* 2. Workspace package symlinks (node_modules/@scope/pkg → packages/pkg) — resolve symlink
1028+
* 3. External node_modules — skip (return undefined)
1029+
*/
1030+
private normalizeCalleePath(rawPath: string): string | undefined {
1031+
if (!rawPath) return undefined;
1032+
1033+
// Case 1: Not in node_modules — direct project file
1034+
if (!rawPath.includes('node_modules')) {
1035+
return normalizeAndRelativize(rawPath, this.repoRoot);
1036+
}
1037+
1038+
// Case 2: Workspace package symlink — resolve to real path
1039+
// pnpm workspace links: node_modules/@scope/pkg → ../../actual-pkg
1040+
// After resolving, the real path is inside the repo but NOT in node_modules
1041+
if (this.repoRoot) {
1042+
try {
1043+
const realPath = fs.realpathSync(rawPath);
1044+
if (realPath.startsWith(this.repoRoot) && !realPath.includes('node_modules')) {
1045+
// It's a workspace package — normalize and make relative
1046+
return normalizeAndRelativize(realPath, this.repoRoot);
1047+
}
1048+
} catch {
1049+
// realpathSync can fail if the file doesn't exist
1050+
}
1051+
}
1052+
1053+
// Case 3: External dependency — skip
1054+
return undefined;
1055+
}
10191056
}

0 commit comments

Comments
 (0)