Skip to content

Commit 9db3f3d

Browse files
Merge pull request #89 from CodeDeficient/claude/project-thread-36cl1x-workspace-packages
Skip fresh-version warnings for the workspace's own packages
2 parents 23ac8f4 + 4d8f82f commit 9db3f3d

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes will be documented in this file.
88
- `swallowed_error` (medium): a `catch` block whose only statements are `console` calls. The error is logged and the code carries on as if the operation succeeded. A comment in the block explaining why that is safe, a rethrow, or any other statement (such as `setError(e)`) means it is not reported, and so does a fallback after the `try`, as for `empty_catch`.
99

1010
### Changed
11+
- `fresh_package_version` skips packages the scanned workspace publishes itself (any `name` in a scanned package.json) and lockfile entries that link a workspace folder. TanStack Query got 184 warnings for `@tanstack/*` versions it had just released.
1112
- Folders named `build`, `out` or `temp` are skipped only at the scan root or next to a package.json, where they hold build output. Deeper ones are often source: Next.js keeps its build pipeline in `packages/next/src/build/`, which was never scanned.
1213
- A run whose paths are all missing, ignored or unsupported ends with a one-line error instead of a stack trace.
1314
- An unknown command-line option stops the run with exit 1 and names the option. `--stirct` used to run without strict mode. A `severityOverrides` key that is not a rule id or custom pattern id prints a warning: `type_asertion: "off"` used to do nothing, silently.

‎ai-slop-detector.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,15 @@ export function uniqueRealFiles(files: readonly string[]): string[] {
249249
return [...byRealPath.values()];
250250
}
251251

252+
function readPackageName(manifestPath: string): string | undefined {
253+
try {
254+
const name = asRecord(JSON.parse(fs.readFileSync(manifestPath, 'utf-8')))?.name;
255+
return typeof name === 'string' && name !== '' ? name : undefined;
256+
} catch {
257+
return undefined;
258+
}
259+
}
260+
252261
function isCliPath(filePath: string, content: string): boolean {
253262
if (content.startsWith('#!')) return true;
254263
const normalized = filePath.replace(/\\/g, '/');
@@ -292,6 +301,8 @@ class AISlopDetector {
292301
private registryWarningLogged = false;
293302
private registryUnavailable = false;
294303
private reportedFreshPackageKeys = new Set<string>();
304+
/** Names of the packages the scanned workspace itself publishes; their fresh versions are its own releases. */
305+
private workspacePackages = new Set<string>();
295306
private targetPaths: string[] = [];
296307
private summary: ScanSummary = { filesScanned: 0, linesScanned: 0, applicationLinesScanned: 0 };
297308
private lineCounts = new Map<string, number>();
@@ -484,6 +495,10 @@ class AISlopDetector {
484495
}
485496

486497
const codeFiles = filesToAnalyze.filter(file => !this.manifestFilenames.includes(path.basename(file)));
498+
this.workspacePackages = new Set(filesToAnalyze
499+
.filter(file => path.basename(file) === 'package.json')
500+
.map(file => readPackageName(file))
501+
.filter(name => name !== undefined));
487502
for (const file of filesToAnalyze) {
488503
await this.analyzeFile(file, quiet);
489504
}
@@ -755,7 +770,8 @@ class AISlopDetector {
755770
// for package freshness.
756771
if (!isRegistryBackedLockfileEntry(pkgPath)) continue;
757772
const info = asRecord(pkgInfo);
758-
if (info === undefined || typeof info.version !== 'string') continue;
773+
// `"link": true` points node_modules at a workspace folder.
774+
if (info === undefined || info.link === true || typeof info.version !== 'string') continue;
759775
const pkgName = typeof info.name === 'string' && info.name
760776
? info.name
761777
: pkgPath.split('node_modules/').pop() || pkgPath;
@@ -780,7 +796,8 @@ class AISlopDetector {
780796
// Keep the rule narrow on purpose: only caret/tilde ranges are treated
781797
// as "fresh package" candidates in package.json. package-lock.json is
782798
// always checked because it contains the resolved version.
783-
.filter(({ isRange }) => isRange || filePath.endsWith('package-lock.json'));
799+
.filter(({ isRange }) => isRange || filePath.endsWith('package-lock.json'))
800+
.filter(({ pkgName }) => !this.workspacePackages.has(pkgName));
784801

785802
// Bound concurrency: npm registry rate-limits unauthenticated callers at
786803
// ~600 req/min, and a typical package-lock.json has 200-1000 deps.

‎tests/ai-slop-detector.behavior.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,31 @@ test('npm registry requests send a karpeslop User-Agent', async (t) => {
777777
assert.deepEqual(userAgents, [`karpeslop/${version}`, `karpeslop/${version}`]);
778778
});
779779

780+
test('fresh_package_version skips packages the scanned workspace publishes itself', async t => {
781+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'karpeslop-workspace-'));
782+
try {
783+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'repo', private: true }));
784+
fs.mkdirSync(path.join(dir, 'packages', 'core'), { recursive: true });
785+
fs.writeFileSync(path.join(dir, 'packages', 'core', 'package.json'), JSON.stringify({ name: '@acme/core', version: '5.1.0' }));
786+
fs.mkdirSync(path.join(dir, 'examples', 'basic'), { recursive: true });
787+
fs.writeFileSync(path.join(dir, 'examples', 'basic', 'package.json'),
788+
JSON.stringify({ name: 'basic-example', dependencies: { '@acme/core': '^5.1.0', 'left-pad': '^1.3.0' } }));
789+
const now = new Date().toISOString();
790+
t.mock.method(globalThis, 'fetch', async () => new Response(JSON.stringify({ time: { '5.1.0': now, '1.3.0': now } }), { status: 200 }));
791+
t.mock.method(console, 'log', () => {});
792+
const savedOffline = process.env.KARPESLOP_OFFLINE;
793+
delete process.env.KARPESLOP_OFFLINE;
794+
try {
795+
const issues = await new AISlopDetector(dir, undefined, async () => 'exists').detect();
796+
assert.deepEqual(issues.filter(issue => issue.type === 'fresh_package_version').map(issue => issue.code), ['"left-pad": "1.3.0"']);
797+
} finally {
798+
if (savedOffline !== undefined) process.env.KARPESLOP_OFFLINE = savedOffline;
799+
}
800+
} finally {
801+
fs.rmSync(dir, { recursive: true, force: true });
802+
}
803+
});
804+
780805
test('finding density counts application code only', async t => {
781806
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'karpeslop-density-'));
782807
try {

0 commit comments

Comments
 (0)