sloplint 0.6.0 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -0
- data/README.md +31 -24
- data/docs/SPEC.md +170 -107
- data/lib/sloplint/cli.rb +30 -14
- data/lib/sloplint/engine.rb +10 -10
- data/lib/sloplint/rules.rb +699 -165
- data/lib/sloplint/version.rb +1 -1
- metadata +1 -1
data/docs/SPEC.md
CHANGED
|
@@ -41,8 +41,9 @@ so the source never needs to enter the repository.
|
|
|
41
41
|
- **`examples_ok` may quote real prose, public domain only**, with the source
|
|
42
42
|
named in a comment. The Moby-Dick and Federalist No. 44 fixtures are the model.
|
|
43
43
|
A common idiom or a title is not a quotation and needs no such treatment.
|
|
44
|
-
- **`rationale:`
|
|
45
|
-
|
|
44
|
+
- **`rationale:` says why the construct reads as AI-written, not how it was
|
|
45
|
+
tested.** Corpus sizes and hit counts belong in the commit message, not the
|
|
46
|
+
shipped text.
|
|
46
47
|
- **A reference corpus of human prose must be public domain.** Copyrighted text
|
|
47
48
|
can't be redistributed, so a corpus built from it can't live in the repo, and
|
|
48
49
|
neither can the false-positive check that depends on it.
|
|
@@ -107,7 +108,7 @@ sloplint/
|
|
|
107
108
|
lib/sloplint/cli.rb # optparse, subcommands, exit codes
|
|
108
109
|
lib/sloplint/rules.rb # RULES: array of Rule (Data) objects — the catalog
|
|
109
110
|
lib/sloplint/engine.rb # Engine.scan(text, rules:, config:) -> [Note]
|
|
110
|
-
lib/sloplint/output.rb # format_human / format_json
|
|
111
|
+
lib/sloplint/output.rb # format_human / format_json
|
|
111
112
|
docs/
|
|
112
113
|
SPEC.md # this file
|
|
113
114
|
spec/
|
|
@@ -138,9 +139,10 @@ global options:
|
|
|
138
139
|
|
|
139
140
|
check options:
|
|
140
141
|
paths ... files to scan; "-" or no paths reads stdin
|
|
141
|
-
--markdown skip fenced/inline code spans
|
|
142
|
+
--markdown skip fenced/inline code spans, HTML comments, and URLs
|
|
142
143
|
--select IDS only run these rules (comma-separated ids or categories)
|
|
143
144
|
--ignore IDS skip these rules
|
|
145
|
+
--strict run every rule, including the off-by-default ones
|
|
144
146
|
```
|
|
145
147
|
|
|
146
148
|
`check` is the default command. A first argument that is not a command name is
|
|
@@ -165,7 +167,7 @@ Three codes carry the contract. A crash just exits nonzero on its own.
|
|
|
165
167
|
|
|
166
168
|
Empty or whitespace-only input is exit 2, like a mistyped rule id: a scan of
|
|
167
169
|
nothing must not report as a clean scan. The text is tested before
|
|
168
|
-
`--markdown` blanks code and URLs, so a file that holds only a fenced code
|
|
170
|
+
`--markdown` blanks code, HTML comments, and URLs, so a file that holds only a fenced code
|
|
169
171
|
block still exits 0. Only when every source is empty.
|
|
170
172
|
|
|
171
173
|
## Note (the diagnostic object)
|
|
@@ -179,17 +181,22 @@ path when multiple files are scanned).
|
|
|
179
181
|
"line": 12,
|
|
180
182
|
"column": 5,
|
|
181
183
|
"severity": "warning",
|
|
184
|
+
"confidence": "high",
|
|
182
185
|
"rule": "no-x-no-y",
|
|
183
|
-
"category": "
|
|
186
|
+
"category": "cadence",
|
|
184
187
|
"message": "\"No X, no Y\" chain (3 items) reads as AI cadence.",
|
|
185
188
|
"excerpt": "No fluff, no filler, no jargon",
|
|
186
189
|
"context": "The report was blunt. [No fluff, no filler, no jargon]. Nothing held back at all.",
|
|
187
190
|
"count": 3,
|
|
188
|
-
"rationale": "Asyndetic negation chains are a signature model cadence, near-absent from human prose at any length
|
|
191
|
+
"rationale": "Asyndetic negation chains are a signature model cadence, near-absent from human prose at any length. A careful writer occasionally stacks two (and, rarely, more), but a model reaches for the pattern constantly.",
|
|
189
192
|
"suggestion": "Cut the chain or make it one plain sentence."
|
|
190
193
|
}
|
|
191
194
|
```
|
|
192
195
|
|
|
196
|
+
- `severity` is what the construct costs the prose (`error`, `warning`, `info`);
|
|
197
|
+
`confidence` is how likely the match is a false positive (`high`, `medium`,
|
|
198
|
+
`low`). Two axes, not one: a cheap tell we are sure about and an expensive
|
|
199
|
+
one we are guessing at no longer share a word.
|
|
193
200
|
- `line`/`column` are 1-indexed, pointing at the start of the match.
|
|
194
201
|
- `excerpt` is the bare match, nothing else. It is what `column` points at.
|
|
195
202
|
- `context` is the match bracketed inside ~40 characters of surrounding prose,
|
|
@@ -202,7 +209,7 @@ path when multiple files are scanned).
|
|
|
202
209
|
- `count` present when the rule counts items (the "badge" in the examples).
|
|
203
210
|
- `rationale` is the same text `sloplint explain` prints under `Why:` — why the
|
|
204
211
|
pattern reads as a tell. `check` carries it on every note so an agent acting
|
|
205
|
-
on
|
|
212
|
+
on a flag (or deciding whether to) doesn't have to shell out to
|
|
206
213
|
`explain` first; that's the whole point of the field.
|
|
207
214
|
- `suggestion` is a short fix hint; agents may use it, humans see it too.
|
|
208
215
|
|
|
@@ -213,18 +220,19 @@ built with `Data.define` (immutable value objects, Ruby 3.2+):
|
|
|
213
220
|
|
|
214
221
|
```ruby
|
|
215
222
|
Rule = Data.define(
|
|
216
|
-
:id, :category, :severity, :pattern, :message, :suggestion,
|
|
217
|
-
:examples_bad, :examples_ok, :count_group, :skip
|
|
223
|
+
:id, :category, :severity, :confidence, :pattern, :message, :suggestion,
|
|
224
|
+
:examples_bad, :examples_ok, :count_group, :skip, :rationale
|
|
218
225
|
) do
|
|
219
226
|
# sensible defaults for the optional fields
|
|
220
|
-
def initialize(count_group: nil, skip: [], **rest) = super
|
|
227
|
+
def initialize(count_group: nil, skip: [], rationale: nil, **rest) = super
|
|
221
228
|
end
|
|
222
229
|
|
|
223
230
|
RULES = [
|
|
224
231
|
Rule.new(
|
|
225
232
|
id: "no-x-no-y",
|
|
226
|
-
category: "
|
|
227
|
-
severity: "warning",
|
|
233
|
+
category: "cadence",
|
|
234
|
+
severity: "warning", # error, warning, info -- cost to the prose
|
|
235
|
+
confidence: "high", # high, medium, low -- false-positive risk
|
|
228
236
|
pattern: /.../i, # regex literal; add /m if multiline
|
|
229
237
|
message: "...", # may reference %{count}
|
|
230
238
|
suggestion: "...",
|
|
@@ -258,114 +266,163 @@ rules to a file (non-devs editing them, third-party rule packs) isn't real yet.
|
|
|
258
266
|
If it becomes real, the migration is cheap precisely because the rules are
|
|
259
267
|
already pure data: write one loader, point it at a JSON dir, done.
|
|
260
268
|
|
|
261
|
-
Categories (for `--select`/`--ignore` by group)
|
|
269
|
+
Categories (for `--select`/`--ignore` by group). Every category names the
|
|
270
|
+
rhetorical move the construct makes, so a new rule goes where its move goes:
|
|
271
|
+
|
|
272
|
+
- `self-rating` — the writer grades their own prose or claim
|
|
273
|
+
- `closer` — closes by restating or announcing the point
|
|
274
|
+
- `cadence` — rhythm: repetition, parallelism, and the long-then-short kicker
|
|
275
|
+
- `puffery` — inflates the subject
|
|
276
|
+
- `false-correction` — corrects a reading nobody offered
|
|
277
|
+
- `false-concession` — performs balance or candour and gives nothing up
|
|
278
|
+
- `reader-address` — instructs or flatters the reader
|
|
279
|
+
- `borrowed-metaphor` — an engineering term applied to an argument
|
|
280
|
+
- `punctuation` — the mark itself
|
|
281
|
+
|
|
282
|
+
Two ratings, answering two questions. **Severity** is what the construct
|
|
283
|
+
costs the prose: `error` when the sentence is worse for it in any register
|
|
284
|
+
(the puffery family, the tautology closers, the self-ranking superlatives),
|
|
285
|
+
`warning` when it dates the draft as model output but the sentence still
|
|
286
|
+
says something, `info` when it is mostly harmless and worth knowing (the em
|
|
287
|
+
dash). **Confidence** is how likely a match is a false positive: `high` when
|
|
288
|
+
almost every hit is the real tell, `medium` when ordinary prose makes the
|
|
289
|
+
same shape often enough that an agent should read the rationale first, `low`
|
|
290
|
+
when the pattern cannot separate the tell from the ordinary use at all. The
|
|
291
|
+
five `low` rules are the ones that stay out of the default run; `--strict`,
|
|
292
|
+
or naming one by its own id, turns them on. The catalog below tags each
|
|
293
|
+
rule's confidence, not its severity.
|
|
262
294
|
|
|
263
|
-
|
|
264
|
-
- `puffery` — Wikipedia "words to watch" (boasts, vibrant, nestled, tapestry…)
|
|
265
|
-
- `structure` — rule-of-three, "not just X but Y", "the question isn't X, it's Y", "less about X more about Y", the trailing significance participle, em dash, em-dash overuse
|
|
266
|
-
- `hedging` — vague attribution ("some critics argue", "it is widely regarded")
|
|
295
|
+
## Rule catalog (v1)
|
|
267
296
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
297
|
+
### self-rating
|
|
298
|
+
|
|
299
|
+
The writer grades their own prose or claim.
|
|
300
|
+
|
|
301
|
+
- `clean-x` — "a clean abstraction/distinction/framing", "clean line between"; `medium` confidence
|
|
302
|
+
- `clean-count` — "two/three clean parts/buckets/categories…"; needs a partition noun
|
|
303
|
+
- `cleanest-x` — "the cleanest framing/formulation", "cleanest way to put it"; noun list only
|
|
304
|
+
- `cleanly` — "cleanly" into/onto/in two/in half — the partition frame; the preposition is the narrowing; the engineering idiom ("applies cleanly", "separated cleanly", "cleanly compiled") is a checkable fact and stays out, and the clause-final form is left out. `medium` confidence: the frame is not the sense, and separating "splits cleanly into two parts" from "retracted cleanly into the well" needs the subject
|
|
305
|
+
- `honest-x` — "an honest comparison", "the honest framing"; short noun list; "answer", "assessment", "account" excluded as ordinary; superlative yielded to the rule below
|
|
306
|
+
- `most-honest-x` — "the most honest framing", "the most honest way to put it"; wider noun list than `honest-x`; no human nouns, so "the most honest person" stays out
|
|
307
|
+
- `honestly` — a word, then "honestly", then a full stop or comma; terminal position required; discourse-marker slot guarded ("and/quite/but honestly,"); no animacy test
|
|
308
|
+
- `worth-naming` — "worth naming/flagging/separating/spelling out"; skip "naming names"; yields to the rule below when a manner adverb follows; `medium` confidence
|
|
309
|
+
- `worth-saying-plainly` — "it's worth saying plainly / better put bluntly…", plus the bare "Put plainly," / "Said bluntly,"; sentence-initial; the bare branch drops "simply"/"clearly" so "put simply" and "simply put" stay clean
|
|
310
|
+
- `earns-its-place` — "earns its place/keep" (any possessive); possessive required; `high` confidence
|
|
311
|
+
- `does-a-lot-of-work` — "does a lot of work here/in that sentence", "a lot of heavy lifting"; plain "the heavy lifting" excluded
|
|
312
|
+
- `exact-exactly` — the reflexive intensifier: "that's exactly", "exactly right", "exactly the point", "the exact problem", "know exactly why"; matches the tell as a closed set of frames rather than matching the word and subtracting an allow-list; a measured quantity ("the exact diameter", "the exact CPU time") is silent by construction. `medium` confidence
|
|
313
|
+
- `genuinely` — any "genuinely"; `low` confidence, off by default; no narrowing holds
|
|
314
|
+
- `the-punchline-is` — "the punchline is/:/?", "the honest answer/version is"; "short version" left out; ordinary writing
|
|
315
|
+
- `announced-takeaway` — colon-led label: "The pattern/lesson/takeaway…:"; sentence-initial
|
|
316
|
+
|
|
317
|
+
### closer
|
|
318
|
+
|
|
319
|
+
Closes by restating or announcing the point.
|
|
320
|
+
|
|
321
|
+
- `thats-the-whole` — "that/this is the whole point/game/thing…"; also value and fix, the closers agents write in technical prose, when they end the sentence or the paragraph or run into a closer's tail word; either apostrophe; `is-the-whole-x` yields the same list
|
|
322
|
+
- `is-the-whole-x` — any subject + "is the whole/real/actual/entire N" (tell, point, test, work, …); opens on the subject word; yields only the exact sentences `thats-the-whole` and `is-the-entire` own; interrogative subjects out; "only", "deal", "thing", "cost" left out; `medium` confidence
|
|
323
|
+
- `is-the-entire` — "X is the entire point/game/business model"
|
|
324
|
+
- `the-entire-is` — "the entire point/game/… is" (flip of above)
|
|
325
|
+
- `thats-how-x` — sentence-initial "that's how…"
|
|
326
|
+
- `thats-the-tension` — sentence-initial "that's the tension/bet" as a closer; noun must end the clause; "tradeoff"/"catch" excluded
|
|
327
|
+
- `right-up-until` — "right up until it doesn't/isn't/stops/breaks"; bare "until it doesn't" excluded
|
|
328
|
+
- `and-nothing-else` — trailing ", and nothing else/more/further", ", and no more"; tail must close the sentence; comma required; bare "no more" needs "and"; "?" excluded
|
|
329
|
+
- `nothing-else-frag` — the same exclusion as a fragment: "Nothing else."; sentence-initial capital; semicolon excluded; must be the whole sentence; "No more." left out
|
|
330
|
+
- `bare-equative` — sentence-initial "The N (here) is (not) the …" with an abstract head noun; the-x-is-the-x's head list, so concrete heads are out; the copula (is, is not, isn't) must be followed by "the" and a lowercase word, so predicate adjectives, indefinites, pointing complements ("the same/one/first/…") and proper nouns are out; a list marker may open it; `medium` confidence
|
|
331
|
+
- `trailing-restatement` — comma plus "which means", "which is to say", or "meaning" opening on one of a closed set of determiners and pronouns; or one of four participle frames with a pronoun object and a closing word ("making it easier", "allowing us to", "giving them more", "leaving you with"). The closer on "making" is a comparative that ends the clause or leads into "to", "for" or "than". Nothing before the comma is inspected, so a gloss ("_ma_, which means hand") matches; the bare participles ("leaving the door open") never do. Off by default: the pattern sees the connective, not whether the tail restates the head. `low` confidence.
|
|
332
|
+
- `and-what-it-should` — ", and what it should." — a second "what" clause closing on a bare modal or a negated auxiliary; comma, conjunction and full stop required; the affirmative copula and do-verb ("what he does.") are complete clauses and out; a question is out
|
|
333
|
+
|
|
334
|
+
### cadence
|
|
335
|
+
|
|
336
|
+
Rhythm: repetition, parallelism, and the long-then-short kicker.
|
|
337
|
+
|
|
338
|
+
- `no-x-no-y` — 2+ comma-separated "no …" items in a row; counts items
|
|
339
|
+
- `no-x-no-y-frag` — the same cadence as sentence fragments ("No fluff. No filler."); counts items; `medium` confidence
|
|
340
|
+
- `did-not-x-did-not-y` — 2+ "did not …"/"didn't …" in a row; counts items
|
|
341
|
+
- `one-x-one-y` — 3+ comma-separated "one X" items standing on their own; counts items; must open a sentence or follow a colon, so a chain after a verb is counting; items letter-led; the distributive "one for …" skipped; a pair never flags
|
|
342
|
+
- `from-x-to-y-chain` — 2+ comma-separated "from X to Y" spans; counts spans; operands open with a letter, so ranges are out; skips the relay ("to B, from B") and the reduplication ("from X to X")
|
|
343
|
+
- `same-determiner-chain` — 3+ comma-separated items opening on the same determiner or quantifier (every, each, your, more, …); backreference; counts items; "one" and "no" left to their own rules; the narrative possessives (my, his, her, their, its) left out; items lowercase-led, so proper nouns are not a chain; `medium` confidence
|
|
344
|
+
- `real-x-real-y` — the same "real" used twice attributively in one sentence, in front of two different nouns: "real API calls … real credentials"; needs no noun list -- narrowed by requiring two attributive uses of the intensifier naming two different things; a hyphen on either side of "real" (real-time, non-real) takes it out of the running; a closed list drops the fixed senses (real time, real-world, real numbers/roots, real user (monitoring), real estate, real money) and the function words that continue a predicative "is real"
|
|
345
|
+
- `epistrophe` — two clauses ending on the same two-word phrase, the second closing the sentence; two backreferences, so the phrase may be hard-wrapped; no article-led phrase, second word 4+ letters, second clause 5–60 chars with no internal punctuation and capped whitespace; off by default; `low` confidence
|
|
346
|
+
- `phrase-echo` — the same three words again within about 400 words; backreference in a lookahead, so the match is the first occurrence; each word 4+ characters and lowercase-led, one of them 6+ letters with nothing but letters, so function-word runs, contractions, proper nouns and Title Case headings are out; a repeat that opens on a quote mark, backtick, emphasis marker or hyphen is out; the gap crosses paragraph breaks but not a list item, a table row or 80+ non-word characters; off by default; `low` confidence
|
|
347
|
+
- `is-is` — doubled copula: "what it is is …", "the thing is, is that …"; comma optional
|
|
348
|
+
- `the-x-is-the-x` — "the X … is the X …": the same abstract head noun on both sides of the copula; backreference; closed list of heads that cannot name an object (key, cost, unit are out); the clause between is capped at 50 chars and may not hold a comma, semicolon or colon; the second head must be followed by a preposition, determiner, quantifier, pronoun, plural noun or punctuation, so compounds are out; "isn't" counts
|
|
349
|
+
- `rule-of-three` — three single-word comma items ending a sentence (heuristic; `low` confidence, off by default; runs under `--select` or `--strict` since it false-positives).
|
|
350
|
+
- `everyone-nobody` — the comma-spliced antithesis on quantifier subjects: one clause opens on everyone/everybody, the other on nobody/no one/none or "one N", joined by a bare comma, the second closing the sentence ("Everyone wants the dashboard, nobody maintains it."). A conjunction, a period, or the same subject twice is not a hinge; "one of/by/per", a measure, a proper noun, "none of which" and "none louder than" are not second subjects; clauses are capped at eighty characters.
|
|
351
|
+
- `short-run` — three consecutive sentences of thirty characters or fewer, each letter-led and closing on a full stop, no quotation marks or digits, no lone-letter labels (either case, though a possessive is not one) or abbreviations, starting at a real sentence boundary (never on a wrap continuation), crossing a hard wrap but not a paragraph break; `medium` confidence. A list marker may open a run but never sit inside one, so consecutive bullets are a list — the marker set covers glyphs, the literal "o" plain-text documents use as a bullet, and numbered and lettered items. Two whole-run exclusions keep document furniture out, each a property of the run rather than of any sentence in it: three single-word sentences in a row is a citation line ("Natl. Inst. Stand. Technol."), and three lowercase-led sentences in a row is transcribed speech ("we all get gas. we go to divert to Albany."). One single-word sentence is the archetypal kicker and stays, and a run that reaches a capital anywhere is prose, so identifier-initial writing ("npm was slow. git blame helped. We moved on.") is untouched. One is a question; a draft that repeats it is the tell.
|
|
352
|
+
- `mic-drop-closer` — a sentence of 60+ characters, then a paragraph-final closer of two to eight words opening on a **quantifier** (Nothing, Most, None, Everything, Everyone, Nobody, Then, Neither, Both): "Nothing here needs a new login."; `medium` confidence. The bare demonstratives (That, This, It) were in the list and are out: procedural writing ends a step with one as a matter of course ("This completes the roughing operations."). A blank line or the end of the text must follow the closer; both sentences may be hard-wrapped; whitespace runs in the long sentence are capped so blanked Markdown cannot make it, and the long-sentence prefix is an atomic group so an unpunctuated stretch cannot send it into catastrophic backtracking. The note points at the closer. One means nothing; a draft where it repeats is the tell.
|
|
353
|
+
- `bare-auxiliary-closer` — the same long-sentence-then-short-closer shape as `mic-drop-closer`, but the tell sits in the verb rather than the subject: the closer's verb phrase is elided down to a bare auxiliary with no object, "The agent did."; `medium` confidence. No subject list is needed — a one-to-three word subject runs straight into a bare `did/does/do/was/were/is/are/had/has/can/could/would/will/should/might/must` (contracted forms included) and a period, and the closer must be the last thing in the paragraph, same as `mic-drop-closer`, so a closing quotation mark after the period excludes quoted dialogue the same way `short-run` excludes it. A negative lookahead drops a closer that still holds "what", "that", "which", "who", "why" or "how", since those introduce a subordinate clause supplying its own complement rather than an elided one ("Nobody knew who did." asks who did it). Reuses `mic-drop-closer`'s long-sentence prefix rather than a second copy of it. One means nothing; a draft where it repeats is the tell.
|
|
354
|
+
- `np-fragment-and` — a whole sentence made of two noun phrases and an "and", opening on A/An/One at a sentence start or after a list marker ("A named owner and a quarterly review."). One to three words a side, no auxiliary or modal anywhere (contractions included); a lexical verb is invisible, so "A car and a truck collided." flags, which is why it ships at `medium` confidence.
|
|
271
355
|
|
|
272
|
-
|
|
356
|
+
### puffery
|
|
273
357
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
| id | catches | notes |
|
|
277
|
-
|----|---------|-------|
|
|
278
|
-
| `no-x-no-y` | 2+ comma-separated "no …" items in a row | counts items |
|
|
279
|
-
| `no-x-no-y-frag` | the same cadence as sentence fragments ("No fluff. No filler.") | counts items; `info` |
|
|
280
|
-
| `thats-the-whole` | "that/this is the whole point/game/thing…" | also value and fix, the closers agents write in technical prose, when they end the sentence or the paragraph or run into a closer's tail word; either apostrophe; `is-the-whole-x` yields the same list |
|
|
281
|
-
| `is-the-whole-x` | any subject + "is the whole/real/actual/entire N" (tell, point, test, work, …) | opens on the subject word; yields only the exact sentences `thats-the-whole` and `is-the-entire` own; interrogative subjects out; "only", "deal", "thing", "cost" left out; `info` |
|
|
282
|
-
| `did-not-x-did-not-y` | 2+ "did not …"/"didn't …" in a row | counts items |
|
|
283
|
-
| `from-x-to-y-chain` | 2+ comma-separated "from X to Y" spans | counts spans; operands open with a letter, so ranges are out; skips the relay ("to B, from B") and the reduplication ("from X to X") |
|
|
284
|
-
| `one-x-one-y` | 3+ comma-separated "one X" items standing on their own | counts items; must open a sentence or follow a colon, so a chain after a verb is counting; items letter-led; the distributive "one for …" skipped; a pair never flags |
|
|
285
|
-
| `and-what-it-should` | ", and what it should." — a second "what" clause closing on a bare modal or a negated auxiliary | comma, conjunction and full stop required; the affirmative copula and do-verb ("what he does.") are complete clauses and out; a question is out |
|
|
286
|
-
| `abstract-lives-in` | an abstract noun that "lives/sits in/between/inside/…" | closed subject list; capitalised subjects skipped; "with" (responsibility), "at" (quantity), "lies in" and "resides in" left out; `info` |
|
|
287
|
-
| `the-x-is-the-x` | "the X … is the X …": the same abstract head noun on both sides of the copula | backreference; closed list of heads that cannot name an object (key, cost, unit are out); the clause between is capped at 50 chars and may not hold a comma, semicolon or colon; the second head must be followed by a preposition, determiner, quantifier, pronoun, plural noun or punctuation, so compounds are out; "isn't" counts |
|
|
288
|
-
| `same-determiner-chain` | 3+ comma-separated items opening on the same determiner or quantifier (every, each, your, more, …) | backreference; counts items; "one" and "no" left to their own rules; the narrative possessives (my, his, her, their, its) left out; items lowercase-led, so proper nouns are not a chain; `info` |
|
|
289
|
-
| `bare-equative` | sentence-initial "The N (here) is (not) the …" with an abstract head noun | the-x-is-the-x's head list, so concrete heads are out; the copula (is, is not, isn't) must be followed by "the" and a lowercase word, so predicate adjectives, indefinites, pointing complements ("the same/one/first/…") and proper nouns are out; a list marker may open it; `info` |
|
|
290
|
-
| `dont-verb-it` | "Don't call it X. Call it Y." (negated verb+it, same verb+it) | |
|
|
291
|
-
| `sit-with-that` | "sit with that/this/it", "sit with the discomfort" | |
|
|
292
|
-
| `hold-onto-that` | sentence-initial "hold onto/on to that/this" | imperative only |
|
|
293
|
-
| `cleanly` | "cleanly" into/onto/in two/in half — the partition frame | the preposition is the narrowing; the engineering idiom ("applies cleanly", "separated cleanly", "cleanly compiled") is a checkable fact and stays out, and the clause-final form is left out. `info`: the frame is not the sense, and separating "splits cleanly into two parts" from "retracted cleanly into the well" needs the subject |
|
|
294
|
-
| `clean-count` | "two/three clean parts/buckets/categories…" | needs a partition noun |
|
|
295
|
-
| `cleanest-x` | "the cleanest framing/formulation", "cleanest way to put it" | noun list only |
|
|
296
|
-
| `clean-x` | "a clean abstraction/distinction/framing", "clean line between" | `info` |
|
|
297
|
-
| `you-already-know` | "you already know" (+ the answer / standalone) | |
|
|
298
|
-
| `is-the-entire` | "X is the entire point/game/business model" | |
|
|
299
|
-
| `the-entire-is` | "the entire point/game/… is" (flip of above) | |
|
|
300
|
-
| `is-real-and-not` | "the X is real, and/not…", "is the real … and it" | skip "real estate/time"; `info` |
|
|
301
|
-
| `the-punchline-is` | "the punchline is/:/?", "the honest answer/version is" | "short version" left out; ordinary writing |
|
|
302
|
-
| `worth-naming` | "worth naming/flagging/separating/spelling out" | skip "naming names"; yields to the rule below when a manner adverb follows; `info` |
|
|
303
|
-
| `worth-saying-plainly` | "it's worth saying plainly / better put bluntly…", plus the bare "Put plainly," / "Said bluntly," | sentence-initial; the bare branch drops "simply"/"clearly" so "put simply" and "simply put" stay clean |
|
|
304
|
-
| `not-nothing` | copula + "not nothing" litotes, any subject | skip personal/there subjects |
|
|
305
|
-
| `exact-exactly` | the reflexive intensifier: "that's exactly", "exactly right", "exactly the point", "the exact problem", "know exactly why" | matches the tell as a closed set of frames rather than matching the word and subtracting an allow-list; a measured quantity ("the exact diameter", "the exact CPU time") is silent by construction. `info` |
|
|
306
|
-
| `load-bearing` | "load-bearing" outside its construction sense | skip building nouns either side |
|
|
307
|
-
| `intersection-of` | "the intersection of X and Y" as positioning | skip street corners, geometry, set arithmetic, airfield surfaces (runway, taxiway, apron), matrix rows and columns, and operands shaped like a US route designator ("US-27A") or a quadrant plus house number ("NE 140th Court") |
|
|
308
|
-
| `impact-verb` | "impact" used as a verb ("the outage impacted 4,000 accounts") | needs an auxiliary or subject pronoun for the base form; "to impact" requires a following object, so the preposition ("prior to impact") stays out; skip the medical and soil sense of "impacted", the struck object of a real collision ("impacted terrain"), the fixed compounds, and hyphenated forms |
|
|
309
|
-
| `impact-noun-bare` | "the impact of X", "measure the impact" — `info` | needs a measuring verb in front or "of" behind; skip the collision sense and the fixed compounds |
|
|
310
|
-
| `thats-how-x` | sentence-initial "that's how…" | |
|
|
311
|
-
| `announced-takeaway` | colon-led label: "The pattern/lesson/takeaway…:" | sentence-initial |
|
|
312
|
-
| `earns-its-place` | "earns its place/keep" (any possessive) | possessive required; `warning` |
|
|
313
|
-
| `does-a-lot-of-work` | "does a lot of work here/in that sentence", "a lot of heavy lifting" | plain "the heavy lifting" excluded |
|
|
314
|
-
| `failure-mode-here` | "the failure mode here is" | deictic required; bare "the failure mode is" excluded |
|
|
315
|
-
| `thats-the-tension` | sentence-initial "that's the tension/bet" as a closer | noun must end the clause; "tradeoff"/"catch" excluded |
|
|
316
|
-
| `right-up-until` | "right up until it doesn't/isn't/stops/breaks" | bare "until it doesn't" excluded |
|
|
317
|
-
| `two-things-true` | "two/both things can be/are true" | count fixed at two |
|
|
318
|
-
| `notice-what-there` | "notice what X did there", "read that again" | sentence-initial |
|
|
319
|
-
| `notice-what` | bare sentence-initial "Notice what…" | yields the "there" frame to the rule above; "how" excluded; `info` |
|
|
320
|
-
| `none-of-this-is-to-say` | "none of this/that/the above is to say" | every other "not to say" phrasing excluded |
|
|
321
|
-
| `if-im-being-honest` | "if I'm/we're (being) honest", "honestly, the answer/truth" | plain "to be honest" and "I'll be honest" excluded |
|
|
322
|
-
| `honestly` | a word, then "honestly", then a full stop or comma | terminal position required; discourse-marker slot guarded ("and/quite/but honestly,"); no animacy test |
|
|
323
|
-
| `honest-x` | "an honest comparison", "the honest framing" | short noun list; "answer", "assessment", "account" excluded as ordinary; superlative yielded to the rule below |
|
|
324
|
-
| `most-honest-x` | "the most honest framing", "the most honest way to put it" | wider noun list than `honest-x`; no human nouns, so "the most honest person" stays out |
|
|
325
|
-
| `genuinely` | any "genuinely" | off by default; no narrowing holds |
|
|
326
|
-
| `epistrophe` | two clauses ending on the same two-word phrase, the second closing the sentence | two backreferences, so the phrase may be hard-wrapped; no article-led phrase, second word 4+ letters, second clause 5–60 chars with no internal punctuation and capped whitespace; off by default; `info` |
|
|
327
|
-
| `and-thats-fine` | "And that's fine/okay." as a whole sentence | "and" required; must open and close the sentence |
|
|
328
|
-
| `and-nothing-else` | trailing ", and nothing else/more/further", ", and no more" | tail must close the sentence; comma required; bare "no more" needs "and"; "?" excluded |
|
|
329
|
-
| `nothing-else-frag` | the same exclusion as a fragment: "Nothing else." | sentence-initial capital; semicolon excluded; must be the whole sentence; "No more." left out |
|
|
330
|
-
| `is-is` | doubled copula: "what it is is …", "the thing is, is that …" | comma optional |
|
|
331
|
-
|
|
332
|
-
### puffery (Wikipedia: Signs of AI writing)
|
|
333
|
-
|
|
334
|
-
Single flat rule per word-cluster, matched as whole words:
|
|
358
|
+
Inflates the subject.
|
|
335
359
|
|
|
336
360
|
- `puffery-words` — boasts a, vibrant, rich (history/cultural/tapestry), nestled (gated to a following in/among/between, so the literal verb — a head nestled against a shoulder — doesn't count), in the heart of (gated to a place object), groundbreaking, renowned, diverse array, breathtaking, natural beauty, stands as a testament, indelible mark, deeply rooted.
|
|
337
|
-
- `
|
|
361
|
+
- `rich-tapestry` — "rich tapestry", "tapestry of".
|
|
338
362
|
- `vital-role` — "plays a (vital/crucial/pivotal/significant/key) role".
|
|
363
|
+
- `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
|
|
339
364
|
- `underscores-highlights` — "underscore(s)" + determiner and "underscored/underscoring" anywhere (the emphasis verb); "highlights/emphasizes its (importance/significance)" stays narrow.
|
|
340
|
-
- `rich-tapestry` — "rich tapestry", "tapestry of".
|
|
341
365
|
- `impact-noun-vague` — an intensity adjective plus "impact" (significant, real, meaningful, lasting, massive, huge, profound, big and the rest), or "make/have an impact". "positive" and "negative" stay out: they name a direction, which is more than the intensity words do. "statistically significant impact" is skipped — that one is a finding.
|
|
366
|
+
- `trailing-significance-participle` — comma plus a participle from a closed verb list (highlighting, showcasing, reinforcing, shaping, enhancing, cementing, solidifying, embodying, fostering, facilitating, signalling), the clause a model hangs off a sentence to say what a fact means. Guards drop gerund lists and "signalling to". `driving`, `representing`, `reflecting`, `marking`, `contributing`, `illustrating`, `demonstrating`, `emphasising`, `echoing` and `affirming` stay out: humans write them in the same position, usually with a person as the subject, and the pattern cannot see the subject. `underscoring` is left to `underscores-highlights`.
|
|
367
|
+
- `abstract-lives-in` — an abstract noun that "lives/sits in/between/inside/…"; closed subject list; capitalised subjects skipped; "with" (responsibility), "at" (quantity), "lies in" and "resides in" left out; `medium` confidence
|
|
342
368
|
|
|
343
|
-
###
|
|
369
|
+
### false-correction
|
|
370
|
+
|
|
371
|
+
Corrects a reading nobody offered.
|
|
344
372
|
|
|
345
373
|
- `not-just-x-but-y` — copula + "not just/only/merely/simply/solely X … but (also) Y", plus "not because X, but because Y". Requires the escalation word.
|
|
346
|
-
- `not-x-but-y` — the bare corrective "is not X but Y" with no escalation word; `
|
|
347
|
-
- `
|
|
348
|
-
- `
|
|
349
|
-
- `
|
|
350
|
-
- `
|
|
351
|
-
- `
|
|
352
|
-
- `
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
- `
|
|
359
|
-
- `
|
|
374
|
+
- `not-x-but-y` — the bare corrective "is not X but Y" with no escalation word; `medium` confidence, because the corrective/concession distinction is syntactic and the pattern can only approximate it.
|
|
375
|
+
- `not-by-x-but-by-y` — the corrective anchored on a repeated preposition rather than a copula: "not by A, but by B", "not from A but from B" (by, for, from, with, about, in, on, at, to, of, through, because of, out of); `medium` confidence. The B side must repeat the A preposition, A is capped at six words, and B may not open with a pronoun. Every corpus hit is the real shape written by a person, which is why it sits at `medium` confidence rather than being narrowed further.
|
|
376
|
+
- `isnt-x-its-y` — the corrective with no conjunction: a negated copula, then a second clause that supplies the replacement ("It isn't the tool. It's the habit."), joined by a period, semicolon, comma, or dash; `medium` confidence. Neither complement may open with a pronoun, possessive, preposition ("about" excepted), degree word, or predicate adjective, which keeps out ordinary two-part contrast; some hits are still the same shape written by a person.
|
|
377
|
+
- `question-isnt` — "the question isn't/is not (whether|if|how|what|why|who) X, it's/but Y"; `medium` confidence. The resolving clause is required, so a plain rhetorical question never matches; "the real question is" is excluded.
|
|
378
|
+
- `less-about-more-about` — "it's/this is/that's less about X (and) more about Y", also "… than about Y"; `medium` confidence. Both halves of the frame are required, and the subject slot is limited to the pronouns.
|
|
379
|
+
- `actually-not-x` — "actually" and a trailing "…, not X" in one clause; the comma before "not" must be the first comma of the clause and must follow a word, so a fronted setup ("Despite the name, …") and a parenthetical both drop it; never crosses a line break; bare "actually" and "not actually" both left out
|
|
380
|
+
- `dont-verb-it` — "Don't call it X. Call it Y." (negated verb+it, same verb+it)
|
|
381
|
+
|
|
382
|
+
### false-concession
|
|
383
|
+
|
|
384
|
+
Performs balance or candour and gives nothing up.
|
|
385
|
+
|
|
386
|
+
- `two-things-true` — "two/both things can be/are true"; count fixed at two
|
|
387
|
+
- `none-of-this-is-to-say` — "none of this/that/the above is to say"; every other "not to say" phrasing excluded
|
|
388
|
+
- `is-real-and-not` — "the X is real, and/not…", "is the real … and it"; skip "real estate/time"; `medium` confidence
|
|
389
|
+
- `not-nothing` — copula + "not nothing" litotes, any subject; skip personal/there subjects
|
|
390
|
+
- `vague-attribution` — "some (critics/experts/observers) (argue/say/believe)", "it is widely (regarded/considered/seen)", "many would argue".
|
|
391
|
+
- `if-im-being-honest` — "if I'm/we're (being) honest", "honestly, the answer/truth"; plain "to be honest" and "I'll be honest" excluded
|
|
392
|
+
- `and-thats-fine` — "And that's fine/okay." as a whole sentence; "and" required; must open and close the sentence
|
|
360
393
|
|
|
361
|
-
###
|
|
394
|
+
### reader-address
|
|
362
395
|
|
|
363
|
-
|
|
396
|
+
Instructs or flatters the reader.
|
|
397
|
+
|
|
398
|
+
- `you-already-know` — "you already know" (+ the answer / standalone)
|
|
399
|
+
- `sit-with-that` — "sit with that/this/it", "sit with the discomfort"
|
|
400
|
+
- `hold-onto-that` — sentence-initial "hold onto/on to that/this"; imperative only
|
|
401
|
+
- `notice-what` — bare sentence-initial "Notice what…"; yields the "there" frame to the rule above; "how" excluded; `medium` confidence
|
|
402
|
+
- `notice-what-there` — "notice what X did there", "read that again"; sentence-initial
|
|
403
|
+
- `quip-question` — the verbless opening question ("No invite?", "New to the tool?", "Still stuck?"): sentence-initial, one of a short list of opening words, one to four more words, no auxiliary or contraction, closing on the question mark; "Need" and "Want" are left out as elided verbs; `medium` confidence, because people ask the same shape in conversation.
|
|
404
|
+
|
|
405
|
+
### borrowed-metaphor
|
|
406
|
+
|
|
407
|
+
An engineering term applied to an argument.
|
|
408
|
+
|
|
409
|
+
- `load-bearing` — "load-bearing" outside its construction sense; skip building nouns either side
|
|
410
|
+
- `failure-mode-here` — "the failure mode here is"; deictic required; bare "the failure mode is" excluded
|
|
411
|
+
- `intersection-of` — "the intersection of X and Y" as positioning; skip street corners, geometry, set arithmetic, airfield surfaces (runway, taxiway, apron), matrix rows and columns, and operands shaped like a US route designator ("US-27A") or a quadrant plus house number ("NE 140th Court")
|
|
412
|
+
- `impact-verb` — "impact" used as a verb ("the outage impacted 4,000 accounts"); needs an auxiliary or subject pronoun for the base form; "to impact" requires a following object, so the preposition ("prior to impact") stays out; skip the medical and soil sense of "impacted", the struck object of a real collision ("impacted terrain"), the fixed compounds, and hyphenated forms
|
|
413
|
+
- `impact-noun-bare` — "the impact of X", "measure the impact" — `medium` confidence; needs a measuring verb in front or "of" behind; skip the collision sense and the fixed compounds
|
|
414
|
+
|
|
415
|
+
### punctuation
|
|
416
|
+
|
|
417
|
+
The mark itself.
|
|
418
|
+
|
|
419
|
+
- `em-dash` — any em dash; `medium` confidence.
|
|
420
|
+
- `em-dash-overuse` — 3+ em dashes in one paragraph; `high` confidence.
|
|
364
421
|
|
|
365
422
|
## Markdown handling
|
|
366
423
|
|
|
367
|
-
`--markdown` blanks out fenced code (```` ``` ````), inline code (`` ` ``),
|
|
368
|
-
URLs before scanning, replacing them with same-length whitespace so line/column
|
|
424
|
+
`--markdown` blanks out fenced code (```` ``` ````), inline code (`` ` ``), HTML
|
|
425
|
+
comments (`<!-- -->`), and URLs before scanning, replacing them with same-length whitespace so line/column
|
|
369
426
|
stay correct. Off by default (plain-text mode) so it never silently eats prose.
|
|
370
427
|
|
|
371
428
|
## Agent-first help text
|
|
@@ -379,12 +436,18 @@ This is a first-class requirement, not an afterthought.
|
|
|
379
436
|
# Recommended for agents:
|
|
380
437
|
cat FILE | sloplint check --markdown -o json -
|
|
381
438
|
# exit 0 = clean, 1 = notes found, >1 = error
|
|
382
|
-
# each note: {path,line,column,severity,rule,category,message,excerpt,context,rationale,suggestion}
|
|
439
|
+
# each note: {path,line,column,severity,confidence,rule,category,message,excerpt,context,rationale,suggestion,count}
|
|
440
|
+
# (count is present only for the rules that tally items)
|
|
383
441
|
```
|
|
384
442
|
|
|
385
443
|
- Every option has a full-sentence help string (no telegraphic fragments).
|
|
386
|
-
- `sloplint rules` prints the catalog: id, category, severity,
|
|
387
|
-
description — and with `--json`, the machine version an agent can
|
|
444
|
+
- `sloplint rules` prints the catalog: id, category, severity, confidence,
|
|
445
|
+
one-line description — and with `--json`, the machine version an agent can
|
|
446
|
+
enumerate, including each rule's `severity`, `confidence` and `rationale`.
|
|
447
|
+
- Naming a category in `check --select` runs only that category's non-low
|
|
448
|
+
rules; naming a rule's own id runs it regardless. `--strict` alone runs the
|
|
449
|
+
whole catalog, low-confidence rules included; with `--select` it only widens
|
|
450
|
+
the named categories to include their low-confidence members.
|
|
388
451
|
- `sloplint explain no-x-no-y` prints the rule's message, rationale, a bad
|
|
389
452
|
example and an ok (non-matching) example. Agents call this to decide whether a
|
|
390
453
|
flag is worth acting on.
|
data/lib/sloplint/cli.rb
CHANGED
|
@@ -19,7 +19,14 @@ module Sloplint
|
|
|
19
19
|
opts = { format: "full" }
|
|
20
20
|
parser = global_parser(opts, out:)
|
|
21
21
|
# Split global options from the subcommand and its args.
|
|
22
|
-
|
|
22
|
+
begin
|
|
23
|
+
parser.order!(argv)
|
|
24
|
+
rescue OptionParser::InvalidOption => e
|
|
25
|
+
# `check` is the default command, so its options are accepted before
|
|
26
|
+
# any command word: `sloplint --markdown -`. The global parser does not
|
|
27
|
+
# know them, so put the option back and let check's parser judge it.
|
|
28
|
+
argv.unshift("check", *e.args)
|
|
29
|
+
end
|
|
23
30
|
return 0 if opts[:help_shown] || opts[:version_shown]
|
|
24
31
|
|
|
25
32
|
command = argv.shift
|
|
@@ -46,15 +53,17 @@ module Sloplint
|
|
|
46
53
|
# ── check ───────────────────────────────────────────────────────────────
|
|
47
54
|
def cmd_check(argv, opts, out:, err:, stdin:)
|
|
48
55
|
markdown = false
|
|
56
|
+
strict = false
|
|
49
57
|
select = nil
|
|
50
58
|
ignore = nil
|
|
51
59
|
p = OptionParser.new do |o|
|
|
52
60
|
o.banner = "usage: sloplint check [options] [paths...] (\"-\" or no paths = stdin)"
|
|
53
61
|
o.on("-o", "--output-format FORMAT", %w[full json],
|
|
54
62
|
"Output format: 'full' or 'json' (may also be given before the command).") { |v| opts[:format] = v }
|
|
55
|
-
o.on("--markdown", "Skip fenced/inline code spans and URLs before scanning.") { markdown = true }
|
|
63
|
+
o.on("--markdown", "Skip fenced/inline code spans, HTML comments, and URLs before scanning.") { markdown = true }
|
|
56
64
|
o.on("--select IDS", "Only run these rules (comma-separated rule ids or category names).") { |v| select = v.split(",").map(&:strip) }
|
|
57
65
|
o.on("--ignore IDS", "Skip these rules (comma-separated rule ids or category names).") { |v| ignore = v.split(",").map(&:strip) }
|
|
66
|
+
o.on("--strict", "Run every rule, including the ones that are off by default.") { strict = true }
|
|
58
67
|
end
|
|
59
68
|
p.order!(argv)
|
|
60
69
|
|
|
@@ -65,7 +74,7 @@ module Sloplint
|
|
|
65
74
|
return 2
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
rules = select_rules(select, ignore)
|
|
77
|
+
rules = select_rules(select, ignore, strict)
|
|
69
78
|
paths = argv.empty? ? ["-"] : argv
|
|
70
79
|
by_path = paths.reject { |x| x == "-" }.size > 1
|
|
71
80
|
|
|
@@ -130,14 +139,14 @@ module Sloplint
|
|
|
130
139
|
|
|
131
140
|
if as_json
|
|
132
141
|
payload = RULES.map do |r|
|
|
133
|
-
{ id: r.id, category: r.category, severity: r.severity,
|
|
134
|
-
message: r.message,
|
|
142
|
+
{ id: r.id, category: r.category, severity: r.severity, confidence: r.confidence,
|
|
143
|
+
message: r.message, rationale: r.rationale, suggestion: r.suggestion }
|
|
135
144
|
end
|
|
136
145
|
out.puts(JSON.pretty_generate(payload))
|
|
137
146
|
else
|
|
138
147
|
RULES.each do |r|
|
|
139
|
-
off = r.
|
|
140
|
-
out.puts("#{r.id.ljust(24)} #{r.category.ljust(
|
|
148
|
+
off = r.confidence == "low" ? " [off by default]" : ""
|
|
149
|
+
out.puts("#{r.id.ljust(24)} #{r.category.ljust(18)} #{r.severity.ljust(8)} #{r.confidence.ljust(7)} #{r.message}#{off}")
|
|
141
150
|
end
|
|
142
151
|
end
|
|
143
152
|
0
|
|
@@ -157,7 +166,7 @@ module Sloplint
|
|
|
157
166
|
return 2
|
|
158
167
|
end
|
|
159
168
|
out.puts(<<~TXT)
|
|
160
|
-
#{rule.id} (#{rule.category}, #{rule.severity}#{rule.
|
|
169
|
+
#{rule.id} (#{rule.category}, #{rule.severity}, #{rule.confidence} confidence#{rule.confidence == "low" ? ", off by default" : ""})
|
|
161
170
|
|
|
162
171
|
#{rule.message}
|
|
163
172
|
|
|
@@ -188,13 +197,19 @@ module Sloplint
|
|
|
188
197
|
refs - known
|
|
189
198
|
end
|
|
190
199
|
|
|
191
|
-
# --select/--ignore accept rule ids or category names.
|
|
192
|
-
#
|
|
193
|
-
|
|
200
|
+
# --select/--ignore accept rule ids or category names. The default set
|
|
201
|
+
# excludes low-confidence rules unless they are explicitly selected. A
|
|
202
|
+
# category ref selects only that category's non-low rules unless --strict
|
|
203
|
+
# is set; naming a rule by its own id still selects it whatever its
|
|
204
|
+
# confidence.
|
|
205
|
+
def select_rules(select, ignore, strict = false)
|
|
206
|
+
runs_by_default = ->(r) { r.confidence != "low" }
|
|
194
207
|
rules = if select
|
|
195
|
-
RULES.select { |r| select.include?(r.id) || select.include?(r.category) }
|
|
208
|
+
RULES.select { |r| select.include?(r.id) || (select.include?(r.category) && (runs_by_default.call(r) || strict)) }
|
|
209
|
+
elsif strict
|
|
210
|
+
RULES
|
|
196
211
|
else
|
|
197
|
-
RULES.select(
|
|
212
|
+
RULES.select(&runs_by_default)
|
|
198
213
|
end
|
|
199
214
|
if ignore
|
|
200
215
|
rules = rules.reject { |r| ignore.include?(r.id) || ignore.include?(r.category) }
|
|
@@ -210,7 +225,8 @@ module Sloplint
|
|
|
210
225
|
# Recommended for agents:
|
|
211
226
|
cat FILE | sloplint check --markdown -o json -
|
|
212
227
|
# exit 0 = clean, 1 = notes found, >1 = error (empty input is an error)
|
|
213
|
-
# each note: {path,line,column,severity,rule,category,message,excerpt,context,rationale,suggestion}
|
|
228
|
+
# each note: {path,line,column,severity,confidence,rule,category,message,excerpt,context,rationale,suggestion,count}
|
|
229
|
+
# (count is present only for the rules that tally items)
|
|
214
230
|
|
|
215
231
|
usage: sloplint [-o full|json] [command] [args]
|
|
216
232
|
|
data/lib/sloplint/engine.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative "rules"
|
|
|
5
5
|
module Sloplint
|
|
6
6
|
# One match = one Note. See docs/SPEC.md "Note".
|
|
7
7
|
Note = Data.define(
|
|
8
|
-
:path, :line, :column, :severity, :rule, :category,
|
|
8
|
+
:path, :line, :column, :severity, :confidence, :rule, :category,
|
|
9
9
|
:message, :excerpt, :context, :count, :rationale, :suggestion
|
|
10
10
|
)
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ module Sloplint
|
|
|
14
14
|
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
|
-
# text: the source. rules: which Rule objects to run. markdown: blank code
|
|
17
|
+
# text: the source. rules: which Rule objects to run. markdown: blank code, HTML comments and URLs first.
|
|
18
18
|
# path: label carried into each Note (e.g. filename or "-" for stdin).
|
|
19
19
|
def scan(text, rules: RULES, markdown: false, path: "-")
|
|
20
20
|
source = text
|
|
@@ -32,7 +32,8 @@ module Sloplint
|
|
|
32
32
|
message = count ? rule.message % { count: count } : rule.message
|
|
33
33
|
notes << Note.new(
|
|
34
34
|
path: path, line: line, column: column,
|
|
35
|
-
severity: rule.severity,
|
|
35
|
+
severity: rule.severity, confidence: rule.confidence,
|
|
36
|
+
rule: rule.id, category: rule.category,
|
|
36
37
|
message: message, excerpt: matched.gsub(/\s+/, " ").strip,
|
|
37
38
|
context: context_for(source, m),
|
|
38
39
|
count: count, rationale: rule.rationale, suggestion: rule.suggestion
|
|
@@ -95,14 +96,13 @@ module Sloplint
|
|
|
95
96
|
starts
|
|
96
97
|
end
|
|
97
98
|
|
|
98
|
-
# Replace fenced code, inline code, and URLs with same-length
|
|
99
|
-
# line/column stay correct. Newlines are preserved.
|
|
99
|
+
# Replace fenced code, HTML comments, inline code, and URLs with same-length
|
|
100
|
+
# whitespace so line/column stay correct. Newlines are preserved. One pass
|
|
101
|
+
# with one alternation, so whichever construct opens first is the one that
|
|
102
|
+
# gets consumed: a `<!--` quoted inside backticks is inline code, and a
|
|
103
|
+
# backtick inside a comment is part of the comment.
|
|
100
104
|
def blank_markdown(text)
|
|
101
|
-
|
|
102
|
-
text
|
|
103
|
-
.gsub(/```.*?```/m) { |s| blank.call(s) } # fenced code
|
|
104
|
-
.gsub(/`[^`\n]*`/) { |s| blank.call(s) } # inline code
|
|
105
|
-
.gsub(%r{https?://\S+}) { |s| blank.call(s) } # bare URLs
|
|
105
|
+
text.gsub(/```.*?```|<!--.*?-->|`[^`\n]*`|https?:\/\/\S+/m) { |s| s.gsub(/[^\n]/, " ") }
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
end
|