Skip to content

Commit 023da3b

Browse files
authored
Merge pull request #30 from ravidsrk/ravidsrk/slop-detect-w2a-core
refactor(core): migrate to @slop-detect/core (tsup + vitest + composite tsconfig)
2 parents 567ec04 + 9db7852 commit 023da3b

15 files changed

Lines changed: 239 additions & 266 deletions

bun.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"demo": "node dist/bin/slop.js https://news.ycombinator.com https://www.aura.build https://lovable.dev"
3333
},
3434
"dependencies": {
35-
"slop-detect-core": "0.7.0",
35+
"@slop-detect/core": "workspace:*",
3636
"playwright": "^1.49.0"
3737
},
3838
"keywords": [

packages/core/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist/
2+
coverage/
3+
.turbo/
4+
*.tsbuildinfo

packages/core/package.json

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "slop-detect-core",
3-
"version": "0.7.0",
2+
"name": "@slop-detect/core",
3+
"version": "0.8.0",
44
"description": "Pure detection engine for AI-design-slop patterns. Runtime-agnostic — runs in Node, Cloudflare Workers, or the browser.",
55
"type": "module",
66
"license": "MIT",
@@ -10,50 +10,32 @@
1010
"url": "git+https://github.com/ravidsrk/slop-detect.git",
1111
"directory": "packages/core"
1212
},
13-
"main": "./dist/index.js",
13+
"main": "./dist/index.cjs",
14+
"module": "./dist/index.js",
1415
"types": "./dist/index.d.ts",
1516
"exports": {
1617
".": {
1718
"types": "./dist/index.d.ts",
18-
"default": "./dist/index.js"
19-
},
20-
"./patterns": {
21-
"types": "./dist/patterns.d.ts",
22-
"default": "./dist/patterns.js"
23-
},
24-
"./fonts": {
25-
"types": "./dist/fonts.d.ts",
26-
"default": "./dist/fonts.js"
27-
},
28-
"./color": {
29-
"types": "./dist/color.d.ts",
30-
"default": "./dist/color.js"
31-
},
32-
"./visibility": {
33-
"types": "./dist/visibility.d.ts",
34-
"default": "./dist/visibility.js"
35-
},
36-
"./fixes": {
37-
"types": "./dist/fixes.d.ts",
38-
"default": "./dist/fixes.js"
39-
},
40-
"./aeo": {
41-
"types": "./dist/aeo.d.ts",
42-
"default": "./dist/aeo.js"
19+
"import": "./dist/index.js",
20+
"require": "./dist/index.cjs"
4321
}
4422
},
4523
"scripts": {
46-
"build": "tsc",
24+
"build": "tsup",
25+
"test": "vitest run",
26+
"test:watch": "vitest",
4727
"typecheck": "tsc --noEmit",
48-
"prepack": "tsc"
28+
"clean": "rm -rf dist .turbo"
4929
},
5030
"files": [
5131
"dist",
52-
"src",
5332
"README.md",
54-
"LICENSE",
55-
"NOTICE"
33+
"LICENSE"
5634
],
35+
"sideEffects": false,
36+
"publishConfig": {
37+
"access": "public"
38+
},
5739
"keywords": [
5840
"slop",
5941
"ai-detection",

packages/core/test/aeo.test.js

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,77 @@
11
// Unit tests for the AEO axis. Pure functions + an injected fetch mock so the
22
// check runner is fully deterministic (no network). Run with:
3-
// node --test packages/core/test/aeo.test.js
4-
import { test } from 'node:test';
5-
import assert from 'node:assert/strict';
3+
// vitest run packages/core/test/aeo.test.js
4+
import { test, expect } from 'vitest';
65
import {
76
AI_BOTS,
87
AEO_CHECKS,
98
detectAIBot,
109
aiBotsBlockedByRobots,
1110
toMarkdownUrl,
1211
runAeoChecks,
13-
} from '../src/aeo.js';
12+
} from '@slop-detect/core';
1413

1514
// ── detectAIBot ──────────────────────────────────────────────────────────────
1615
test('detectAIBot identifies known crawlers by substring, case-insensitive', () => {
1716
const gpt = detectAIBot('Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)');
18-
assert.equal(gpt.isBot, true);
19-
assert.equal(gpt.vendor, 'OpenAI');
20-
assert.equal(gpt.purpose, 'training');
17+
expect(gpt.isBot).toBe(true);
18+
expect(gpt.vendor).toBe('OpenAI');
19+
expect(gpt.purpose).toBe('training');
2120

2221
const claude = detectAIBot('claudebot/1.0');
23-
assert.equal(claude.isBot, true);
24-
assert.equal(claude.vendor, 'Anthropic');
22+
expect(claude.isBot).toBe(true);
23+
expect(claude.vendor).toBe('Anthropic');
2524

2625
const human = detectAIBot('Mozilla/5.0 (Macintosh) Safari/605');
27-
assert.equal(human.isBot, false);
28-
assert.equal(human.vendor, null);
26+
expect(human.isBot).toBe(false);
27+
expect(human.vendor).toBe(null);
2928

30-
assert.equal(detectAIBot('').isBot, false);
29+
expect(detectAIBot('').isBot).toBe(false);
3130
});
3231

