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,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.charDiagnosticMessage = charDiagnosticMessage;
4
+ exports.scanText = scanText;
5
+ exports.offsetToLineCol = offsetToLineCol;
6
+ const comments_1 = require("./comments");
7
+ function charDiagnosticMessage(def) {
8
+ const parts = [def.name];
9
+ if (def.replacement !== undefined) {
10
+ const shown = def.replacement === '' ? 'delete' :
11
+ def.replacement === '\n' ? 'newline' :
12
+ def.replacement === ' ' ? 'regular space' :
13
+ JSON.stringify(def.replacement);
14
+ parts.push(`fix: ${shown}`);
15
+ }
16
+ else if (def.suggestion) {
17
+ parts.push(def.suggestion);
18
+ }
19
+ return `${parts.join(' - ')} [${def.source}]`;
20
+ }
21
+ function scanText(text, rules, language) {
22
+ const findings = [];
23
+ const excluded = computeExcludedRanges(text, language);
24
+ if (excluded === null)
25
+ return findings;
26
+ const suppressions = computeSuppressions(text, excluded);
27
+ rules.charRegex.lastIndex = 0;
28
+ let m;
29
+ while ((m = rules.charRegex.exec(text)) !== null) {
30
+ const def = rules.chars.get(m[0]);
31
+ if (!def)
32
+ continue;
33
+ if (offsetInRanges(m.index, excluded))
34
+ continue;
35
+ if (isSuppressed(m.index, suppressions, 'char', m[0]))
36
+ continue;
37
+ findings.push({
38
+ offset: m.index,
39
+ length: m[0].length,
40
+ matchText: m[0],
41
+ code: 'char',
42
+ severity: def.severity,
43
+ message: charDiagnosticMessage(def),
44
+ source: def.source,
45
+ });
46
+ }
47
+ for (const p of rules.phrases) {
48
+ p.regex.lastIndex = 0;
49
+ while ((m = p.regex.exec(text)) !== null) {
50
+ if (m[0].length === 0) {
51
+ p.regex.lastIndex++;
52
+ continue;
53
+ }
54
+ if (offsetInRanges(m.index, excluded))
55
+ continue;
56
+ if (isSuppressed(m.index, suppressions, 'phrase', m[0], p.pattern))
57
+ continue;
58
+ const reasonBit = p.reason ? ` - ${p.reason}` : '';
59
+ findings.push({
60
+ offset: m.index,
61
+ length: m[0].length,
62
+ matchText: m[0],
63
+ code: 'phrase',
64
+ severity: p.severity,
65
+ message: `LLM-style phrase: "${m[0]}"${reasonBit} [${p.source}]`,
66
+ source: p.source,
67
+ rulePattern: p.pattern,
68
+ });
69
+ }
70
+ }
71
+ return findings;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Scope: markdown exclusions, code-comment inclusion, inline ignore directives
75
+ // ---------------------------------------------------------------------------
76
+ // Returns the ranges that should be SKIPPED during scanning. null means the
77
+ // language isn't supported and the file should not be scanned at all.
78
+ function computeExcludedRanges(text, language) {
79
+ if (language === 'markdown')
80
+ return computeMarkdownExclusions(text);
81
+ if (language === 'plaintext' || language === 'scminput')
82
+ return [];
83
+ if (language === 'git-commit')
84
+ return computeGitCommitExclusions(text);
85
+ const commentScanner = (0, comments_1.getCommentScanner)(language);
86
+ if (commentScanner === null)
87
+ return null;
88
+ const comments = mergeRanges(commentScanner(text));
89
+ return invertRanges(comments, text.length);
90
+ }
91
+ // Skip '#' comment lines (stripped by git before commit) and everything after
92
+ // the verbose-commit scissors marker ("# ------------------------ >8 ---...").
93
+ function computeGitCommitExclusions(text) {
94
+ const ranges = [];
95
+ const scissorsRe = /^#\s*-+\s*>8\s*-+/;
96
+ let i = 0;
97
+ while (i <= text.length) {
98
+ const nl = text.indexOf('\n', i);
99
+ const lineEnd = nl === -1 ? text.length : nl;
100
+ const line = text.slice(i, lineEnd);
101
+ if (scissorsRe.test(line)) {
102
+ ranges.push([i, text.length]);
103
+ break;
104
+ }
105
+ if (line.startsWith('#')) {
106
+ ranges.push([i, nl === -1 ? text.length : nl + 1]);
107
+ }
108
+ if (nl === -1)
109
+ break;
110
+ i = nl + 1;
111
+ }
112
+ return mergeRanges(ranges);
113
+ }
114
+ function invertRanges(ranges, textLen) {
115
+ const inverted = [];
116
+ let cursor = 0;
117
+ for (const [s, e] of ranges) {
118
+ if (s > cursor)
119
+ inverted.push([cursor, s]);
120
+ cursor = Math.max(cursor, e);
121
+ }
122
+ if (cursor < textLen)
123
+ inverted.push([cursor, textLen]);
124
+ return inverted;
125
+ }
126
+ function mergeRanges(ranges) {
127
+ if (ranges.length === 0)
128
+ return ranges;
129
+ ranges.sort((a, b) => a[0] - b[0]);
130
+ const merged = [[ranges[0][0], ranges[0][1]]];
131
+ for (let i = 1; i < ranges.length; i++) {
132
+ const last = merged[merged.length - 1];
133
+ const curr = ranges[i];
134
+ if (curr[0] <= last[1]) {
135
+ last[1] = Math.max(last[1], curr[1]);
136
+ }
137
+ else {
138
+ merged.push([curr[0], curr[1]]);
139
+ }
140
+ }
141
+ return merged;
142
+ }
143
+ function offsetInRanges(offset, ranges) {
144
+ let lo = 0, hi = ranges.length - 1;
145
+ while (lo <= hi) {
146
+ const mid = (lo + hi) >> 1;
147
+ const [s, e] = ranges[mid];
148
+ if (offset < s)
149
+ hi = mid - 1;
150
+ else if (offset >= e)
151
+ lo = mid + 1;
152
+ else
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ // Line-based scan for fenced code blocks and YAML frontmatter, plus regex for
158
+ // inline code spans and link URLs. Good enough for the 95% case without
159
+ // pulling in a CommonMark parser.
160
+ function computeMarkdownExclusions(text) {
161
+ const ranges = [];
162
+ let i = 0;
163
+ let lineIdx = 0;
164
+ let inFence = false;
165
+ let fenceChar = '';
166
+ let fenceLen = 0;
167
+ let fenceStart = 0;
168
+ let inFrontmatter = false;
169
+ let frontmatterStart = 0;
170
+ while (i <= text.length) {
171
+ const nl = text.indexOf('\n', i);
172
+ const lineEnd = nl === -1 ? text.length : nl;
173
+ const line = text.slice(i, lineEnd);
174
+ const nextLineStart = nl === -1 ? text.length : nl + 1;
175
+ if (!inFence) {
176
+ if (lineIdx === 0 && line === '---') {
177
+ inFrontmatter = true;
178
+ frontmatterStart = i;
179
+ }
180
+ else if (inFrontmatter && (line === '---' || line === '...')) {
181
+ ranges.push([frontmatterStart, nextLineStart]);
182
+ inFrontmatter = false;
183
+ }
184
+ else if (!inFrontmatter) {
185
+ const m = line.match(/^ {0,3}(`{3,}|~{3,})/);
186
+ if (m) {
187
+ inFence = true;
188
+ fenceChar = m[1][0];
189
+ fenceLen = m[1].length;
190
+ fenceStart = i;
191
+ }
192
+ }
193
+ }
194
+ else {
195
+ const closer = new RegExp('^ {0,3}' + (fenceChar === '`' ? '`' : '~') + '{' + fenceLen + ',}\\s*$');
196
+ if (closer.test(line)) {
197
+ ranges.push([fenceStart, nextLineStart]);
198
+ inFence = false;
199
+ }
200
+ }
201
+ if (nl === -1)
202
+ break;
203
+ lineIdx++;
204
+ i = nextLineStart;
205
+ }
206
+ if (inFence)
207
+ ranges.push([fenceStart, text.length]);
208
+ let m;
209
+ const inlineCodeRe = /`[^`\n]+`/g;
210
+ while ((m = inlineCodeRe.exec(text)) !== null) {
211
+ ranges.push([m.index, m.index + m[0].length]);
212
+ }
213
+ const linkRe = /\[[^\]\n]*\]\(([^)\n]+)\)/g;
214
+ while ((m = linkRe.exec(text)) !== null) {
215
+ const parenOpen = m.index + m[0].lastIndexOf('(');
216
+ const parenClose = m.index + m[0].length - 1;
217
+ ranges.push([parenOpen + 1, parenClose]);
218
+ }
219
+ const autolinkRe = /<https?:\/\/[^>\s]+>/gi;
220
+ while ((m = autolinkRe.exec(text)) !== null) {
221
+ ranges.push([m.index, m.index + m[0].length]);
222
+ }
223
+ return mergeRanges(ranges);
224
+ }
225
+ function parseSuppressionSpecs(raw) {
226
+ const specs = raw.trim().split(/\s+/).filter(Boolean);
227
+ if (specs.length === 0)
228
+ return () => true;
229
+ const phraseSpecs = [];
230
+ const charSpecs = [];
231
+ for (const s of specs) {
232
+ if (s.startsWith('phrase:'))
233
+ phraseSpecs.push(s.slice('phrase:'.length));
234
+ else if (s.startsWith('char:'))
235
+ charSpecs.push(s.slice('char:'.length));
236
+ }
237
+ return (code, matchText, rulePattern) => {
238
+ if (code === 'phrase') {
239
+ return phraseSpecs.some(p => rulePattern === p);
240
+ }
241
+ return charSpecs.some(c => {
242
+ if (/^u\+/i.test(c)) {
243
+ const cp = parseInt(c.slice(2), 16);
244
+ return !Number.isNaN(cp) && matchText.codePointAt(0) === cp;
245
+ }
246
+ return matchText === c;
247
+ });
248
+ };
249
+ }
250
+ function computeSuppressions(text, excluded) {
251
+ const directiveRe = /<!--\s*slop-(disable-next-line|disable-line|disable|enable)\b([^>]*?)-->/gi;
252
+ const directives = [];
253
+ let m;
254
+ while ((m = directiveRe.exec(text)) !== null) {
255
+ if (offsetInRanges(m.index, excluded))
256
+ continue;
257
+ directives.push({
258
+ kind: m[1].toLowerCase(),
259
+ applies: parseSuppressionSpecs(m[2] || ''),
260
+ start: m.index,
261
+ end: m.index + m[0].length,
262
+ });
263
+ }
264
+ const result = [];
265
+ let blockStart = null;
266
+ let blockApplies = null;
267
+ for (const d of directives) {
268
+ if (d.kind === 'disable') {
269
+ if (blockStart === null) {
270
+ blockStart = d.end;
271
+ blockApplies = d.applies;
272
+ }
273
+ }
274
+ else if (d.kind === 'enable') {
275
+ if (blockStart !== null && blockApplies !== null) {
276
+ result.push({ start: blockStart, end: d.start, applies: blockApplies });
277
+ blockStart = null;
278
+ blockApplies = null;
279
+ }
280
+ }
281
+ else if (d.kind === 'disable-line') {
282
+ const lineStart = text.lastIndexOf('\n', d.start - 1) + 1;
283
+ const nl = text.indexOf('\n', d.end);
284
+ const lineEnd = nl === -1 ? text.length : nl;
285
+ result.push({ start: lineStart, end: lineEnd, applies: d.applies });
286
+ }
287
+ else if (d.kind === 'disable-next-line') {
288
+ const nl = text.indexOf('\n', d.end);
289
+ if (nl === -1)
290
+ continue;
291
+ const nextLineStart = nl + 1;
292
+ const nextNl = text.indexOf('\n', nextLineStart);
293
+ const nextLineEnd = nextNl === -1 ? text.length : nextNl;
294
+ result.push({ start: nextLineStart, end: nextLineEnd, applies: d.applies });
295
+ }
296
+ }
297
+ if (blockStart !== null && blockApplies !== null) {
298
+ result.push({ start: blockStart, end: text.length, applies: blockApplies });
299
+ }
300
+ return result;
301
+ }
302
+ function isSuppressed(offset, suppressions, code, matchText, rulePattern) {
303
+ for (const s of suppressions) {
304
+ if (offset >= s.start && offset < s.end && s.applies(code, matchText, rulePattern)) {
305
+ return true;
306
+ }
307
+ }
308
+ return false;
309
+ }
310
+ function offsetToLineCol(text, offset) {
311
+ let line = 1;
312
+ let lastNl = -1;
313
+ for (let i = 0; i < offset && i < text.length; i++) {
314
+ if (text.charCodeAt(i) === 10) {
315
+ line++;
316
+ lastNl = i;
317
+ }
318
+ }
319
+ return { line, col: offset - lastNl };
320
+ }
321
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SEVERITY_RANK = void 0;
4
+ exports.SEVERITY_RANK = {
5
+ error: 0,
6
+ warning: 1,
7
+ information: 2,
8
+ hint: 3,
9
+ };
10
+ //# sourceMappingURL=types.js.map