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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +27 -21
- data/docs/SPEC.md +165 -108
- data/lib/sloplint/cli.rb +15 -10
- data/lib/sloplint/engine.rb +3 -2
- data/lib/sloplint/rules.rb +239 -164
- data/lib/sloplint/version.rb +1 -1
- metadata +1 -1
data/docs/SPEC.md
CHANGED
|
@@ -41,8 +41,9 @@ so the source never needs to enter the repository.
|
|
|
41
41
|
- **`examples_ok` may quote real prose, public domain only**, with the source
|
|
42
42
|
named in a comment. The Moby-Dick and Federalist No. 44 fixtures are the model.
|
|
43
43
|
A common idiom or a title is not a quotation and needs no such treatment.
|
|
44
|
-
- **`rationale:`
|
|
45
|
-
|
|
44
|
+
- **`rationale:` says why the construct reads as AI-written, not how it was
|
|
45
|
+
tested.** Corpus sizes and hit counts belong in the commit message, not the
|
|
46
|
+
shipped text.
|
|
46
47
|
- **A reference corpus of human prose must be public domain.** Copyrighted text
|
|
47
48
|
can't be redistributed, so a corpus built from it can't live in the repo, and
|
|
48
49
|
neither can the false-positive check that depends on it.
|
|
@@ -107,7 +108,7 @@ sloplint/
|
|
|
107
108
|
lib/sloplint/cli.rb # optparse, subcommands, exit codes
|
|
108
109
|
lib/sloplint/rules.rb # RULES: array of Rule (Data) objects — the catalog
|
|
109
110
|
lib/sloplint/engine.rb # Engine.scan(text, rules:, config:) -> [Note]
|
|
110
|
-
lib/sloplint/output.rb # format_human / format_json
|
|
111
|
+
lib/sloplint/output.rb # format_human / format_json
|
|
111
112
|
docs/
|
|
112
113
|
SPEC.md # this file
|
|
113
114
|
spec/
|
|
@@ -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": "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
356
|
+
### puffery
|
|
274
357
|
|
|
275
|
-
|
|
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
|
-
- `
|
|
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
|
-
###
|
|
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; `
|
|
351
|
-
- `
|
|
352
|
-
- `
|
|
353
|
-
- `
|
|
354
|
-
- `
|
|
355
|
-
- `
|
|
356
|
-
- `
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
- `
|
|
363
|
-
- `
|
|
364
|
-
- `
|
|
365
|
-
- `
|
|
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
|
-
###
|
|
394
|
+
### reader-address
|
|
368
395
|
|
|
369
|
-
|
|
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,
|
|
393
|
-
description — and with `--json`, the machine version an agent can
|
|
444
|
+
- `sloplint rules` prints the catalog: id, category, severity, confidence,
|
|
445
|
+
one-line description — and with `--json`, the machine version an agent can
|
|
446
|
+
enumerate, including each rule's `severity`, `confidence` and `rationale`.
|
|
447
|
+
- Naming a category in `check --select` runs only that category's non-low
|
|
448
|
+
rules; naming a rule's own id runs it regardless. `--strict` alone runs the
|
|
449
|
+
whole catalog, low-confidence rules included; with `--select` it only widens
|
|
450
|
+
the named categories to include their low-confidence members.
|
|
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,
|
|
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.
|
|
149
|
-
out.puts("#{r.id.ljust(24)} #{r.category.ljust(
|
|
148
|
+
off = r.confidence == "low" ? " [off by default]" : ""
|
|
149
|
+
out.puts("#{r.id.ljust(24)} #{r.category.ljust(18)} #{r.severity.ljust(8)} #{r.confidence.ljust(7)} #{r.message}#{off}")
|
|
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.
|
|
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.
|
|
201
|
-
#
|
|
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(
|
|
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
|
|
data/lib/sloplint/engine.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative "rules"
|
|
|
5
5
|
module Sloplint
|
|
6
6
|
# One match = one Note. See docs/SPEC.md "Note".
|
|
7
7
|
Note = Data.define(
|
|
8
|
-
:path, :line, :column, :severity, :rule, :category,
|
|
8
|
+
:path, :line, :column, :severity, :confidence, :rule, :category,
|
|
9
9
|
:message, :excerpt, :context, :count, :rationale, :suggestion
|
|
10
10
|
)
|
|
11
11
|
|
|
@@ -32,7 +32,8 @@ module Sloplint
|
|
|
32
32
|
message = count ? rule.message % { count: count } : rule.message
|
|
33
33
|
notes << Note.new(
|
|
34
34
|
path: path, line: line, column: column,
|
|
35
|
-
severity: rule.severity,
|
|
35
|
+
severity: rule.severity, confidence: rule.confidence,
|
|
36
|
+
rule: rule.id, category: rule.category,
|
|
36
37
|
message: message, excerpt: matched.gsub(/\s+/, " ").strip,
|
|
37
38
|
context: context_for(source, m),
|
|
38
39
|
count: count, rationale: rule.rationale, suggestion: rule.suggestion
|