3332
test('AI_BOTS registry is non-trivial and covers the big four vendors', () => {
34-
assert.ok(AI_BOTS.length >= 20, `expected a substantial registry, got ${AI_BOTS.length}`);
33+
expect(AI_BOTS.length >= 20).toBeTruthy();
3534
const vendors = new Set(AI_BOTS.map((b) => b.vendor));
3635
for (const v of ['OpenAI', 'Anthropic', 'Perplexity', 'Google']) {
37-
assert.ok(vendors.has(v), `registry missing ${v}`);
36+
expect(vendors.has(v)).toBeTruthy();
3837
}
3938
});
4039

4140
// ── toMarkdownUrl ────────────────────────────────────────────────────────────
4241
test('toMarkdownUrl maps pages to their .md twin', () => {
43-
assert.equal(toMarkdownUrl('https://x.com/blog/post'), 'https://x.com/blog/post.md');
44-
assert.equal(toMarkdownUrl('https://x.com/'), 'https://x.com/index.md');
45-
assert.equal(toMarkdownUrl('https://x.com/a/'), 'https://x.com/a.md');
42+
expect(toMarkdownUrl('https://x.com/blog/post')).toBe('https://x.com/blog/post.md');
43+
expect(toMarkdownUrl('https://x.com/')).toBe('https://x.com/index.md');
44+
expect(toMarkdownUrl('https://x.com/a/')).toBe('https://x.com/a.md');
4645
// idempotent
47-
assert.equal(toMarkdownUrl('https://x.com/a.md'), 'https://x.com/a.md');
46+
expect(toMarkdownUrl('https://x.com/a.md')).toBe('https://x.com/a.md');
4847
});
4948

5049
// ── robots.txt parser ────────────────────────────────────────────────────────
5150
test('aiBotsBlockedByRobots: blanket Disallow under * blocks AI bots', () => {
5251
const r = aiBotsBlockedByRobots('User-agent: *\nDisallow: /', '/blog');
53-
assert.equal(r.blocked, true);
54-
assert.deepEqual(r.agents, ['*']);
52+
expect(r.blocked).toBe(true);
53+
expect(r.agents).toEqual(['*']);
5554
});
5655

5756
test('aiBotsBlockedByRobots: named AI bot disallow is detected', () => {
5857
const r = aiBotsBlockedByRobots(
5958
'User-agent: GPTBot\nDisallow: /\n\nUser-agent: *\nDisallow:',
6059
'/'
6160
);
62-
assert.equal(r.blocked, true);
63-
assert.ok(r.agents.includes('gptbot'));
61+
expect(r.blocked).toBe(true);
62+
expect(r.agents.includes('gptbot')).toBeTruthy();
6463
});
6564

6665
test('aiBotsBlockedByRobots: empty Disallow and unrelated bots do not block', () => {
67-
assert.equal(aiBotsBlockedByRobots('User-agent: *\nDisallow:', '/').blocked, false);
68-
assert.equal(aiBotsBlockedByRobots('User-agent: AhrefsBot\nDisallow: /', '/').blocked, false);
69-
assert.equal(aiBotsBlockedByRobots('', '/').blocked, false);
66+
expect(aiBotsBlockedByRobots('User-agent: *\nDisallow:', '/').blocked).toBe(false);
67+
expect(aiBotsBlockedByRobots('User-agent: AhrefsBot\nDisallow: /', '/').blocked).toBe(false);
68+
expect(aiBotsBlockedByRobots('', '/').blocked).toBe(false);
7069
});
7170

7271
test('aiBotsBlockedByRobots: path-scoped disallow only blocks matching paths', () => {
7372
const robots = 'User-agent: *\nDisallow: /admin';
74-
assert.equal(aiBotsBlockedByRobots(robots, '/admin/users').blocked, true);
75-
assert.equal(aiBotsBlockedByRobots(robots, '/blog/post').blocked, false);
73+
expect(aiBotsBlockedByRobots(robots, '/admin/users').blocked).toBe(true);
74+
expect(aiBotsBlockedByRobots(robots, '/blog/post').blocked).toBe(false);
7675
});
7776

