sloplint 0.7.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/
@@ -180,17 +181,22 @@ path when multiple files are scanned).
180
181
  "line": 12,
181
182
  "column": 5,
182
183
  "severity": "warning",
184
+ "confidence": "high",
183
185
  "rule": "no-x-no-y",
184
- "category": "rhetorical-tic",
186
+ "category": "cadence",
185
187
  "message": "\"No X, no Y\" chain (3 items) reads as AI cadence.",
186
188
  "excerpt": "No fluff, no filler, no jargon",
187
189
  "context": "The report was blunt. [No fluff, no filler, no jargon]. Nothing held back at all.",
188
190
  "count": 3,
189
- "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.",
190
192
  "suggestion": "Cut the chain or make it one plain sentence."
191
193
  }
192
194
  ```
193
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.
194
200
  - `line`/`column` are 1-indexed, pointing at the start of the match.
195
201
  - `excerpt` is the bare match, nothing else. It is what `column` points at.
196
202
  - `context` is the match bracketed inside ~40 characters of surrounding prose,
@@ -203,7 +209,7 @@ path when multiple files are scanned).
203
209
  - `count` present when the rule counts items (the "badge" in the examples).
204
210
  - `rationale` is the same text `sloplint explain` prints under `Why:` — why the
205
211
  pattern reads as a tell. `check` carries it on every note so an agent acting
206
- 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
207
213
  `explain` first; that's the whole point of the field.
208
214
  - `suggestion` is a short fix hint; agents may use it, humans see it too.
209
215
 
@@ -214,18 +220,19 @@ built with `Data.define` (immutable value objects, Ruby 3.2+):
214
220
 
215
221
  ```ruby
216
222
  Rule = Data.define(
217
- :id, :category, :severity, :pattern, :message, :suggestion,
218
- :examples_bad, :examples_ok, :count_group, :skip
223
+ :id, :category, :severity, :confidence, :pattern, :message, :suggestion,
224
+ :examples_bad, :examples_ok, :count_group, :skip, :rationale
219
225
  ) do
220
226
  # sensible defaults for the optional fields
221
- def initialize(count_group: nil, skip: [], **rest) = super
227
+ def initialize(count_group: nil, skip: [], rationale: nil, **rest) = super
222
228
  end
223
229
 
