llm-slop-detector 0.5.0

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.
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SLOPIGNORE_FILENAME = void 0;
4
+ exports.parseIgnorePatterns = parseIgnorePatterns;
5
+ exports.buildIgnoreMatcher = buildIgnoreMatcher;
6
+ exports.loadIgnoreMatcher = loadIgnoreMatcher;
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ exports.SLOPIGNORE_FILENAME = '.slopignore';
10
+ // Parse .gitignore-style pattern lines into matchers. Blank lines and lines
11
+ // starting with `#` are skipped. `!foo` negates; trailing `/` means
12
+ // directory-only; a leading `/` or an internal `/` anchors to the root; a bare
13
+ // name matches at any depth.
14
+ function parseIgnorePatterns(lines) {
15
+ const out = [];
16
+ for (const rawLine of lines) {
17
+ let line = rawLine.replace(/\r$/, '');
18
+ // Strip unescaped trailing whitespace. `\ ` preserves a trailing space.
19
+ line = line.replace(/(?:^|[^\\])\s+$/, m => {
20
+ const first = m[0];
21
+ return first === ' ' || first === '\t' ? '' : first;
22
+ });
23
+ if (line.length === 0)
24
+ continue;
25
+ if (line.startsWith('#'))
26
+ continue;
27
+ if (line.startsWith('\\#'))
28
+ line = line.slice(1);
29
+ let negate = false;
30
+ if (line.startsWith('!')) {
31
+ negate = true;
32
+ line = line.slice(1);
33
+ }
34
+ let dirOnly = false;
35
+ if (line.endsWith('/')) {
36
+ dirOnly = true;
37
+ line = line.slice(0, -1);
38
+ }
39
+ if (line.length === 0)
40
+ continue;
41
+ let rooted;
42
+ if (line.startsWith('/')) {
43
+ rooted = true;
44
+ line = line.slice(1);
45
+ }
46
+ else {
47
+ rooted = line.includes('/');
48
+ }
49
+ const regex = globToRegex(line, rooted);
50
+ out.push({ raw: rawLine, negate, dirOnly, regex });
51
+ }
52
+ return out;
53
+ }
54
+ function escapeRegexChar(c) {
55
+ return /[.+^${}()|[\]\\/]/.test(c) ? '\\' + c : c;
56
+ }
57
+ function globToRegex(pattern, rooted) {
58
+ let re = '';
59
+ let i = 0;
60
+ while (i < pattern.length) {
61
+ const c = pattern[i];
62
+ if (c === '*') {
63
+ if (pattern[i + 1] === '*') {
64
+ if (pattern[i + 2] === '/') {
65
+ re += '(?:.*/)?';
66
+ i += 3;
67
+ continue;
68
+ }
69
+ re += '.*';
70
+ i += 2;
71
+ continue;
72
+ }
73
+ re += '[^/]*';
74
+ i++;
75
+ continue;
76
+ }
77
+ if (c === '?') {
78
+ re += '[^/]';
79
+ i++;
80
+ continue;
81
+ }
82
+ if (c === '[') {
83
+ const j = pattern.indexOf(']', i + 1);
84
+ if (j !== -1) {
85
+ re += pattern.slice(i, j + 1);
86
+ i = j + 1;
87
+ continue;
88
+ }
89
+ re += '\\[';
90
+ i++;
91
+ continue;
92
+ }
93
+ if (c === '\\' && i + 1 < pattern.length) {
94
+ re += escapeRegexChar(pattern[i + 1]);
95
+ i += 2;
96
+ continue;
97
+ }
98
+ re += escapeRegexChar(c);
99
+ i++;
100
+ }
101
+ const prefix = rooted ? '^' : '^(?:.*/)?';
102
+ // Trailing "(?:/.*)?$" lets a directory pattern ("docs" or "docs/") match
103
+ // every descendant without a second pass.
104
+ const suffix = '(?:/.*)?$';
105
+ return new RegExp(prefix + re + suffix);
106
+ }
107
+ function toPosix(p) {
108
+ return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\//, '');
109
+ }
110
+ function buildIgnoreMatcher(patterns) {
111
+ return {
112
+ patterns,
113
+ ignores(relPath, isDirectory) {
114
+ const normalized = toPosix(relPath);
115
+ if (normalized.length === 0)
116
+ return false;
117
+ let ignored = false;
118
+ for (const p of patterns) {
119
+ if (p.dirOnly && isDirectory === false)
120
+ continue;
121
+ if (p.regex.test(normalized)) {
122
+ ignored = !p.negate;
123
+ }
124
+ }
125
+ return ignored;
126
+ },
127
+ };
128
+ }
129
+ function loadIgnoreMatcher(rootDir, extraPatterns = []) {
130
+ const lines = [];
131
+ if (rootDir !== null) {
132
+ const file = path.join(rootDir, exports.SLOPIGNORE_FILENAME);
133
+ try {
134
+ const text = fs.readFileSync(file, 'utf8');
135
+ lines.push(...text.split(/\r?\n/));
136
+ }
137
+ catch {
138
+ // No .slopignore present -- fall through to just the extra patterns.
139
+ }
140
+ }
141
+ for (const p of extraPatterns) {
142
+ if (typeof p === 'string')
143
+ lines.push(p);
144
+ }
145
+ return buildIgnoreMatcher(parseIgnorePatterns(lines));
146
+ }
147
+ //# sourceMappingURL=ignore.js.map
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILTIN_PACKS = exports.LOCAL_RULES_FILENAME = void 0;
4
+ exports.parseSeverityOverrides = parseSeverityOverrides;
5
+ exports.loadRules = loadRules;
6
+ exports.findLocalRulePathFromCwd = findLocalRulePathFromCwd;
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ exports.LOCAL_RULES_FILENAME = '.llmsloprc.json';
10
+ exports.BUILTIN_PACKS = ['academic', 'cliches', 'fiction', 'claudeisms', 'structural', 'security'];
11
+ function parseSeverity(s, fallback) {
12
+ switch (s) {
13
+ case 'error': return 'error';
14
+ case 'warning': return 'warning';
15
+ case 'information':
16
+ case 'info': return 'information';
17
+ case 'hint': return 'hint';
18
+ default: return fallback;
19
+ }
20
+ }
21
+ // Chars in the invisible/zero-width ranges are dangerous (hide in text,
22
+ // break diffs, enable Trojan Source attacks); visible punctuation is merely suspicious.
23
+ function defaultCharSeverity(char) {
24
+ const code = char.codePointAt(0);
25
+ const invisible = code === 0x00AD ||
26
+ code === 0x00A0 ||
27
+ code === 0x1160 ||
28
+ code === 0x180E ||
29
+ (code >= 0x200B && code <= 0x200F) ||
30
+ (code >= 0x202A && code <= 0x202E) ||
31
+ code === 0x202F ||
32
+ code === 0x2028 || code === 0x2029 ||
33
+ code === 0x2060 ||
34
+ (code >= 0x2066 && code <= 0x2069) ||
35
+ code === 0x3164 ||
36
+ code === 0xFEFF;
37
+ return invisible ? 'warning' : 'information';
38
+ }
39
+ function ingestList(raw, origin, target) {
40
+ const name = typeof raw.name === 'string' ? raw.name : origin;
41
+ let charCount = 0;
42
+ let phraseCount = 0;
43
+ if (Array.isArray(raw.chars)) {
44
+ for (const c of raw.chars) {
45
+ if (typeof c.char !== 'string' || c.char.length === 0)
46
+ continue;
47
+ const charStr = c.char;
48
+ target.chars.set(charStr, {
49
+ char: charStr,
50
+ name: typeof c.name === 'string'
51
+ ? c.name
52
+ : `Unknown char (U+${charStr.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')})`,
53
+ severity: parseSeverity(c.severity, defaultCharSeverity(charStr)),
54
+ replacement: typeof c.replacement === 'string' ? c.replacement : undefined,
55
+ suggestion: typeof c.suggestion === 'string' ? c.suggestion : undefined,
56
+ source: name,
57
+ });
58
+ charCount++;
59
+ }
60
+ }
61
+ if (Array.isArray(raw.phrases)) {
62
+ for (const p of raw.phrases) {
63
+ if (typeof p.pattern !== 'string' || p.pattern.length === 0)
64
+ continue;
65
+ let regex;
66
+ try {
67
+ regex = new RegExp(p.pattern, 'gi');
68
+ }
69
+ catch (e) {
70
+ console.warn(`[LLM Slop] Invalid regex in ${origin}: ${p.pattern}`, e);
71
+ continue;
72
+ }
73
+ target.phrases.push({
74
+ pattern: p.pattern,
75
+ regex,
76
+ reason: typeof p.reason === 'string' ? p.reason : undefined,
77
+ severity: parseSeverity(p.severity, 'information'),
78
+ source: name,
79
+ });
80
+ phraseCount++;
81
+ }
82
+ }
83
+ target.sources.push({
84
+ name,
85
+ version: typeof raw.version === 'string' ? raw.version : undefined,
86
+ description: typeof raw.description === 'string' ? raw.description : undefined,
87
+ origin,
88
+ charCount,
89
+ phraseCount,
90
+ });
91
+ }
92
+ function readJsonFile(p) {
93
+ try {
94
+ const text = fs.readFileSync(p, 'utf8');
95
+ const parsed = JSON.parse(text);
96
+ if (typeof parsed === 'object' && parsed !== null)
97
+ return parsed;
98
+ console.warn(`[LLM Slop] ${p} is not a JSON object`);
99
+ return null;
100
+ }
101
+ catch (e) {
102
+ console.warn(`[LLM Slop] Failed to read ${p}:`, e);
103
+ return null;
104
+ }
105
+ }
106
+ function buildCharRegex(chars) {
107
+ if (chars.size === 0)
108
+ return /(?!)/g;
109
+ const body = Array.from(chars.keys())
110
+ .map(c => '\\u{' + c.codePointAt(0).toString(16) + '}')
111
+ .join('');
112
+ return new RegExp('[' + body + ']', 'gu');
113
+ }
114
+ function charCodepointSelector(char) {
115
+ return `char:U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`;
116
+ }
117
+ // Normalize `char:u+2014` / `char:U+2014` / `char:U+02014` to a single canonical
118
+ // key so lookups don't depend on user casing or zero-padding. Literal char keys
119
+ // (`char:—`) and non-char selectors pass through unchanged.
120
+ function normalizeOverrideKey(key) {
121
+ if (!key.startsWith('char:'))
122
+ return key;
123
+ const rest = key.slice(5);
124
+ const m = /^[uU]\+([0-9a-fA-F]+)$/.exec(rest);
125
+ if (!m)
126
+ return key;
127
+ const cp = parseInt(m[1], 16);
128
+ if (Number.isNaN(cp))
129
+ return key;
130
+ return `char:U+${cp.toString(16).toUpperCase().padStart(4, '0')}`;
131
+ }
132
+ function parseSeverityOverrideValue(v) {
133
+ if (v === 'off')
134
+ return 'off';
135
+ if (v === 'error' || v === 'warning' || v === 'hint')
136
+ return v;
137
+ if (v === 'information' || v === 'info')
138
+ return 'information';
139
+ return null;
140
+ }
141
+ // Canonicalize user-provided override map: normalize char selector keys and
142
+ // validate/coerce values. Invalid values are dropped with a console warning.
143
+ function parseSeverityOverrides(raw) {
144
+ if (!raw || typeof raw !== 'object')
145
+ return {};
146
+ const out = {};
147
+ for (const [key, value] of Object.entries(raw)) {
148
+ const parsed = parseSeverityOverrideValue(value);
149
+ if (parsed === null) {
150
+ console.warn(`[LLM Slop] invalid severity in severityOverrides for "${key}": ${String(value)}`);
151
+ continue;
152
+ }
153
+ out[normalizeOverrideKey(key)] = parsed;
154
+ }
155
+ return out;
156
+ }
157
+ function resolveCharOverride(rule, overrides) {
158
+ // Most specific: rule-level. Literal first so `char:—: hint` beats
159
+ // `char:U+2014: off` when both are present.
160
+ const literal = `char:${rule.char}`;
161
+ if (literal in overrides)
162
+ return overrides[literal];
163
+ const cp = charCodepointSelector(rule.char);
164
+ if (cp in overrides)
165
+ return overrides[cp];
166
+ // Pack-level (source is `pack:<name>` for built-in packs).
167
+ if (rule.source.startsWith('pack:') && rule.source in overrides)
168
+ return overrides[rule.source];
169
+ // Source-level.
170
+ const src = `source:${rule.source}`;
171
+ if (src in overrides)
172
+ return overrides[src];
173
+ return undefined;
174
+ }
175
+ function resolvePhraseOverride(rule, overrides) {
176
+ const phraseKey = `phrase:${rule.pattern}`;
177
+ if (phraseKey in overrides)
178
+ return overrides[phraseKey];
179
+ if (rule.source.startsWith('pack:') && rule.source in overrides)
180
+ return overrides[rule.source];
181
+ const src = `source:${rule.source}`;
182
+ if (src in overrides)
183
+ return overrides[src];
184
+ return undefined;
185
+ }
186
+ function applySeverityOverrides(rules, overrides) {
187
+ if (Object.keys(overrides).length === 0)
188
+ return;
189
+ let applied = 0;
190
+ const charsToDelete = [];
191
+ for (const [char, rule] of rules.chars) {
192
+ const ov = resolveCharOverride(rule, overrides);
193
+ if (ov === undefined)
194
+ continue;
195
+ applied++;
196
+ if (ov === 'off')
197
+ charsToDelete.push(char);
198
+ else
199
+ rules.chars.set(char, { ...rule, severity: ov });
200
+ }
201
+ for (const c of charsToDelete)
202
+ rules.chars.delete(c);
203
+ const remainingPhrases = [];
204
+ for (const phrase of rules.phrases) {
205
+ const ov = resolvePhraseOverride(phrase, overrides);
206
+ if (ov === undefined) {
207
+ remainingPhrases.push(phrase);
208
+ continue;
209
+ }
210
+ applied++;
211
+ if (ov === 'off')
212
+ continue;
213
+ remainingPhrases.push({ ...phrase, severity: ov });
214
+ }
215
+ rules.phrases = remainingPhrases;
216
+ // Recompute per-source counts so the rule-sources quick pick reflects
217
+ // effective rule counts rather than raw ingest counts.
218
+ const countBySource = new Map();
219
+ const bump = (name, kind) => {
220
+ const entry = countBySource.get(name) ?? { chars: 0, phrases: 0 };
221
+ entry[kind]++;
222
+ countBySource.set(name, entry);
223
+ };
224
+ for (const r of rules.chars.values())
225
+ bump(r.source, 'chars');
226
+ for (const r of rules.phrases)
227
+ bump(r.source, 'phrases');
228
+ for (const src of rules.sources) {
229
+ const c = countBySource.get(src.name);
230
+ src.charCount = c?.chars ?? 0;
231
+ src.phraseCount = c?.phrases ?? 0;
232
+ }
233
+ rules.overridesApplied = applied;
234
+ }
235
+ function loadRules(opts) {
236
+ const rules = {
237
+ chars: new Map(),
238
+ phrases: [],
239
+ sources: [],
240
+ charRegex: /(?!)/g,
241
+ overridesApplied: 0,
242
+ };
243
+ if (opts.useBuiltin) {
244
+ const builtinPath = path.join(opts.extensionRoot, 'builtin-rules.json');
245
+ const raw = readJsonFile(builtinPath);
246
+ if (raw)
247
+ ingestList(raw, 'built-in', rules);
248
+ }
249
+ const allowed = new Set(exports.BUILTIN_PACKS);
250
+ for (const pack of opts.enabledPacks) {
251
+ if (!allowed.has(pack))
252
+ continue;
253
+ const packPath = path.join(opts.extensionRoot, 'builtin-packs', `${pack}.json`);
254
+ const raw = readJsonFile(packPath);
255
+ if (raw)
256
+ ingestList(raw, `pack:${pack}`, rules);
257
+ }
258
+ for (const p of opts.localRulePaths) {
259
+ const raw = readJsonFile(p);
260
+ if (raw)
261
+ ingestList(raw, p, rules);
262
+ }
263
+ if (opts.userPhrases.length > 0) {
264
+ ingestList({ name: 'user settings', phrases: opts.userPhrases.map(pattern => ({ pattern })) }, 'settings.json', rules);
265
+ }
266
+ for (const [char, replacement] of Object.entries(opts.charReplacements)) {
267
+ const existing = rules.chars.get(char);
268
+ if (existing) {
269
+ rules.chars.set(char, {
270
+ ...existing,
271
+ replacement,
272
+ source: `${existing.source} + settings`,
273
+ });
274
+ }
275
+ else {
276
+ rules.chars.set(char, {
277
+ char,
278
+ name: `User-defined (U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')})`,
279
+ severity: defaultCharSeverity(char),
280
+ replacement,
281
+ source: 'settings.json',
282
+ });
283
+ }
284
+ }
285
+ applySeverityOverrides(rules, opts.severityOverrides);
286
+ rules.charRegex = buildCharRegex(rules.chars);
287
+ return rules;
288
+ }
289
+ function findLocalRulePathFromCwd(startDir) {
290
+ let dir = path.resolve(startDir);
291
+ while (true) {
292
+ const candidate = path.join(dir, exports.LOCAL_RULES_FILENAME);
293
+ if (fs.existsSync(candidate))
294
+ return candidate;
295
+ const parent = path.dirname(dir);
296
+ if (parent === dir)
297
+ return null;
298
+ dir = parent;
299
+ }
300
+ }
301
+ //# sourceMappingURL=rules.js.map