7877
// ── runAeoChecks with an injected fetch mock ─────────────────────────────────
@@ -115,14 +114,10 @@ test('runAeoChecks: fully AEO-ready site scores AI-Ready (100)', async () => {
115114
});
116115

117116
const r = await runAeoChecks('https://ready.example', { fetchImpl, timeoutMs: 1000 });
118-
assert.equal(r.axis, 'aeo');
119-
assert.equal(
120-
r.score,
121-
100,
122-
`expected perfect, got ${r.score}: ${JSON.stringify(r.failed.map((c) => c.id))}`
123-
);
124-
assert.equal(r.tier, 'AI-Ready');
125-
assert.equal(r.requiredFailed, 0);
117+
expect(r.axis).toBe('aeo');
118+
expect(r.score).toBe(100);
119+
expect(r.tier).toBe('AI-Ready');
120+
expect(r.requiredFailed).toBe(0);
126121
});
127122

128123
test('runAeoChecks: site that blocks GPTBot + has noindex fails the required checks', async () => {
@@ -142,14 +137,14 @@ test('runAeoChecks: site that blocks GPTBot + has noindex fails the required che
142137

143138
const r = await runAeoChecks('https://blocked.example', { fetchImpl, timeoutMs: 1000 });
144139
const failedIds = new Set(r.failed.map((c) => c.id));
145-
assert.ok(failedIds.has('bot.notBlocked'), 'should flag blocked GPTBot');
146-
assert.ok(failedIds.has('robots.aiAllowed'), 'should flag robots disallow');
147-
assert.ok(failedIds.has('html.indexable'), 'should flag noindex');
148-
assert.ok(r.requiredFailed >= 3);
149-
assert.equal(r.tier, 'Invisible');
140+
expect(failedIds.has('bot.notBlocked')).toBeTruthy();
141+
expect(failedIds.has('robots.aiAllowed')).toBeTruthy();
142+
expect(failedIds.has('html.indexable')).toBeTruthy();
143+
expect(r.requiredFailed >= 3).toBeTruthy();
144+
expect(r.tier).toBe('Invisible');
150145
});
151146

152147
test('AEO_CHECKS weights total 100', () => {
153148
const total = AEO_CHECKS.reduce((s, c) => s + c.weight, 0);
154-
assert.equal(total, 100);
149+
expect(total).toBe(100);
155150
});

packages/core/test/aeo_ssrf.test.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
// when given an isUrlAllowed hook, so a public URL can't bounce the fetcher to
33
// an internal host.
44

5-
import { test } from 'node:test';
6-
import assert from 'node:assert/strict';
7-
import { runAeoChecks } from '../src/aeo.js';
5+
import { test, expect } from 'vitest';
6+
import { runAeoChecks } from '@slop-detect/core';
87

98
// A fetch mock that 302-redirects the main page to cloud-metadata, and returns
109
// a benign 200 for everything else (robots/llms/md/etc.).
@@ -31,10 +30,10 @@ test('runAeoChecks blocks a redirect to an internal host (no internal fetch)', a
3130
timeoutMs: 2000,
3231
});
3332
// The internal metadata host must never have been fetched.
34-
assert.ok(!seen.some((u) => u.includes('169.254.169.254')), 'must not fetch the internal host');
33+
expect(seen.some((u) => u.includes('169.254.169.254'))).toBe(false);
3534
// And the reachability check should have failed (blocked), not silently passed.
3635
const reachable = report.checks.find((c) => c.id === 'html.reachable');
37-
assert.equal(reachable.passed, false, 'blocked redirect → not reachable');
36+
expect(reachable.passed).toBe(false);
3837
});
3938

4039
test('without isUrlAllowed, behavior is unchanged (auto-follow)', async () => {
@@ -45,5 +44,5 @@ test('without isUrlAllowed, behavior is unchanged (auto-follow)', async () => {
4544
headers: { 'content-type': 'text/html' },
4645
});
4746
const report = await runAeoChecks('https://ok.example/', { fetchImpl, timeoutMs: 2000 });
48-
assert.equal(report.checks.find((c) => c.id === 'html.reachable').passed, true);
47+
expect(report.checks.find((c) => c.id === 'html.reachable').passed).toBe(true);
4948
});

packages/core/test/color.test.js

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
// createColorHelpers() builds a <canvas> at construction (for parseColor), so we
66
// stub a minimal document. The math functions under test never touch the canvas.
77
//
8-
// Run with: node --test packages/core/test/color.test.js
9-
import { test } from 'node:test';
10-
import assert from 'node:assert/strict';
8+
// Run with: vitest run packages/core/test/color.test.js
9+
import { test, expect } from 'vitest';
10+
import { createColorHelpers } from '@slop-detect/core';
1111

