unslop-ci 0.1.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,289 @@
1
+ type Severity = 'high' | 'medium' | 'low';
2
+ /**
3
+ * The orthogonal axis to severity. "bug" means the flagged code is wrong (it
4
+ * eats a failure or ships unfinished) and gets fixed on those grounds alone;
5
+ * "cosmetic" means it is the model's chat voice leaking into the file. All
6
+ * text and UI rules are cosmetic; only the code scanner carries bug-class
7
+ * rules.
8
+ */
9
+ type RuleClass = 'bug' | 'cosmetic';
10
+ type ScannerName = 'code' | 'text' | 'ui';
11
+ interface Rule {
12
+ id: string;
13
+ label: string;
14
+ severity: Severity;
15
+ class: RuleClass;
16
+ /** The share of the underlying Reddit study this tell carries, kept verbatim. */
17
+ share?: string;
18
+ fix: string;
19
+ patterns: RegExp[];
20
+ /** Rules compiled case-sensitively (verbose-naming keys on camelCase). */
21
+ caseSensitive?: boolean;
22
+ /**
23
+ * Text scanner only: flagged even inside quotes/backticks/blockquotes
24
+ * (the em dash rule — the rule is simply not to ship one).
25
+ */
26
+ raw?: boolean;
27
+ /** UI scanner only: a line matching this is not flagged by the rule. */
28
+ suppress?: RegExp;
29
+ }
30
+ interface ScannerDef {
31
+ name: ScannerName;
32
+ title: string;
33
+ exts: Set<string>;
34
+ skipDirs: Set<string>;
35
+ rules: Rule[];
36
+ /** Skip .min.js/.min.css/.map files and single-line files > 5000 chars. */
37
+ skipMinified: boolean;
38
+ /** Use the prose pipeline (frontmatter, fences, quote stripping, word counts). */
39
+ prose: boolean;
40
+ }
41
+ interface Finding {
42
+ scanner: ScannerName;
43
+ rule: string;
44
+ label: string;
45
+ severity: Severity;
46
+ class: RuleClass;
47
+ share?: string;
48
+ fix: string;
49
+ /** Repo-relative posix path. */
50
+ file: string;
51
+ /** 1-based line number. */
52
+ line: number;
53
+ match: string;
54
+ snippet: string;
55
+ /** True when this finding fails the gate. Filled in by the gate. */
56
+ blocking: boolean;
57
+ }
58
+ type FailOn = 'high+bugs' | 'high' | 'medium' | 'any';
59
+ interface ScannerReport {
60
+ scanner: ScannerName;
61
+ filesScanned: number;
62
+ counts: Record<Severity, number>;
63
+ classCounts: Record<RuleClass, number>;
64
+ score: number;
65
+ verdict: string;
66
+ /** Text scanner only: words considered (added lines in diff mode). */
67
+ words?: number;
68
+ /** Text scanner only: weighted score per 1,000 words. */
69
+ densityPer1kWords?: number;
70
+ }
71
+ interface Report {
72
+ mode: 'diff' | 'scan';
73
+ base?: string;
74
+ head?: string;
75
+ failOn: FailOn;
76
+ filesScanned: number;
77
+ /** Diff mode only: total added lines considered. */
78
+ addedLines?: number;
79
+ findings: Finding[];
80
+ blockingCount: number;
81
+ scanners: ScannerReport[];
82
+ }
83
+
84
+ interface ScannerConfig {
85
+ /** When set, only paths matching one of these globs are scanned. */
86
+ include?: string[];
87
+ /** Paths matching one of these globs are never scanned. */
88
+ exclude?: string[];
89
+ /** Rule ids disabled for this scanner. */
90
+ disableRules?: string[];
91
+ }
92
+ interface UnslopConfig {
93
+ /** What blocks the build. Default: 'high+bugs'. */
94
+ failOn?: FailOn;
95
+ /** Global path excludes, applied to every scanner. */
96
+ exclude?: string[];
97
+ /** Rule ids disabled everywhere. */
98
+ disableRules?: string[];
99
+ /** Per-rule severity override, or 'off' to disable. */
100
+ rules?: Record<string, Severity | 'off'>;
101
+ /** Per-scanner settings; set a scanner to false to disable it entirely. */
102
+ scanners?: Partial<Record<ScannerName, ScannerConfig | false>>;
103
+ }
104
+ declare const DEFAULT_CONFIG_FILENAME = "unslop.config.json";
105
+ declare class ConfigError extends Error {
106
+ }
107
+ declare function validateConfig(raw: unknown, source: string): UnslopConfig;
108
+ /**
109
+ * Load config from an explicit path, or from unslop.config.json in cwd when
110
+ * present. An explicit path that does not exist is an error; a missing
111
+ * default file just means defaults.
112
+ */
113
+ declare function loadConfig(cwd: string, explicitPath?: string): UnslopConfig;
114
+
115
+ declare const ALL_SCANNERS: ScannerDef[];
116
+ interface RunOptions {
117
+ cwd: string;
118
+ mode: 'diff' | 'scan';
119
+ /** Diff mode: the ref to diff against (required). */
120
+ base?: string;
121
+ /** Diff mode: the ref whose content is scanned; omit for the working tree. */
122
+ head?: string;
123
+ /** Diff mode: use merge-base (three-dot) semantics. Default true. */
124
+ mergeBase?: boolean;
125
+ /** Scan mode: the path to walk. Defaults to cwd. */
126
+ scanPath?: string;
127
+ configPath?: string;
128
+ /** Pre-loaded config; skips file loading when provided. */
129
+ config?: UnslopConfig;
130
+ /** Overrides config failOn. */
131
+ failOn?: FailOn;
132
+ /** Restrict to a subset of scanners. */
133
+ scanners?: ScannerName[];
134
+ }
135
+ /** Apply config rule overrides/disables to a scanner definition. */
136
+ declare function effectiveRules(def: ScannerDef, cfg: UnslopConfig): Rule[];
137
+ declare function run(opts: RunOptions): Report;
138
+
139
+ interface RawFinding {
140
+ rule: string;
141
+ label: string;
142
+ severity: Severity;
143
+ class: Finding['class'];
144
+ share?: string;
145
+ fix: string;
146
+ line: number;
147
+ match: string;
148
+ snippet: string;
149
+ }
150
+ interface ScanResult {
151
+ findings: RawFinding[];
152
+ /** Prose scanners only: word count per 1-based line (index 0 unused). */
153
+ lineWords?: number[];
154
+ }
155
+ /** True when the file looks minified: a single line longer than 5000 chars. */
156
+ declare function looksMinified(lines: string[]): boolean;
157
+ interface NoiseState {
158
+ inCode: boolean;
159
+ inQuote: boolean;
160
+ }
161
+ /**
162
+ * Blank what the author is quoting or showing as a literal example, so the
163
+ * prose rules lint the author's own sentences. Inline-code spans (backticks)
164
+ * and double-quoted spans are removed, and the open/closed state is carried
165
+ * across lines so a span that wraps onto the next line is still skipped.
166
+ * State is reset at every blank line by the caller, so an unbalanced quote
167
+ * can never swallow more than one paragraph.
168
+ */
169
+ declare function stripNoise(line: string, state: NoiseState): string;
170
+ /** Scan a file's content with one scanner. Content is split on newlines. */
171
+ declare function scanContent(scanner: ScannerDef, content: string): ScanResult;
172
+
173
+ interface DiffOptions {
174
+ base: string;
175
+ /** When omitted, the diff is taken against the working tree. */
176
+ head?: string;
177
+ cwd: string;
178
+ /**
179
+ * Diff from the merge-base of base and head (three-dot semantics), so only
180
+ * lines the branch itself added are considered. Defaults to true.
181
+ */
182
+ mergeBase?: boolean;
183
+ }
184
+ declare class GitError extends Error {
185
+ }
186
+ /**
187
+ * Unquote a path from a `+++ b/...` header. Git C-quotes paths containing
188
+ * special characters; handle the common escapes and octal bytes.
189
+ */
190
+ declare function unquoteGitPath(raw: string): string;
191
+ /**
192
+ * Parse `git diff -U0` output into a map of file path (repo-relative posix)
193
+ * to the set of 1-based line numbers that the diff ADDS. With zero context
194
+ * every `@@ -a,b +c,d @@` hunk contributes exactly the range c..c+d-1.
195
+ */
196
+ declare function parseAddedLines(diffText: string): Map<string, Set<number>>;
197
+ /** Compute the added-line map for a base..head (or base..worktree) diff. */
198
+ declare function getAddedLines(opts: DiffOptions): Map<string, Set<number>>;
199
+
200
+ /**
201
+ * Whether a finding fails the build.
202
+ *
203
+ * - 'high' only high-severity findings block
204
+ * - 'high+bugs' high-severity findings block, plus bug-class findings of any
205
+ * severity (a swallowed error is quiet but wrong) — the default
206
+ * - 'medium' high and medium block, plus bug-class findings
207
+ * - 'any' every finding blocks
208
+ */
209
+ declare function isBlocking(finding: Pick<Finding, 'severity' | 'class'>, failOn: FailOn): boolean;
210
+
211
+ /**
212
+ * A small glob matcher for repo-relative posix paths. Supported syntax:
213
+ *
214
+ * - `*` any run of characters within one path segment
215
+ * - `?` one character within a segment
216
+ * - `**` any number of whole segments (including zero)
217
+ * - `{a,b}` alternation (no nesting)
218
+ * - a trailing `/` matches the directory and everything under it
219
+ * - a pattern with no `/` matches against the basename as well as the path
220
+ */
221
+ declare function globToRegExp(glob: string): RegExp;
222
+ declare class GlobSet {
223
+ private readonly regexes;
224
+ private readonly basenames;
225
+ constructor(globs: string[]);
226
+ matches(path: string): boolean;
227
+ get isEmpty(): boolean;
228
+ }
229
+
230
+ declare function countBySeverity(findings: Pick<Finding, 'severity'>[]): Record<Severity, number>;
231
+ declare function weightedScore(counts: Record<Severity, number>): number;
232
+ /** Weighted slop score per 1,000 words; concentration is the real signal. */
233
+ declare function densityPer1kWords(weighted: number, words: number): number;
234
+ /** Ported per-scanner verdict logic from the upstream scanners. */
235
+ declare function verdict(scanner: ScannerName, counts: Record<Severity, number>, weighted: number, words?: number): string;
236
+
237
+ interface ConsoleReportOptions {
238
+ color: boolean;
239
+ /** Max examples printed per non-blocking rule group. */
240
+ maxExamples: number;
241
+ }
242
+ declare function renderConsoleReport(report: Report, opts: ConsoleReportOptions): string;
243
+
244
+ /** Render workflow-command annotations for the findings. */
245
+ declare function renderAnnotations(report: Report): string[];
246
+ /** Render the markdown step summary. */
247
+ declare function renderStepSummary(report: Report): string;
248
+ /** Append the step summary when running inside GitHub Actions. */
249
+ declare function writeStepSummary(report: Report): boolean;
250
+
251
+ /**
252
+ * In GitHub Actions, work out the base ref of the event when no explicit
253
+ * base was given: the PR base sha for pull_request events, the merge-group
254
+ * base for merge queues, the pre-push sha for pushes, then the base branch
255
+ * name as a last resort.
256
+ */
257
+ declare function resolveBaseFromEnv(env: NodeJS.ProcessEnv): string | undefined;
258
+
259
+ /**
260
+ * Ported verbatim from unslop_code_scan.py (vibecoded-design-tells, MIT).
261
+ * Severity follows the verified shares from the Reddit study; the two
262
+ * conclusive paste-in artifacts are HIGH because when they survive into
263
+ * committed code they are unmistakable. Class is the orthogonal axis:
264
+ * "bug" means the code is wrong, "cosmetic" means chat voice leaked in.
265
+ * The biggest bug of all (hallucinated APIs) is structural and cannot be
266
+ * caught by regex — build, type-check, and run the code for that.
267
+ */
268
+ declare const CODE_RULES: Rule[];
269
+ declare const codeScanner: ScannerDef;
270
+
271
+ /**
272
+ * Ported verbatim from unslop_text_scan.py (vibecoded-design-tells, MIT).
273
+ * Severity tiers follow the cited/keyword shares from the study:
274
+ * HIGH = the strongest cited tells, down to LOW = real but minor.
275
+ * All text rules are cosmetic-class: prose is never a bug.
276
+ */
277
+ declare const TEXT_RULES: Rule[];
278
+ declare const textScanner: ScannerDef;
279
+
280
+ /**
281
+ * Ported verbatim from devibe_scan.py (vibecoded-design-tells, MIT).
282
+ * Severity follows how often each tell is named in the underlying study.
283
+ * Patterns are kept specific enough to avoid drowning the report in false
284
+ * positives. All UI rules are cosmetic-class.
285
+ */
286
+ declare const UI_RULES: Rule[];
287
+ declare const uiScanner: ScannerDef;
288
+
289
+ export { ALL_SCANNERS, CODE_RULES, ConfigError, DEFAULT_CONFIG_FILENAME, type FailOn, type Finding, GitError, GlobSet, type Report, type Rule, type RuleClass, type RunOptions, type ScannerConfig, type ScannerDef, type ScannerName, type ScannerReport, type Severity, TEXT_RULES, UI_RULES, type UnslopConfig, codeScanner, countBySeverity, densityPer1kWords, effectiveRules, getAddedLines, globToRegExp, isBlocking, loadConfig, looksMinified, parseAddedLines, renderAnnotations, renderConsoleReport, renderStepSummary, resolveBaseFromEnv, run, scanContent, stripNoise, textScanner, uiScanner, unquoteGitPath, validateConfig, verdict, weightedScore, writeStepSummary };