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.
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:` states frequencies, not quotations.** "24 hits in 1.02M words"
45
- is the form.
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 / format_compact
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": "rhetorical-tic",
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 -- 24 hits in 1.02M words across Austen, Melville, Madison, Thoreau, and Emerson combined. A careful writer occasionally stacks two (and, rarely, more), but a model reaches for the pattern constantly.",
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 an `info` flag (or deciding whether to) doesn't have to shell out to
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: "rhetorical-tic",
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
- - `rhetorical-tic` the cadence patterns (the user's list below)
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
- Severities: `warning` for strong tells, `info` for weak/contextual ones. No
269
- rule ships at `error` yet -- reserved for a pattern with essentially zero
270
- false-positive risk, which none has demonstrated.
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
- ## Rule catalog (v1)
356
+ ### puffery
273
357
 
274
- ### rhetorical-tic (from the request)
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
- - `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
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
- ### structure
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; `info`, because the corrective/concession distinction is syntactic and the pattern can only approximate it.
347
- - `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; `info`. 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.
348
- - `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); `info`. 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 info rather than being narrowed further.
349
- - `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.
350
- - `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 `info`.
351
- - `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; `info`, because people ask the same shape in conversation.
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."; `info`. 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
- - `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; `info`. 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.
354
- - `question-isnt` — "the question isn't/is not (whether|if|how|what|why|who) X, it's/but Y"; `info`. The resolving clause is required, so a plain rhetorical question never matches; "the real question is" is excluded.
355
- - `less-about-more-about` — "it's/this is/that's less about X (and) more about Y", also "… than about Y"; `info`. Both halves of the frame are required, and the subject slot is limited to the pronouns.
356
- - `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`.
357
- - `rule-of-three` — three parallel comma items ending a sentence (heuristic; `info` severity, off by default via `--select` since it false-positives).
358
- - `em-dash` — any em dash; `info`.
359
- - `em-dash-overuse` — 3+ em dashes in one paragraph; `warning`.
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
- ### hedging
394
+ ### reader-address
362
395
 
363
- - `vague-attribution` "some (critics/experts/observers) (argue/say/believe)", "it is widely (regarded/considered/seen)", "many would argue".
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 (`` ` ``), and
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, one-line
387
- description — and with `--json`, the machine version an agent can enumerate.
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
- parser.order!(argv)
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, suggestion: r.suggestion, default_on: r.default_on }
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.default_on ? "" : " [off by default]"
140
- out.puts("#{r.id.ljust(24)} #{r.category.ljust(14)} #{r.severity.ljust(8)} #{r.message}#{off}")
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.default_on ? "" : ", off by default"})
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. Default set excludes
192
- # default_on:false rules unless they are explicitly selected.
193
- def select_rules(select, ignore)
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(&:default_on)
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
 
@@ -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/URLs first.
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, rule: rule.id, category: rule.category,
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 whitespace so
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
- blank = lambda { |s| s.gsub(/[^\n]/, " ") }
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