1212
// Minimal DOM stub so createColorHelpers() can construct.
1313
globalThis.document = {
@@ -26,47 +26,46 @@ globalThis.document = {
2626
}),
2727
};
2828

29-
const { createColorHelpers } = await import('../src/color.js');
3029
const h = createColorHelpers();
3130

3231
const WHITE = { r: 255, g: 255, b: 255, a: 1 };
3332
const BLACK = { r: 0, g: 0, b: 0, a: 1 };
3433

3534
test('relativeLuminance: black is 0, white is 1', () => {
36-
assert.equal(h.relativeLuminance(BLACK), 0);
37-
assert.ok(Math.abs(h.relativeLuminance(WHITE) - 1) < 1e-9);
35+
expect(h.relativeLuminance(BLACK)).toBe(0);
36+
expect(Math.abs(h.relativeLuminance(WHITE) - 1) < 1e-9).toBeTruthy();
3837
});
3938

4039
test('contrastRatio: black-on-white is 21:1', () => {
41-
assert.ok(Math.abs(h.contrastRatio(BLACK, WHITE) - 21) < 0.01);
42-
assert.equal(h.contrastRatio(WHITE, WHITE), 1); // identical = 1:1
40+
expect(Math.abs(h.contrastRatio(BLACK, WHITE) - 21) < 0.01).toBeTruthy();
41+
expect(h.contrastRatio(WHITE, WHITE)).toBe(1); // identical = 1:1
4342
});
4443

4544
test('contrastRatio: known WCAG pair #767676 on white ≈ 4.54:1 (AA body pass)', () => {
4645
const grey = { r: 0x76, g: 0x76, b: 0x76, a: 1 };
4746
const ratio = h.contrastRatio(grey, WHITE);
48-
assert.ok(ratio >= 4.5 && ratio < 4.6, `expected ~4.54, got ${ratio}`);
47+
expect(ratio >= 4.5 && ratio < 4.6).toBeTruthy();
4948
});
5049

5150
test('contrastRatio: light grey #aaa on white fails AA body (< 4.5)', () => {
5251
const lightGrey = { r: 0xaa, g: 0xaa, b: 0xaa, a: 1 };
53-
assert.ok(h.contrastRatio(lightGrey, WHITE) < 4.5);
52+
expect(h.contrastRatio(lightGrey, WHITE) < 4.5).toBeTruthy();
5453
});
5554

5655
test('channelSpread: greys are ~0, saturated colors are high', () => {
57-
assert.equal(h.channelSpread({ r: 128, g: 128, b: 128 }), 0);
58-
assert.equal(h.channelSpread({ r: 99, g: 102, b: 241 }), 142); // indigo-500
56+
expect(h.channelSpread({ r: 128, g: 128, b: 128 })).toBe(0);
57+
expect(h.channelSpread({ r: 99, g: 102, b: 241 })).toBe(142); // indigo-500
5958
});
6059

6160
test('isNeutral: greys/black/white neutral, saturated not', () => {
62-
assert.equal(h.isNeutral({ r: 128, g: 130, b: 132, a: 1 }), true); // spread 4
63-
assert.equal(h.isNeutral(WHITE), true);
64-
assert.equal(h.isNeutral({ r: 0, g: 0, b: 0, a: 0 }), true); // transparent
65-
assert.equal(h.isNeutral({ r: 99, g: 102, b: 241, a: 1 }), false); // indigo
66-
assert.equal(h.isNeutral({ r: 200, g: 200, b: 160, a: 1 }), false); // spread 40 — tinted
61+
expect(h.isNeutral({ r: 128, g: 130, b: 132, a: 1 })).toBe(true); // spread 4
62+
expect(h.isNeutral(WHITE)).toBe(true);
63+
expect(h.isNeutral({ r: 0, g: 0, b: 0, a: 0 })).toBe(true); // transparent
64+
expect(h.isNeutral({ r: 99, g: 102, b: 241, a: 1 })).toBe(false); // indigo
65+
expect(h.isNeutral({ r: 200, g: 200, b: 160, a: 1 })).toBe(false); // spread 40 — tinted
6766
});
6867

6968
test('isNeutral: spread exactly at 30 boundary is NOT neutral', () => {
70-
assert.equal(h.isNeutral({ r: 100, g: 100, b: 130, a: 1 }), false); // spread 30
71-
assert.equal(h.isNeutral({ r: 100, g: 100, b: 129, a: 1 }), true); // spread 29
69+
expect(h.isNeutral({ r: 100, g: 100, b: 130, a: 1 })).toBe(false); // spread 30
70+
expect(h.isNeutral({ r: 100, g: 100, b: 129, a: 1 })).toBe(true); // spread 29
7271
});

0 commit comments

Comments
 (0)