224
230
  RULES = [
225
231
  Rule.new(
226
232
  id: "no-x-no-y",
227
- category: "rhetorical-tic",
228
- severity: "warning",
233
+ category: "cadence",
234
+ severity: "warning", # error, warning, info -- cost to the prose
235
+ confidence: "high", # high, medium, low -- false-positive risk
229
236
  pattern: /.../i, # regex literal; add /m if multiline
230
237
  message: "...", # may reference %{count}
231
238
  suggestion: "...",
@@ -259,114 +266,158 @@ rules to a file (non-devs editing them, third-party rule packs) isn't real yet.
259
266
  If it becomes real, the migration is cheap precisely because the rules are
260
267
  already pure data: write one loader, point it at a JSON dir, done.
261
268
 
262
- 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.
263
294
 
264
- - `rhetorical-tic` the cadence patterns (the user's list below)
265
- - `puffery` — Wikipedia "words to watch" (boasts, vibrant, nestled, tapestry…)
266
- - `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, the trailing restatement, em dash, em-dash overuse
267
- - `hedging` — vague attribution ("some critics argue", "it is widely regarded")
295
+ ## Rule catalog (v1)
268
296
 
269
- Severities: `warning` for strong tells, `info` for weak/contextual ones. No
270
- rule ships at `error` yet -- reserved for a pattern with essentially zero
271
- 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.
272
355
 
273
- ## Rule catalog (v1)
356
+ ### puffery
274
357
 
275
- ### rhetorical-tic (from the request)
276
-
277
- | id | catches | notes |
278
- |----|---------|-------|
279
- | `no-x-no-y` | 2+ comma-separated "no …" items in a row | counts items |
280
- | `no-x-no-y-frag` | the same cadence as sentence fragments ("No fluff. No filler.") | counts items; `info` |
281
- | `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 |
282
- | `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` |
283
- | `did-not-x-did-not-y` | 2+ "did not …"/"didn't …" in a row | counts items |
284
- | `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") |
285
- | `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 |
286
- | `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 |
287
- | `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` |
288
- | `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 |
289
- | `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` |
290
- | `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` |
291
- | `dont-verb-it` | "Don't call it X. Call it Y." (negated verb+it, same verb+it) | |
292
- | `sit-with-that` | "sit with that/this/it", "sit with the discomfort" | |
293
- | `hold-onto-that` | sentence-initial "hold onto/on to that/this" | imperative only |
294
- | `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 |
295
- | `clean-count` | "two/three clean parts/buckets/categories…" | needs a partition noun |
296
- | `cleanest-x` | "the cleanest framing/formulation", "cleanest way to put it" | noun list only |
297
- | `clean-x` | "a clean abstraction/distinction/framing", "clean line between" | `info` |
298
- | `you-already-know` | "you already know" (+ the answer / standalone) | |
299
- | `is-the-entire` | "X is the entire point/game/business model" | |
300
- | `the-entire-is` | "the entire point/game/… is" (flip of above) | |
301
- | `is-real-and-not` | "the X is real, and/not…", "is the real … and it" | skip "real estate/time"; `info` |
302
- | `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" |
303
- | `the-punchline-is` | "the punchline is/:/?", "the honest answer/version is" | "short version" left out; ordinary writing |
304
- | `worth-naming` | "worth naming/flagging/separating/spelling out" | skip "naming names"; yields to the rule below when a manner adverb follows; `info` |
305
- | `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 |
306
- | `not-nothing` | copula + "not nothing" litotes, any subject | skip personal/there subjects |
307
- | `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` |
308
- | `load-bearing` | "load-bearing" outside its construction sense | skip building nouns either side |
309
- | `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") |
310
- | `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 |
311
- | `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 |
312
- | `thats-how-x` | sentence-initial "that's how…" | |
313
- | `announced-takeaway` | colon-led label: "The pattern/lesson/takeaway…:" | sentence-initial |
314
- | `earns-its-place` | "earns its place/keep" (any possessive) | possessive required; `warning` |
315
- | `does-a-lot-of-work` | "does a lot of work here/in that sentence", "a lot of heavy lifting" | plain "the heavy lifting" excluded |
316
- | `failure-mode-here` | "the failure mode here is" | deictic required; bare "the failure mode is" excluded |
317
- | `thats-the-tension` | sentence-initial "that's the tension/bet" as a closer | noun must end the clause; "tradeoff"/"catch" excluded |
318
- | `right-up-until` | "right up until it doesn't/isn't/stops/breaks" | bare "until it doesn't" excluded |
319
- | `two-things-true` | "two/both things can be/are true" | count fixed at two |
320
- | `notice-what-there` | "notice what X did there", "read that again" | sentence-initial |
321
- | `notice-what` | bare sentence-initial "Notice what…" | yields the "there" frame to the rule above; "how" excluded; `info` |
322
- | `none-of-this-is-to-say` | "none of this/that/the above is to say" | every other "not to say" phrasing excluded |
323
- | `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 |
324
- | `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 |
325
- | `honest-x` | "an honest comparison", "the honest framing" | short noun list; "answer", "assessment", "account" excluded as ordinary; superlative yielded to the rule below |
326
- | `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 |
327
- | `genuinely` | any "genuinely" | off by default; no narrowing holds |
328
- | `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 |
329
- | `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` |
330
- | `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; `info` |
331
- | `and-thats-fine` | "And that's fine/okay." as a whole sentence | "and" required; must open and close the sentence |
332
- | `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 |
333
- | `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 |
334
- | `is-is` | doubled copula: "what it is is …", "the thing is, is that …" | comma optional |
335
-
336
- ### puffery (Wikipedia: Signs of AI writing)
337
-
338
- Single flat rule per word-cluster, matched as whole words:
358
+ Inflates the subject.
339
359
 
340
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.
341
- - `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
361
+ - `rich-tapestry` — "rich tapestry", "tapestry of".
342
362
  - `vital-role` — "plays a (vital/crucial/pivotal/significant/key) role".
363
+ - `stands-serves-as` — "stands as / serves as", "is a testament/reminder to".
343
364
  - `underscores-highlights` — "underscore(s)" + determiner and "underscored/underscoring" anywhere (the emphasis verb); "highlights/emphasizes its (importance/significance)" stays narrow.
344
- - `rich-tapestry` — "rich tapestry", "tapestry of".
345
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
346
368
 
347
- ### structure
369
+ ### false-correction
370
+
371
+ Corrects a reading nobody offered.
348
372
 
349
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.
350
- - `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.
351
- - `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.
352
- - `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.
353
- - `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.
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 `info`.
355
- - `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.
356
- - `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.
357
- - `ellipsis-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."; `info`. 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.
358
- - `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.
359
- - `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.
360
- - `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.
361
- - `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`.
362
- - `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. `info`.
363
- - `rule-of-three` — three single-word comma items ending a sentence (heuristic; `info` severity, off by default; runs under `--select` or `--strict` since it false-positives).
364
- - `em-dash` — any em dash; `info`.
365
- - `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
366
393
 
367
- ### hedging
394
+ ### reader-address
368
395
 
369
- - `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.
370
421
 
371
422
  ## Markdown handling
372
423
 
@@ -385,12 +436,18 @@ This is a first-class requirement, not an afterthought.
385
436
  # Recommended for agents:
386
437
  cat FILE | sloplint check --markdown -o json -
387
438
  # exit 0 = clean, 1 = notes found, >1 = error
388
- # 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)
389
441
  ```
390
442
 
391
443
  - Every option has a full-sentence help string (no telegraphic fragments).
392
- - `sloplint rules` prints the catalog: id, category, severity, one-line
393
- 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.
394
451
  - `sloplint explain no-x-no-y` prints the rule's message, rationale, a bad
395
452
  example and an ok (non-matching) example. Agents call this to decide whether a
396
453
  flag is worth acting on.
data/lib/sloplint/cli.rb CHANGED
@@ -139,14 +139,14 @@ module Sloplint
139
139
 
140
140
  if as_json
141
141
  payload = RULES.map do |r|
142
- { id: r.id, category: r.category, severity: r.severity,
143
- 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 }
144
144
  end
145
145
  out.puts(JSON.pretty_generate(payload))
146
146
  else
147
147
  RULES.each do |r|
148
- off = r.default_on ? "" : " [off by default]"
149
- 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}")
150
150
  end
151
151
  end
152
152
  0
@@ -166,7 +166,7 @@ module Sloplint
166
166
  return 2
167
167
  end
168
168
  out.puts(<<~TXT)
169
- #{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" : ""})
170
170
 
171
171
  #{rule.message}
172
172
 
@@ -197,15 +197,19 @@ module Sloplint
197
197
  refs - known
198
198
  end
199
199
 
200
- # --select/--ignore accept rule ids or category names. Default set excludes
201
- # default_on:false rules unless they are explicitly selected.
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.
202
205
  def select_rules(select, ignore, strict = false)
206
+ runs_by_default = ->(r) { r.confidence != "low" }
203
207
  rules = if select
204
- 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)) }
205
209
  elsif strict
206
210
  RULES
207
211
  else
208
- RULES.select(&:default_on)
212
+ RULES.select(&runs_by_default)
209
213
  end
210
214
  if ignore
211
215
  rules = rules.reject { |r| ignore.include?(r.id) || ignore.include?(r.category) }
@@ -221,7 +225,8 @@ module Sloplint
221
225
  # Recommended for agents:
222
226
  cat FILE | sloplint check --markdown -o json -
223
227
  # exit 0 = clean, 1 = notes found, >1 = error (empty input is an error)
224
- # 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)
225
230
 
226
231
  usage: sloplint [-o full|json] [command] [args]
227
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
 
@@ -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