diff --git a/README.md b/README.md index 3e13895..afc8e75 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ New here? Start with [plugins/concise/README.md](plugins/concise/README.md). - Comment blocks longer than 2 lines in `Write`, `Edit`, `MultiEdit`, and Codex `apply_patch` additions - New files longer than 300 lines - Inline `gh pr` and `gh issue` bodies with more than 1 prose paragraph or more than 3 sentences in a paragraph -- Em dashes and 44 categories of AI writing patterns, both off until you turn them on +- Em dashes and 47 categories of AI writing patterns, both off until you turn them on - Output from pytest, `go test`, npm test, Jest, and Vitest; each run's full output is kept in its own log under `$TMPDIR/concise-test-filter-//`, and the path is printed with the filtered output Structured GitHub bodies that use headings and lists are allowed. A denied call includes the exact limit and location. After 2 denied retries for the same target, Concise permits the next attempt and flags it. @@ -71,7 +71,7 @@ Start a new omp session. omp loads the hooks as an extension at session start, s - [plugins/concise/README.md](plugins/concise/README.md): the entry point. Quick start, what each check does, the 8 presets, and the escape hatches. - [plugins/concise/docs/configuration.md](plugins/concise/docs/configuration.md): every config key with its default, the 5 config layers, `mode`, `allowList`, `bypass`, and logging. -- [plugins/concise/docs/categories.md](plugins/concise/docs/categories.md): all 44 AI writing categories with an example, a fix, the presets, and the scopes. +- [plugins/concise/docs/categories.md](plugins/concise/docs/categories.md): all 47 AI writing categories with an example, a fix, the presets, and the scopes. - [plugins/concise/docs/environment.md](plugins/concise/docs/environment.md): every `BEC_` variable and 3 worked scenarios, including a cloud agent with no config file. - [plugins/concise/docs/packs.md](plugins/concise/docs/packs.md): the pack file format, the 3 pattern kinds, `detect(text, ctx)`, and the validator and renderer commands. - [plugins/concise/docs/tools.md](plugins/concise/docs/tools.md): the `concise-config` CLI, the MCP tools, the settings skills, and the writing-sample tuner. diff --git a/plugins/concise/README.md b/plugins/concise/README.md index f11da3e..6529fdc 100644 --- a/plugins/concise/README.md +++ b/plugins/concise/README.md @@ -88,7 +88,7 @@ To keep it, send the identical write again to confirm. To fix it, read / | File length | A new file over 300 lines, on `Write` or `*** Add File` | `[concise] /tmp/demo/big.js would be 305 lines (limit 300).` | | PR and issue bodies | A `gh` body over 1 prose paragraph, or a paragraph over 3 sentences | `[concise] PR/issue body is too verbose: a paragraph has 4 sentences (limit 3)` | | Em dashes | `U+2014`, plus `U+2013` when `enDash` is true, plus `--` between word characters when `doubleHyphen` is true | `The parser runs first — then it rejects the file.` | -| AI writing patterns | 44 categories of phrasing, commit hygiene, punctuation, and text statistics | `We delve into the parser.` | +| AI writing patterns | 47 categories of phrasing, commit hygiene, punctuation, and text statistics | `We delve into the parser.` | | Dictionary | Terms you list in `features.dictionary.entries` | `[concise:dictionary:blacklist] 1 match at line 3: "blacklisted" (denylist).` | A bulleted `## Summary` body is never flagged as a verbose PR body. Only unstructured prose is. @@ -110,7 +110,7 @@ After 2 denied retries on the same target, the hook allows the action and flags | `minimal` | A team that only wants chat boilerplate gone | 4 categories: `chatbot`, `sycophancy`, `ai-identity`, and `ai-attribution`. | | `git` | Commit messages, PR bodies, and reviews | 7 commit, PR, and review categories. | | `statistical` | A writing audit over long documents | 10 text statistics categories. | -| `all` | Every rule, `ste` included | 44 categories. | +| `all` | Every rule, `ste` included | 47 categories. | Pick one: @@ -128,14 +128,14 @@ Setting `features.aiWriting.categories` to a list of category ids replaces the p ## Categories -The 44 categories fall into 5 groups. +The 47 categories fall into 5 groups. | Group | Count | Flagged example | Fix | |---|---|---|---| -| Phrase categories | 14 | `In conclusion` (`closers`) | End on the last fact. | +| Phrase categories | 16 | `In conclusion` (`closers`) | End on the last fact. | | Rhetorical categories | 11 | `studies show` (`vague-attribution`) | Cite the study. | | Commit and review categories | 7 | `Co-authored-by: Claude` (`ai-attribution`) | Drop the trailer. | -| Punctuation categories | 2 | an arrow character (`unicode-glyphs`) | Use `->`. | +| Punctuation categories | 3 | an arrow character (`unicode-glyphs`) | Use `->`. | | Text statistics categories | 10 | passive voice over 30% of sentences (`passive-voice`) | Name the actor. | The full table, with every category id, its presets, and the scopes it runs in, is in [docs/categories.md](docs/categories.md). The flagged text and the replacement for every pattern are in [skills/concise-rules/references/ai-speak-patterns.md](skills/concise-rules/references/ai-speak-patterns.md). diff --git a/plugins/concise/docs/INDEX.md b/plugins/concise/docs/INDEX.md index e30639e..798be36 100644 --- a/plugins/concise/docs/INDEX.md +++ b/plugins/concise/docs/INDEX.md @@ -1,15 +1,15 @@ # Docs index Created: 2026-10-05 -Last updated: 2026-10-07 +Last updated: 2026-10-08 | Doc | Read it to | Created | Last updated | |---|---|---|---| | [configuration.md](configuration.md) | Find a config key, its default, the layer order, the merge rules, `mode`, and the dictionary. | 2026-09-04 | 2026-10-06 | | [tools.md](tools.md) | Change settings with the `concise-config` CLI or the MCP tools, or tune them from writing samples. | 2026-10-05 | 2026-10-05 | | [environment.md](environment.md) | Control a session with `BEC_` variables. | 2026-09-04 | 2026-10-05 | -| [categories.md](categories.md) | Look up an AI writing category, its presets, and its scopes. | 2026-09-04 | 2026-09-29 | -| [packs.md](packs.md) | Write or check a pattern pack. | 2026-09-04 | 2026-09-05 | +| [categories.md](categories.md) | Look up an AI writing category, its presets, and its scopes. | 2026-09-04 | 2026-10-08 | +| [packs.md](packs.md) | Write or check a pattern pack. | 2026-09-04 | 2026-10-08 | | [host-features.md](host-features.md) | Check which Claude Code, Codex, and omp hook, skill, and MCP features the manifests and the omp extension use, with the host doc sources and the live test results. | 2026-10-05 | 2026-10-07 | Read [host-features.md](host-features.md) and its sources before you change `hooks/hooks.json`, `hooks/codex.json`, `.mcp.json`, `.codex-mcp.json`, `package.json`, `omp/`, or a plugin manifest. diff --git a/plugins/concise/docs/categories.md b/plugins/concise/docs/categories.md index 2e5b707..b837ab3 100644 --- a/plugins/concise/docs/categories.md +++ b/plugins/concise/docs/categories.md @@ -1,6 +1,6 @@ # Categories -The `aiWriting` feature ships 44 categories. Each category is one pack file under `hooks/lib/patterns/`. This page lists every category, the presets that turn it on, and the scopes it runs in. +The `aiWriting` feature ships 47 categories. Each category is one pack file under `hooks/lib/patterns/`, except `claude-tells`, which spans `claude-tells.json`, `closing-offer.json`, and `backtick-branches.mjs`. This page lists every category, the presets that turn it on, and the scopes it runs in. Pick categories with `features.aiWriting.preset`, or replace the preset list with `features.aiWriting.categories`. Both keys are described in [configuration.md](configuration.md). The flagged text and the replacement for every pattern are in [../skills/concise-rules/references/ai-speak-patterns.md](../skills/concise-rules/references/ai-speak-patterns.md). @@ -12,7 +12,9 @@ Scopes across this group: `files`, `comments`, `gh`, `commit`, `reply`. The scop | id | What it catches | Flagged example | Fix | Presets | Scopes | |---|---|---|---|---|---| +| `british-spelling` | British spellings in text that otherwise uses American English. | `behaviour` | use the American spelling | `ryan`, `all` | `files`, `comments`, `gh`, `commit`, `reply` | | `chatbot` | Assistant boilerplate that belongs to a chat window. | `Great question` | delete the line | `default`, `ryan`, `technical`, `minimal`, `all` | `files`, `comments`, `gh`, `commit`, `reply` | +| `claude-tells` | Phrasing that Claude 5.x models write far more often than people do. | `still not committed, on purpose` | `still intentionally not committed` | `default`, `ryan`, `technical`, `all` | `files`, `comments`, `gh`, `commit`, `reply` | | `closers` | Endings that restate the text instead of stopping. | `In conclusion` | end on the last fact | `default`, `ryan`, `technical`, `all` | `files`, `comments`, `gh`, `commit`, `reply` | | `contrast` | Frames that define the subject by what it is not. | `it's not a config change, it's a rewrite` | state the positive claim | `default`, `ryan`, `technical`, `all` | `files`, `comments`, `gh`, `commit`, `reply` | | `copula` | Verbs that interpret the subject instead of naming what it does. | `serves as` | say what it does | `ryan`, `ste`, `all` | `files`, `comments`, `gh`, `commit`, `reply` | @@ -61,10 +63,11 @@ Scopes across this group: `comments`, `gh`, `commit`, `command`, `reply`. The sc ## Punctuation categories -Scopes across this group: `comments`, `gh`, `commit`, `command`, `reply`. The scope column below gives the scopes for each category. +Scopes across this group: `files`, `comments`, `gh`, `commit`, `command`, `reply`. The scope column below gives the scopes for each category. | id | What it catches | Flagged example | Fix | Presets | Scopes | |---|---|---|---|---|---| +| `hidden-characters` | Invisible or look-alike characters that model output and chat UIs leave in text. | a zero-width space | remove the character | `default`, `ryan`, `technical`, `git`, `all` | `files`, `comments`, `gh`, `commit`, `command`, `reply` | | `smart-punctuation` | Curly quotes and the ellipsis character typical of text pasted from a chat UI. | a curly apostrophe | use a straight apostrophe | `default`, `ryan`, `technical`, `all` | `comments`, `commit`, `gh`, `command`, `reply` | | `unicode-glyphs` | Arrows, box drawing, dingbats, and geometric symbols used in place of plain ASCII or words. | an arrow character | use `->` | `default`, `ryan`, `technical`, `all` | `commit`, `gh`, `command`, `reply` | diff --git a/plugins/concise/docs/packs.md b/plugins/concise/docs/packs.md index c7871f7..660cca0 100644 --- a/plugins/concise/docs/packs.md +++ b/plugins/concise/docs/packs.md @@ -1,6 +1,6 @@ # Pattern packs -Every category is one pack file. The built-in packs live under `hooks/lib/patterns/`: `ai/` for writing patterns, `git/` for commit, PR, and review checks, `prose/` for punctuation. A project adds its own packs the same way. +Most categories are one pack file. A pack whose `category` is a string joins that category, as `closing-offer.json` and `backtick-branches.mjs` join `claude-tells`. The built-in packs live under `hooks/lib/patterns/`: `ai/` for writing patterns, `git/` for commit, PR, and review checks, `prose/` for punctuation. A project adds its own packs the same way. A `.mjs` pack executes code from the checkout. The hook imports it, so its top-level code and its `detect` function run in the hook process with your permissions on every tool call. Read a `.mjs` pack before you add it. A `.json` pack holds data only. @@ -167,6 +167,7 @@ export default { | `ctx.path` | The file path, or `null` for a reply, commit message, `gh` body, or command. | | `ctx.scope` | The scope name the text came from. | | `ctx.options` | The pack's `options`, merged with `features.aiWriting.options.`. | +| `ctx.raw` | The text before code spans, fences, comments, and URLs are blanked. It has the same length as `ctx.stats.text`, so an index into one is an index into the other. | | `ctx.stats.text` | The text being scanned. | | `ctx.stats.words()` | `{ text, start, end }` per word. | | `ctx.stats.sentences()` | `{ text, start, end }` per sentence. | diff --git a/plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs b/plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs new file mode 100644 index 0000000..1ce9561 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/backtick-branches.mjs @@ -0,0 +1,28 @@ +// stripCode blanks code spans before packs run, so this check reads ctx.raw. +// Without the engine change in engine.patch, ctx.raw is undefined and the pack reports nothing. +const LEAD = String.raw`\b(?:from|into|onto|against|off|to|on|of|rebased? on|merged? into|checkout|switch to|pushed? to)\s+`; + +function branchPattern(branches) { + const names = branches.map((b) => b.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")).join("|"); + return new RegExp(`${LEAD}(?\`(?:${names})\`)|(?\`(?:${names})\`)(?=\\s+branch(?:es)?\\b)`, "gi"); +} + +export default { + id: "backtick-branches", + feature: "aiWriting", + category: "claude-tells", + scope: ["gh", "commit", "reply"], + presets: ["default", "ryan", "technical", "all"], + options: { branches: ["main", "master", "dev", "develop", "development", "staging", "stage", "production", "prod", "release", "trunk", "next", "beta", "canary"] }, + notes: "Flags a branch name in backticks after `from`, `into`, `onto`, `against`, or a similar word, or before `branch`. Prose files stay out of scope: docs often set branch names as code by convention. Set `options.branches` to match the repo's branch names.", + detect(text, ctx) { + if (typeof ctx.raw !== "string" || ctx.raw.length !== text.length) return []; + const re = branchPattern(ctx.options.branches || []); + const out = []; + for (const m of ctx.raw.matchAll(re)) { + const hit = m.groups.lead || m.groups.tail; + out.push({ index: m.index + m[0].indexOf(hit), match: hit, fix: "drop the backticks around the branch name" }); + } + return out; + }, +}; diff --git a/plugins/concise/hooks/lib/patterns/ai/british-spelling.json b/plugins/concise/hooks/lib/patterns/ai/british-spelling.json new file mode 100644 index 0000000..435fa25 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/british-spelling.json @@ -0,0 +1,28 @@ +{ + "id": "british-spelling", + "feature": "aiWriting", + "category": { "id": "british-spelling", "label": "British spelling", "description": "British spellings in text that otherwise uses American English.", "tags": ["phrase", "claude"] }, + "source": "Measured on Claude Code transcripts: Opus 5.5 wrote `behaviour` in 31% of uses, `labelled` in 58%, and `grey` in 77%; npm READMEs used them in 11%, 20%, and 30%", + "presets": ["ryan", "all"], + "scope": ["files", "comments", "gh", "commit", "reply"], + "notes": "A repo that writes British English lists `british-spelling` in `excludePacks`. `cancelled`, `judgement`, `acknowledgement`, `towards`, and `dialogue` are left out because American writers use them too. `analyses` is left out because it is also the plural of `analysis`. Elixir's `@behaviour` attribute sits in code, which the scan skips.", + "patterns": [ + { "regex": "\\bbehaviour(?:s|al|ally)?\\b", "fix": "behavior", "show": "`behaviour`, `behaviours`, `behavioural`" }, + { "regex": "\\bcolour(?:s|ed|ing|ful)?\\b", "fix": "color", "show": "`colour`, `colours`, `coloured`" }, + { "regex": "\\b(?:fav|hon|neighb|lab|flav|hum|rum|harb|arm|vap)our(?:s|ed|ing|ite|ites|able|hood)?\\b", "fix": "drop the `u`: `favor`, `honor`, `neighbor`", "show": "`favour`, `honour`, `neighbour`, `labour`, `flavour`, `humour`, `rumour`, `harbour`, `armour`, `vapour`" }, + { "regex": "\\b(?:label|model|travel|signal|fuel|level|tunnel|channel)l(?:ed|ing)\\b", "fix": "one `l`: `labeled`, `modeling`", "show": "`labelled`, `modelling`, `travelled`, `signalled`, `levelled`, `channelled`" }, + { "regex": "\\b(?:cent|met|lit|fib|theat)re(?:s|d)?\\b", "fix": "`-er`: `center`, `meter`", "show": "`centre`, `metre`, `litre`, `fibre`, `theatre`" }, + { "regex": "\\b(?:de)?(?:organi|recogni|reali|initiali|seriali|normali|optimi|minimi|maximi|prioriti|customi|finali|synchroni|saniti|authori|summari|categori|tokeni|visuali|standardi|generali|characteri|memori|parameteri|containeri|virtuali|centrali|capitali|emphasi|apologi|materiali|speciali|stabili|anonymi|randomi|locali|internationali|critici|digiti|moderni|personali)s(?:e|es|ed|ing|ation|ations|er|ers)\\b", "fix": "`-ize`, `-ization`", "show": "`-ise` and `-isation` forms such as `normalise`, `initialised`, `organisation`" }, + { "regex": "\\b(?:analy|paraly|cataly)s(?:e|ed|ing)\\b", "fix": "`-yze`: `analyze`", "show": "`analyse`, `analysed`, `analysing`" }, + { "phrase": "grey(?:s|ed|ing)?", "fix": "gray" }, + { "phrase": "whilst", "fix": "while" }, + { "phrase": "amongst", "fix": "among" }, + { "phrase": "programmes?", "fix": "program" }, + { "phrase": "licence[ds]?", "fix": "license" }, + { "phrase": "defence|offence", "fix": "defense, offense" }, + { "phrase": "catalogue[ds]?", "fix": "catalog" }, + { "phrase": "artefacts?", "fix": "artifact" }, + { "phrase": "spelt|learnt", "fix": "spelled, learned" }, + { "phrase": "per cent", "fix": "percent" } + ] +} diff --git a/plugins/concise/hooks/lib/patterns/ai/claude-tells.json b/plugins/concise/hooks/lib/patterns/ai/claude-tells.json new file mode 100644 index 0000000..ecf3ec0 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/claude-tells.json @@ -0,0 +1,23 @@ +{ + "id": "claude-tells", + "feature": "aiWriting", + "category": { "id": "claude-tells", "label": "Claude tells", "description": "Phrasing that Claude 5.x models write far more often than people do.", "tags": ["phrase", "claude"] }, + "source": "Measured on 1.29M words of Opus 5.5 Claude Code replies against 1.13M words of npm READMEs and 87K words of pre-2023 commits; Anthropic's Opus 5.5 and Fable 5.1 prompting guides", + "presets": ["default", "ryan", "technical", "all"], + "scope": ["files", "comments", "gh", "commit", "reply"], + "notes": "Every tier-1 row fired at most 0.02 times per 10,000 words in the README and commit baselines. The status-label row needs a verbless fragment at the start of a line or sentence, so `This is a known limitation, not a bug` stays with the `contrast` pack. The closing offer lives in `closing-offer.json` so it can be excluded alone.", + "patterns": [ + { "regex": "\\b(?:the |this |that |its )?(?:docs?|documentation|README|comment|changelog|spec|plan|proposal|code|tests?|commit|PR|diff|issue|ticket|log|output|page|guide|report|schema|config)s? (?:itself )?(?:admits?|concedes?|confess(?:es)?|lies|pretends?|swears?)\\b", "fix": "say what the text states: `the doc says X`", "show": "`the doc admits`, `the code pretends`, `the test lies`" }, + { "regex": "\\b(?:admits|concedes|confesses) (?:it|as much)\\b", "fix": "say what the text states", "show": "`admits it`, `concedes as much`" }, + { "regex": "(?:^|[.!?:]\\s+|[-*]\\s+(?:\\*\\*)?)(?:Pre-?existing|Existing|Intentional|Deliberate|Expected|Cosmetic|Unrelated|Harmless|Benign|Separate|Known|Upstream|Flaky|Latent)\\b[^.!?\\n,]{0,30}(?,\\s+not\\s+(?:a |an |the )?[\\w-]+)", "flags": "gim", "fix": "cut the `not X` tail, the label already says it", "show": "`Existing bug, not a regression`" }, + { "regex": "\\bthe (?big|huge|giant|scary) (?:one|migration|refactor|rewrite|change|fix|bug|risk|issue|problem|question|PR|diff|win|gap|piece|part|item|table|query|file|job|deploy|release|merge|commit|cleanup|update|upgrade)s?\\b", "fix": "cut the size word, or give the size", "show": "`the big migration`, `the huge refactor`" }, + { "regex": "\\b(?:covers?|covered|exercises?|exercised|hits?|reach(?:es|ed)?|tests?|tested|guards?|skips?|suspect|traced?) (?:this|that|the same|either|each|both|the other) (?:code )?(?path)\\b", "fix": "name the branch: the function, condition, or flag", "show": "`covers this path`, `hits that path`" }, + { "phrase": "on purpose", "fix": "put `intentionally` before the verb, or give the reason" }, + { "regex": ",\\s*(?:by design|as intended|as designed)(?=\\s*[.!?;:)]|\\s*$)", "flags": "gim", "fix": "cut the tag, or give the reason", "show": "`, by design.`, `, as intended.`" }, + { "regex": ",\\s*(?:with|given|plus) your [^.!?\\n,]{0,40}?\\b(?:note|caveat|point|comment|context|feedback|concern|flag|constraint|correction|request|ask|framing|numbers?|rule|instructions?)\\b", "fix": "put the aside in parentheses, or make it a clause", "show": "`, with your low call volume note`" }, + { "phrase": "quietly", "fix": "cut it, or say what is not logged or reported" }, + { "regex": "\\bearns? (?:its|their) (?:keep|place)\\b", "fix": "say what it does", "show": "`earns its keep`, `earns its place`" }, + { "regex": "\\b(?:a|one) real (?:bug|risk|problem|issue|gap|concern|regression|win|finding)\\b|\\bthe real (?:fix|bug|risk|issue|problem|gap|cause|culprit|cost|blocker|story)\\b", "fix": "cut `real`", "tier": 2, "show": "`a real bug`, `the real gap`, `the real story`" }, + { "regex": "\\breads (?:well|cleanly|naturally)\\b", "fix": "say what works", "tier": 2, "show": "`reads well`, `reads cleanly`" } + ] +} diff --git a/plugins/concise/hooks/lib/patterns/ai/closing-offer.json b/plugins/concise/hooks/lib/patterns/ai/closing-offer.json new file mode 100644 index 0000000..6b094a6 --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/ai/closing-offer.json @@ -0,0 +1,12 @@ +{ + "id": "closing-offer", + "feature": "aiWriting", + "category": "claude-tells", + "source": "Anthropic's Opus 5.5 prompting guide lists \"an offer to carry on with something unless the user would prefer otherwise\" as a habit to suppress; 1.93 per 10,000 words in Opus 5.5 replies, none in the baselines", + "presets": ["default", "ryan", "technical", "all"], + "scope": ["gh", "reply"], + "notes": "Fires only when the offer is the last sentence of the text. A question followed by a reason, such as `Do you want me to delete it? It has two unmerged commits.`, stays unflagged. A setup that asks before every test run can list `closing-offer` in `excludePacks`.", + "patterns": [ + { "regex": "(?:^|[.!?]\\s+)(?(?:Do you |Would you )?(?:want|like) me to\\b[^?\\n]{0,160}\\?)\\s*$", "flags": "gi", "fix": "cut the offer, or name the step that needs approval", "show": "`Want me to ...?` or `Do you want me to ...?` at the end of the text" } + ] +} diff --git a/plugins/concise/hooks/lib/patterns/git/ai-attribution.json b/plugins/concise/hooks/lib/patterns/git/ai-attribution.json index 4eee664..3716923 100644 --- a/plugins/concise/hooks/lib/patterns/git/ai-attribution.json +++ b/plugins/concise/hooks/lib/patterns/git/ai-attribution.json @@ -2,7 +2,7 @@ "id": "ai-attribution", "feature": "aiWriting", "category": { "id": "ai-attribution", "label": "AI attribution trailer", "description": "A commit or PR trailer credits an AI tool as co-author, generator, or signer.", "tags": ["git"] }, - "source": "Group G research: claude-code#1296, Codex CLI commit attribution, VS Code #314311, Cursor forum #168258, docs.devin.ai", + "source": "Group G research: claude-code#1296, Codex CLI commit attribution, VS Code #314311, Cursor forum #168258, docs.devin.ai, code.claude.com settings reference (attribution.sessionUrl), Cursor forum #154494, GitHub changelog 2026-03-20", "presets": ["default", "ryan", "technical", "minimal", "git", "all"], "scope": ["commit", "gh", "command"], "notes": "The two `Co-authored-by` patterns sit at tier 2. A trailer that pairs an AI tool name with an AI email reports; a co-author named Claude with a personal address stays quiet.", @@ -15,6 +15,9 @@ { "regex": "^Generated-By:.*$", "flags": "gim", "fix": "drop the trailer", "show": "a `Generated-By:` trailer" }, { "phrase": "This commit was generated by", "fix": "drop the disclosure sentence" }, { "regex": "^Assisted-by:.*$", "flags": "gim", "fix": "drop the trailer", "show": "an `Assisted-by:` trailer" }, + { "regex": "^Claude-Session:.*$", "flags": "gim", "fix": "drop the session trailer", "show": "a `Claude-Session:` trailer" }, + { "regex": "^Made-with:\\s*Cursor\\b.*$", "flags": "gim", "fix": "drop the trailer", "show": "a `Made-with: Cursor` trailer" }, + { "regex": "^Agent-Logs-Url:.*$", "flags": "gim", "fix": "drop the trailer", "show": "an `Agent-Logs-Url:` trailer" }, { "regex": "^Signed-off-by:.*\\[bot\\].*$", "flags": "gim", "fix": "drop the bot sign-off", "show": "`Signed-off-by:` plus a `[bot]` account" }, { "regex": "^Signed-off-by:.*(?:noreply@anthropic\\.com|noreply@openai\\.com|all-hands\\.dev|cursor\\.com)>?\\s*$", "flags": "gim", "fix": "drop the bot sign-off", "show": "`Signed-off-by:` plus an AI tool email" } ] diff --git a/plugins/concise/hooks/lib/patterns/git/ai-identity.json b/plugins/concise/hooks/lib/patterns/git/ai-identity.json index d0d003f..4f9d0cf 100644 --- a/plugins/concise/hooks/lib/patterns/git/ai-identity.json +++ b/plugins/concise/hooks/lib/patterns/git/ai-identity.json @@ -5,7 +5,7 @@ "source": "Group G research: claude-code#72477, VS Code #314311, Cursor forum #168258, gemini-cli #11447", "presets": ["default", "ryan", "technical", "minimal", "git", "all"], "scope": ["command"], - "notes": "Bare first names such as `Claude`, `Devin`, and `Gemini` sit at tier 2, so a person who sets their own identity with a personal email produces one hit and stays quiet. A repo that commits as a bot on purpose lists `ai-identity` in `excludePacks`.", + "notes": "Bare first names such as `Claude`, `Devin`, and `Gemini` sit at tier 2, so a person who sets their own identity with a personal email produces one hit and stays quiet. A repo that intentionally commits as a bot lists `ai-identity` in `excludePacks`.", "patterns": [ { "regex": "--author=(['\"])[^'\"]*(?:Claude Code|OpenAI Codex|GitHub Copilot|Cursor Agent|OpenHands)[^'\"]*\\1", "fix": "use a human author name", "show": "`--author=` naming an AI tool" }, { "regex": "--author=(['\"])[^'\"]*(?:noreply@anthropic\\.com|noreply@openai\\.com|codex@openai\\.com|copilot@github\\.com|cursoragent@cursor\\.com|openhands@all-hands\\.dev)[^'\"]*\\1", "fix": "use a human email address", "show": "`--author=` holding an AI tool email" }, diff --git a/plugins/concise/hooks/lib/patterns/git/benefit-tail.json b/plugins/concise/hooks/lib/patterns/git/benefit-tail.json index 0c6267d..3a61121 100644 --- a/plugins/concise/hooks/lib/patterns/git/benefit-tail.json +++ b/plugins/concise/hooks/lib/patterns/git/benefit-tail.json @@ -5,7 +5,7 @@ "source": "Group G research: AI-generated PR summary and commit body conventions", "presets": ["ryan", "technical", "git", "all"], "scope": ["commit", "gh", "comments", "reply"], - "notes": "The noun list stays abstract on purpose. A tail that names a measured effect, such as `cuts p99 latency to 90ms`, does not match.", + "notes": "The noun list is intentionally abstract. A tail that names a measured effect, such as `cuts p99 latency to 90ms`, does not match.", "patterns": [ { "regex": ",\\s*(?:which|thereby|thus|to|for|ensuring|so that it's) [^.!?\\n]{0,40}?\\b(?:improves?|increases?|reduces?|ensures?|enhances?|makes?)\\b[^.!?\\n]{0,30}?\\b(?:readability|maintainability|reliability|performance|consistency|robustness|clarity|scalability|efficiency|usability|stability|testability|extensibility|security|quality|correctness)\\b", "fix": "cut the justification clause", "show": "`, which improves readability` and the like" }, { "regex": ",\\s*making it (?:easier|simpler|harder|more \\w+) to \\w+", "fix": "cut the justification clause", "show": "`, making it easier to maintain`" }, diff --git a/plugins/concise/hooks/lib/patterns/prose/hidden-characters.json b/plugins/concise/hooks/lib/patterns/prose/hidden-characters.json new file mode 100644 index 0000000..10822db --- /dev/null +++ b/plugins/concise/hooks/lib/patterns/prose/hidden-characters.json @@ -0,0 +1,20 @@ +{ + "id": "hidden-characters", + "feature": "aiWriting", + "category": { "id": "hidden-characters", "label": "hidden characters", "description": "Invisible or look-alike characters that model output and chat UIs leave in text.", "tags": ["punctuation", "watermark"] }, + "source": "Opus 5.5 replies (U+2011 at 0.33 per 10,000 words), Microsoft known issue on LLM hyphens (Apr 2026), OpenAI U+202F reports (2025), Trojan Source bidi controls, Wikipedia:Signs of AI writing", + "presets": ["default", "ryan", "technical", "git", "all"], + "scope": ["files", "comments", "gh", "commit", "command", "reply"], + "notes": "Anthropic's text watermark adds no characters, so this pack cannot see it. U+00A0 is left out: npm READMEs carry it 5.6 times per 10,000 words. A zero-width joiner inside an emoji sequence and tag characters after the black flag (subdivision flags) stay unflagged. A right-to-left project lists `hidden-characters` in `excludePacks` or drops the bidi row with its own pack.", + "patterns": [ + { "regex": "[\\u200B\\u200C\\u2060\\u180E]", "flags": "gu", "fix": "remove the zero-width character", "show": "a zero-width space, zero-width non-joiner, word joiner, or Mongolian vowel separator" }, + { "regex": "(? PROSE_EXTENSIONS.includes(extOf(filePath)); export function proseSpans(text, path) { - if (isProsePath(path)) return [{ text: stripCode(text), line: 1 }]; - return scanComments(text, path).map((run) => ({ text: run.text, line: run.startLine })); + if (isProsePath(path)) return [{ text: stripCode(text), raw: text, line: 1 }]; + return scanComments(text, path).map((run) => ({ text: run.text, raw: run.text, line: run.startLine })); } const MESSAGE_FLAG = /(?:^|\s)(?:--message(?:=|\s+)|-[A-Za-z]*m\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g; diff --git a/plugins/concise/hooks/lib/style-check.mjs b/plugins/concise/hooks/lib/style-check.mjs index c054a7f..30a4380 100644 --- a/plugins/concise/hooks/lib/style-check.mjs +++ b/plugins/concise/hooks/lib/style-check.mjs @@ -135,7 +135,7 @@ export function styleFindings(text, path, config, scope = "files", hook = null) if (keep(at(hit.line), hit.match)) dictionary.push({ ...hit, line: at(hit.line) }); } if (packs.length === 0) continue; - const hits = scanAiWriting(span.text, { packs, allow: resolved.allow, ctx: { path, scope }, problems: runtime }); + const hits = scanAiWriting(span.text, { packs, allow: resolved.allow, ctx: { path, scope, raw: span.raw }, problems: runtime }); for (const hit of hits) { if (keep(at(hit.line), hit.match)) aiWriting.push({ ...hit, line: at(hit.line) }); } diff --git a/plugins/concise/skills/concise-rules/references/ai-speak-patterns-2.md b/plugins/concise/skills/concise-rules/references/ai-speak-patterns-2.md index 01920e2..8f90c3c 100644 --- a/plugins/concise/skills/concise-rules/references/ai-speak-patterns-2.md +++ b/plugins/concise/skills/concise-rules/references/ai-speak-patterns-2.md @@ -5,6 +5,68 @@ Built from the packs in `hooks/lib/patterns`. Each section names its own source. The category index is in `ai-speak-patterns.md`. +## inflation + +Presets: `default`, `ryan`, `technical`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. + +Wording that makes a change sound larger than it is. + +| Flagged | Fix | +|---|---| +| `marks a significant`, `marks a major`, `marks a new`, `marks a turning` | state what happened | +| `represents a significant`, `represents a major`, `represents a fundamental` | state what changed | +| `a significant step forward`, `a major step towards`, `a bold step toward`, `a big step forward`, `step forward for` | state what changed | +| `turning point`, `sea change`, `paradigm shift`, `game-changer`, `game-changing`, `watershed moment` | state what changed | +| `groundbreaking` | name the prior art it beats | +| `first of its kind` | cite the search | +| `revolutionary`, `revolutionize`, `revolutionizes`, `revolutionized` | describe the change | +| `the future of` | cut | +| `unprecedented` | name the precedent, or cut | +| `redefine the`, `redefine how`, `redefine what`, `redefines the`, `redefines how`, `redefines what`, `reshape the`, `reshapes the`, `reshaping the` | describe the change | +| `real utility`, `actual value`, `genuine impact`, `true change`, `real results` | drop the adjective | +| `despite challenges`, `despite these challenges`, `despite the challenges` | name the challenge | +| `continues to thrive`, `remains resilient` | cite the number | + +## lexical-diversity + +Presets: `statistical`, `all`. + +Flags text that reuses the same words instead of varying vocabulary. + +| Flagged | Fix | +|---|---| +| Flags text that reuses the same words instead of varying vocabulary. | `minWords = 150`, `minRatio = 0.55`, `segment = 100`, `tailSegment = 50` | + +Mean segmental type-token ratio over 100-word segments. Direction of the academic signal is contested, so it stays out of the ryan preset. + +## negative-parallelism + +Presets: `default`, `ryan`, `technical`, `all`. Source: Wikipedia:Signs of AI writing, WikiProject AI Cleanup catchphrases, tropes.fyi, refine.so. + +A chain of two or more negated clauses resolved by one affirmation. + +| Flagged | Fix | +|---|---| +| `Not X. Not Y. Just Z.` | state the one claim | +| `no X, no Y, just Z` | state the one claim | +| `not X, not Y, but Z` | state the one claim | +| `no X, no Y. Just Z.` | state the one claim | +| `isn't X. It's not Y. It's Z.` | state the one claim | + +The contrast pack owns the single "not X, it's Y" pair. This pack fires only on two or more stacked negations before the resolution. + +## outline-conclusion + +Presets: `ryan`, `all`. Source: Wikipedia:Signs of AI writing (outline-like conclusions) and tropes.fyi fractal summaries. + +A closing paragraph that re-lists the section headings instead of adding a new fact. + +| Flagged | Fix | +|---|---| +| A closing paragraph that re-lists the section headings instead of adding a new fact. | `minWords = 300`, `minHeadings = 3`, `minOverlap = 3`, `minRatio = 0.6`, `smallDoc = 4`, `maxParagraphWords = 120` | + +Needs 300 words, three headings, a closing paragraph that echoes three section titles, and a restatement cue. A closing paragraph that adds a new fact passes. + ## overgeneralization Presets: `default`, `ryan`, `technical`, `all`. Source: Wikipedia:Signs of AI writing and WikiProject AI Cleanup catchphrases. @@ -228,65 +290,3 @@ A significance claim with no evidence behind it. | `speaks volumes about` | state the fact | | `carries significant weight`, `carries considerable weight`, `carries real weight` | name the weight, or cut | | `highlighting its importance` | state the fact, drop the tail | - -## sycophancy - -Presets: `default`, `ryan`, `technical`, `minimal`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. - -Praise aimed at the reader instead of the work. - -| Flagged | Fix | -|---|---| -| `You're right` (sentence-initial) | cut, state the fix | -| `you're absolutely right` | cut | -| `what a great`, `what a fantastic`, `what a wonderful` | cut | -| `Great point`, `Great idea`, `Great catch`, `Excellent observation`, `Excellent suggestion` | cut | -| `That's a great point`, `That's an excellent point`, `That's a fantastic idea`, `That's a really good observation`, `That's an insightful observation` | cut | -| `I completely understand`, `I completely agree`, `I totally understand`, `I totally agree` | cut | -| `thank you for sharing`, `thank you for bringing this`, `thank you for pointing`, `thank you for the clarification`, `thank you for your patience` | cut | -| `thanks for sharing`, `thanks for pointing`, `thanks for flagging` | cut | -| `You raise a great point`, `You raise an excellent point`, `You raise a good point`, `You raise a valid point`, `You raise an important point` | cut | -| `I apologize for the confusion`, `I apologize for the oversight`, `I apologize for the inconvenience`, `I apologize for any confusion`, `I apologize for any oversight`, `I apologize for any inconvenience`, `apologies for the confusion`, `my apologies` | state the correction | - -## terminal-punctuation - -Presets: `statistical`, `all`. - -Flags list items that all carry a trailing period even though they read as short fragments. - -| Flagged | Fix | -|---|---| -| Flags list items that all carry a trailing period even though they read as short fragments. | `minWords = 0`, `minItems = 8`, `minBlocks = 2`, `maxItemWords = 8` | - -List items only. Every declarative sentence ends in a period, so sentence-level uniformity carries no signal. - -## transition-density - -Presets: `ryan`, `statistical`, `all`. - -Flags text where too many sentences open on a transition or connector word. - -| Flagged | Fix | -|---|---| -| Flags text where too many sentences open on a transition or connector word. | `minWords = 150`, `minSentences = 8`, `maxDensity = 0.3` | - -Document-wide connector rate. The transitions pack flags single openers; this one flags the habit. - -## transitions - -Presets: `default`, `ryan`, `technical`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. - -Connectors and scene setters that add length without adding a fact. - -| Flagged | Fix | -|---|---| -| `Moreover`, `Furthermore`, `Additionally` (sentence-initial) | cut, or "and", "also" | -| `Notably`, `Importantly`, `Interestingly`, `Surprisingly` (sentence-initial) | cut | -| `in today's` | name the context or cut | -| `in an era where`, `in an era of` | cut | -| `in the evolving world of`, `in the evolving field of`, `in the changing world of`, `in the changing field of`, `in the rapidly evolving world of`, `in the rapidly evolving field of`, `in the rapidly changing world of`, `in the rapidly changing field of` | lead with the point | -| `worth noting`, `it's worth noting`, `it is worth noting` | state the fact | -| `when it comes to` | name the thing | -| `here's what's interesting`, `here's what caught my eye`, `here's what stood out`, `here's the interesting part` | state the fact | -| `that said`, `that being said` | cut, or "but" | -| `Certainly`, `Undoubtedly`, `Without a doubt` | cut | diff --git a/plugins/concise/skills/concise-rules/references/ai-speak-patterns-3.md b/plugins/concise/skills/concise-rules/references/ai-speak-patterns-3.md index d134203..61a8a9d 100644 --- a/plugins/concise/skills/concise-rules/references/ai-speak-patterns-3.md +++ b/plugins/concise/skills/concise-rules/references/ai-speak-patterns-3.md @@ -5,6 +5,68 @@ Built from the packs in `hooks/lib/patterns`. Each section names its own source. The category index is in `ai-speak-patterns.md`. +## sycophancy + +Presets: `default`, `ryan`, `technical`, `minimal`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. + +Praise aimed at the reader instead of the work. + +| Flagged | Fix | +|---|---| +| `You're right` (sentence-initial) | cut, state the fix | +| `you're absolutely right` | cut | +| `what a great`, `what a fantastic`, `what a wonderful` | cut | +| `Great point`, `Great idea`, `Great catch`, `Excellent observation`, `Excellent suggestion` | cut | +| `That's a great point`, `That's an excellent point`, `That's a fantastic idea`, `That's a really good observation`, `That's an insightful observation` | cut | +| `I completely understand`, `I completely agree`, `I totally understand`, `I totally agree` | cut | +| `thank you for sharing`, `thank you for bringing this`, `thank you for pointing`, `thank you for the clarification`, `thank you for your patience` | cut | +| `thanks for sharing`, `thanks for pointing`, `thanks for flagging` | cut | +| `You raise a great point`, `You raise an excellent point`, `You raise a good point`, `You raise a valid point`, `You raise an important point` | cut | +| `I apologize for the confusion`, `I apologize for the oversight`, `I apologize for the inconvenience`, `I apologize for any confusion`, `I apologize for any oversight`, `I apologize for any inconvenience`, `apologies for the confusion`, `my apologies` | state the correction | + +## terminal-punctuation + +Presets: `statistical`, `all`. + +Flags list items that all carry a trailing period even though they read as short fragments. + +| Flagged | Fix | +|---|---| +| Flags list items that all carry a trailing period even though they read as short fragments. | `minWords = 0`, `minItems = 8`, `minBlocks = 2`, `maxItemWords = 8` | + +List items only. Every declarative sentence ends in a period, so sentence-level uniformity carries no signal. + +## transition-density + +Presets: `ryan`, `statistical`, `all`. + +Flags text where too many sentences open on a transition or connector word. + +| Flagged | Fix | +|---|---| +| Flags text where too many sentences open on a transition or connector word. | `minWords = 150`, `minSentences = 8`, `maxDensity = 0.3` | + +Document-wide connector rate. The transitions pack flags single openers; this one flags the habit. + +## transitions + +Presets: `default`, `ryan`, `technical`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. + +Connectors and scene setters that add length without adding a fact. + +| Flagged | Fix | +|---|---| +| `Moreover`, `Furthermore`, `Additionally` (sentence-initial) | cut, or "and", "also" | +| `Notably`, `Importantly`, `Interestingly`, `Surprisingly` (sentence-initial) | cut | +| `in today's` | name the context or cut | +| `in an era where`, `in an era of` | cut | +| `in the evolving world of`, `in the evolving field of`, `in the changing world of`, `in the changing field of`, `in the rapidly evolving world of`, `in the rapidly evolving field of`, `in the rapidly changing world of`, `in the rapidly changing field of` | lead with the point | +| `worth noting`, `it's worth noting`, `it is worth noting` | state the fact | +| `when it comes to` | name the thing | +| `here's what's interesting`, `here's what caught my eye`, `here's what stood out`, `here's the interesting part` | state the fact | +| `that said`, `that being said` | cut, or "but" | +| `Certainly`, `Undoubtedly`, `Without a doubt` | cut | + ## undue-emphasis Presets: `ryan`, `all`. Source: Pangram signs-of-ai-writing frequency counts and Wikipedia:Manual of Style/Words to watch. @@ -179,7 +241,7 @@ A hit here is a writing fix and says nothing about who wrote the text. ## ai-attribution -Presets: `default`, `ryan`, `technical`, `minimal`, `git`, `all`. Source: Group G research: claude-code#1296, Codex CLI commit attribution, VS Code #314311, Cursor forum #168258, docs.devin.ai. +Presets: `default`, `ryan`, `technical`, `minimal`, `git`, `all`. Source: Group G research: claude-code#1296, Codex CLI commit attribution, VS Code #314311, Cursor forum #168258, docs.devin.ai, code.claude.com settings reference (attribution.sessionUrl), Cursor forum #154494, GitHub changelog 2026-03-20. A commit or PR trailer credits an AI tool as co-author, generator, or signer. @@ -191,6 +253,9 @@ A commit or PR trailer credits an AI tool as co-author, generator, or signer. | a `Generated-By:` trailer | drop the trailer | | `This commit was generated by` | drop the disclosure sentence | | an `Assisted-by:` trailer | drop the trailer | +| a `Claude-Session:` trailer | drop the session trailer | +| a `Made-with: Cursor` trailer | drop the trailer | +| an `Agent-Logs-Url:` trailer | drop the trailer | | `Signed-off-by:` plus a `[bot]` account | drop the bot sign-off | | `Signed-off-by:` plus an AI tool email | drop the bot sign-off | @@ -223,63 +288,4 @@ The commit author or committer field names an AI tool instead of a person. | `--author=` naming a bare AI tool name | use a human author name | | `GIT_AUTHOR_NAME` or `GIT_COMMITTER_NAME` set to a bare AI tool name | use a human author name | -Bare first names such as `Claude`, `Devin`, and `Gemini` sit at tier 2, so a person who sets their own identity with a personal email produces one hit and stays quiet. A repo that commits as a bot on purpose lists `ai-identity` in `excludePacks`. - -## ai-tool-mention - -Presets: `default`, `ryan`, `technical`, `git`, `all`. Source: Group G research: anthropics/claude-code#1296 and anthropics/claude-code#66602. - -Names of AI coding tools, or the agent narrating its own process instead of the change. - -| Flagged | Fix | -|---|---| -| `as requested by the user` | state the change directly | -| `per the user's instructions` | state the change directly | -| `per the assistant`, `per the model`, `per the AI` | state the change directly | -| `as suggested by the assistant`, `as suggested by the model`, `as suggested by the AI` | state the change directly | -| `the model suggested`, `the assistant wrote` | state the change directly | -| `AI-generated`, `AI generated` | cut the disclosure | -| `with AI assistance` | cut the disclosure | -| `generated by AI`, `written by an AI` | cut the disclosure | -| `generated by` or `written with` a named AI tool | cut the tool name | -| a `git commit` command naming an AI tool | cut the tool name | - -A tool name reports only next to process wording (`generated by`, `written by`, `suggested by`) or inside a `git commit` command, so a comment that just names a tool stays clean. That wording around `the model` or `the assistant` reports as well, which a repo whose subject is model output drops with `disablePatterns`. - -## benefit-tail - -Presets: `ryan`, `technical`, `git`, `all`. Source: Group G research: AI-generated PR summary and commit body conventions. - -A sentence ends in a justification clause tacked on after a comma. - -| Flagged | Fix | -|---|---| -| `, which improves readability` and the like | cut the justification clause | -| `, making it easier to maintain` | cut the justification clause | -| `, for better performance` | cut the justification clause | -| `, ensuring consistency` | cut the justification clause | -| `, so that it's more robust` | cut the justification clause | - -The noun list stays abstract on purpose. A tail that names a measured effect, such as `cuts p99 latency to 90ms`, does not match. - -## canned-review - -Presets: `default`, `ryan`, `technical`, `git`, `all`. Source: Group G research: PR review boilerplate. - -Stock PR-review phrasing used regardless of what the diff actually does. - -| Flagged | Fix | -|---|---| -| `LGTM with minor nits` | cut, or approve plainly | -| `Looks good to me` | cut, or approve plainly | -| `Nice work` | cut | -| `Great work on this` | cut | -| `One small suggestion` | state the suggestion | -| `Minor nitpick` | state the point | -| `Overall, this looks good` | cut, or approve plainly | -| `solid implementation` | cut | -| `well-structured` | cut, or name the structure | -| `clean implementation` | cut | -| `It might be worth considering` | state the suggestion | - -`Consider adding tests` and `I'd suggest` are left out. Careful reviewers open with them before a concrete point, which held the full list under 85% precision. +Bare first names such as `Claude`, `Devin`, and `Gemini` sit at tier 2, so a person who sets their own identity with a personal email produces one hit and stays quiet. A repo that intentionally commits as a bot lists `ai-identity` in `excludePacks`. diff --git a/plugins/concise/skills/concise-rules/references/ai-speak-patterns-4.md b/plugins/concise/skills/concise-rules/references/ai-speak-patterns-4.md index 90c2e22..2032fd6 100644 --- a/plugins/concise/skills/concise-rules/references/ai-speak-patterns-4.md +++ b/plugins/concise/skills/concise-rules/references/ai-speak-patterns-4.md @@ -5,6 +5,65 @@ Built from the packs in `hooks/lib/patterns`. Each section names its own source. The category index is in `ai-speak-patterns.md`. +## ai-tool-mention + +Presets: `default`, `ryan`, `technical`, `git`, `all`. Source: Group G research: anthropics/claude-code#1296 and anthropics/claude-code#66602. + +Names of AI coding tools, or the agent narrating its own process instead of the change. + +| Flagged | Fix | +|---|---| +| `as requested by the user` | state the change directly | +| `per the user's instructions` | state the change directly | +| `per the assistant`, `per the model`, `per the AI` | state the change directly | +| `as suggested by the assistant`, `as suggested by the model`, `as suggested by the AI` | state the change directly | +| `the model suggested`, `the assistant wrote` | state the change directly | +| `AI-generated`, `AI generated` | cut the disclosure | +| `with AI assistance` | cut the disclosure | +| `generated by AI`, `written by an AI` | cut the disclosure | +| `generated by` or `written with` a named AI tool | cut the tool name | +| a `git commit` command naming an AI tool | cut the tool name | + +A tool name reports only next to process wording (`generated by`, `written by`, `suggested by`) or inside a `git commit` command, so a comment that just names a tool stays clean. That wording around `the model` or `the assistant` reports as well, which a repo whose subject is model output drops with `disablePatterns`. + +## benefit-tail + +Presets: `ryan`, `technical`, `git`, `all`. Source: Group G research: AI-generated PR summary and commit body conventions. + +A sentence ends in a justification clause tacked on after a comma. + +| Flagged | Fix | +|---|---| +| `, which improves readability` and the like | cut the justification clause | +| `, making it easier to maintain` | cut the justification clause | +| `, for better performance` | cut the justification clause | +| `, ensuring consistency` | cut the justification clause | +| `, so that it's more robust` | cut the justification clause | + +The noun list is intentionally abstract. A tail that names a measured effect, such as `cuts p99 latency to 90ms`, does not match. + +## canned-review + +Presets: `default`, `ryan`, `technical`, `git`, `all`. Source: Group G research: PR review boilerplate. + +Stock PR-review phrasing used regardless of what the diff actually does. + +| Flagged | Fix | +|---|---| +| `LGTM with minor nits` | cut, or approve plainly | +| `Looks good to me` | cut, or approve plainly | +| `Nice work` | cut | +| `Great work on this` | cut | +| `One small suggestion` | state the suggestion | +| `Minor nitpick` | state the point | +| `Overall, this looks good` | cut, or approve plainly | +| `solid implementation` | cut | +| `well-structured` | cut, or name the structure | +| `clean implementation` | cut | +| `It might be worth considering` | state the suggestion | + +`Consider adding tests` and `I'd suggest` are left out. Careful reviewers open with them before a concrete point, which held the full list under 85% precision. + ## file-narration Presets: `ryan`, `technical`, `git`, `all`. Source: Group G research: git commit-message conventions and conventional commits. @@ -37,6 +96,26 @@ Review or reply text opens and closes with praise around a middle critique. Needs praise in the first and last paragraph and a critique marker between them. People are taught this shape too, so the pack stays out of the `default` preset. +## hidden-characters + +Presets: `default`, `ryan`, `technical`, `git`, `all`. Source: Opus 5.5 replies (U+2011 at 0.33 per 10,000 words), Microsoft known issue on LLM hyphens (Apr 2026), OpenAI U+202F reports (2025), Trojan Source bidi controls, Wikipedia:Signs of AI writing. + +Invisible or look-alike characters that model output and chat UIs leave in text. + +| Flagged | Fix | +|---|---| +| a zero-width space, zero-width non-joiner, word joiner, or Mongolian vowel separator | remove the zero-width character | +| a zero-width joiner outside an emoji sequence | remove the zero-width joiner | +| a byte order mark after the first character | remove the byte order mark | +| a soft hyphen or non-breaking hyphen | use `-`, or remove the soft hyphen | +| a narrow no-break space or figure space | use a plain space | +| a Unicode tag character | remove the tag character, it can hide text | +| a variation selector other than the emoji and text selectors | remove the variation selector, it can hide data | +| a bidirectional control character | remove the bidi control | +| `citeturn0search0`, `contentReference[oaicite:0]` | remove the citation residue | + +Anthropic's text watermark adds no characters, so this pack cannot see it. U+00A0 is left out: npm READMEs carry it 5.6 times per 10,000 words. A zero-width joiner inside an emoji sequence and tag characters after the black flag (subdivision flags) stay unflagged. A right-to-left project lists `hidden-characters` in `excludePacks` or drops the bidi row with its own pack. + ## smart-punctuation Presets: `default`, `ryan`, `technical`, `all`. Source: Group P research: Wikipedia, Signs of AI writing. @@ -50,7 +129,7 @@ Curly quotes and the ellipsis character typical of text pasted from a chat UI. | a curly apostrophe | use a straight apostrophe | | an ellipsis character | use three periods | -Prose files stay out of scope on purpose: a whole-file scan re-reports typeset quotes that a person committed once. A project that wants `files` coverage overrides the pack. +Prose files are intentionally out of scope: a whole-file scan re-reports typeset quotes that a person committed once. A project that wants `files` coverage overrides the pack. ## unicode-glyphs diff --git a/plugins/concise/skills/concise-rules/references/ai-speak-patterns.md b/plugins/concise/skills/concise-rules/references/ai-speak-patterns.md index adc2b29..8be65a9 100644 --- a/plugins/concise/skills/concise-rules/references/ai-speak-patterns.md +++ b/plugins/concise/skills/concise-rules/references/ai-speak-patterns.md @@ -7,6 +7,8 @@ Phrase patterns are case-insensitive. The left column holds the flagged text, th | Category | Label | Pack | Presets | File | |---|---|---|---|---| +| `claude-tells` | Claude tells | `backtick-branches`, `claude-tells`, `closing-offer` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns.md` | +| `british-spelling` | British spelling | `british-spelling` | `ryan`, `all` | `ai-speak-patterns.md` | | `chatbot` | chatbot phrases | `chatbot` | `default`, `ryan`, `technical`, `minimal`, `all` | `ai-speak-patterns.md` | | `closers` | closers | `closers` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns.md` | | `contrast` | contrast frames | `contrast` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns.md` | @@ -16,10 +18,10 @@ Phrase patterns are case-insensitive. The left column holds the flagged text, th | `filler` | filler | `filler` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns.md` | | `formatting` | formatting | `formatting` | `default`, `ryan`, `all` | `ai-speak-patterns.md` | | `hedging` | hedging | `hedging` | `ryan`, `ste`, `all` | `ai-speak-patterns.md` | -| `inflation` | significance inflation | `inflation` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns.md` | -| `lexical-diversity` | lexical diversity | `lexical-diversity` | `statistical`, `all` | `ai-speak-patterns.md` | -| `negative-parallelism` | negative parallelism | `negative-parallelism` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns.md` | -| `outline-conclusion` | outline-restating conclusions | `outline-conclusion` | `ryan`, `all` | `ai-speak-patterns.md` | +| `inflation` | significance inflation | `inflation` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-2.md` | +| `lexical-diversity` | lexical diversity | `lexical-diversity` | `statistical`, `all` | `ai-speak-patterns-2.md` | +| `negative-parallelism` | negative parallelism | `negative-parallelism` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-2.md` | +| `outline-conclusion` | outline-restating conclusions | `outline-conclusion` | `ryan`, `all` | `ai-speak-patterns-2.md` | | `overgeneralization` | overgeneralization | `overgeneralization` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-2.md` | | `paragraph-coherence` | paragraph coherence | `paragraph-coherence` | `ryan`, `statistical`, `all` | `ai-speak-patterns-2.md` | | `parallel-bullets` | parallel bullets | `parallel-bullets` | `ryan`, `all` | `ai-speak-patterns-2.md` | @@ -33,10 +35,10 @@ Phrase patterns are case-insensitive. The left column holds the flagged text, th | `ste` | simplified technical english | `ste` | `ste`, `all` | `ai-speak-patterns-2.md` | | `structure` | openers and frames | `structure` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-2.md` | | `superficial-analysis` | superficial analysis | `superficial-analysis` | `ryan`, `all` | `ai-speak-patterns-2.md` | -| `sycophancy` | sycophancy | `sycophancy` | `default`, `ryan`, `technical`, `minimal`, `all` | `ai-speak-patterns-2.md` | -| `terminal-punctuation` | terminal punctuation uniformity | `terminal-punctuation` | `statistical`, `all` | `ai-speak-patterns-2.md` | -| `transition-density` | transition density | `transition-density` | `ryan`, `statistical`, `all` | `ai-speak-patterns-2.md` | -| `transitions` | transitions | `transitions` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-2.md` | +| `sycophancy` | sycophancy | `sycophancy` | `default`, `ryan`, `technical`, `minimal`, `all` | `ai-speak-patterns-3.md` | +| `terminal-punctuation` | terminal punctuation uniformity | `terminal-punctuation` | `statistical`, `all` | `ai-speak-patterns-3.md` | +| `transition-density` | transition density | `transition-density` | `ryan`, `statistical`, `all` | `ai-speak-patterns-3.md` | +| `transitions` | transitions | `transitions` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-3.md` | | `undue-emphasis` | undue emphasis | `undue-emphasis` | `ryan`, `all` | `ai-speak-patterns-3.md` | | `vague-attribution` | vague attribution | `vague-attribution` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-3.md` | | `vocabulary` | AI frequency words | `vocabulary` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-3.md` | @@ -44,14 +46,75 @@ Phrase patterns are case-insensitive. The left column holds the flagged text, th | `wordiness` | wordiness | `wordiness` | `ryan`, `ste`, `all` | `ai-speak-patterns-3.md` | | `ai-attribution` | AI attribution trailer | `ai-attribution` | `default`, `ryan`, `technical`, `minimal`, `git`, `all` | `ai-speak-patterns-3.md` | | `ai-identity` | AI author identity | `ai-identity` | `default`, `ryan`, `technical`, `minimal`, `git`, `all` | `ai-speak-patterns-3.md` | -| `ai-tool-mention` | AI tool mention | `ai-tool-mention` | `default`, `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-3.md` | -| `benefit-tail` | benefit tail | `benefit-tail` | `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-3.md` | -| `canned-review` | canned review | `canned-review` | `default`, `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-3.md` | +| `ai-tool-mention` | AI tool mention | `ai-tool-mention` | `default`, `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-4.md` | +| `benefit-tail` | benefit tail | `benefit-tail` | `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-4.md` | +| `canned-review` | canned review | `canned-review` | `default`, `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-4.md` | | `file-narration` | file narration | `file-narration` | `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-4.md` | | `praise-sandwich` | praise sandwich | `praise-sandwich` | `ryan`, `git`, `all` | `ai-speak-patterns-4.md` | +| `hidden-characters` | hidden characters | `hidden-characters` | `default`, `ryan`, `technical`, `git`, `all` | `ai-speak-patterns-4.md` | | `smart-punctuation` | smart punctuation | `smart-punctuation` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-4.md` | | `unicode-glyphs` | unicode glyphs | `unicode-glyphs` | `default`, `ryan`, `technical`, `all` | `ai-speak-patterns-4.md` | +## claude-tells + +Presets: `default`, `ryan`, `technical`, `all`. Source: Measured on 1.29M words of Opus 5.5 Claude Code replies against 1.13M words of npm READMEs and 87K words of pre-2023 commits; Anthropic's Opus 5.5 and Fable 5.1 prompting guides; Anthropic's Opus 5.5 prompting guide lists "an offer to carry on with something unless the user would prefer otherwise" as a habit to suppress; 1.93 per 10,000 words in Opus 5.5 replies, none in the baselines. + +Phrasing that Claude 5.x models write far more often than people do. + +| Flagged | Fix | +|---|---| +| Phrasing that Claude 5.x models write far more often than people do. | `branches = ["main","master","dev","develop","development","staging","stage","production","prod","release","trunk","next","beta","canary"]` | +| `the doc admits`, `the code pretends`, `the test lies` | say what the text states: `the doc says X` | +| `admits it`, `concedes as much` | say what the text states | +| `Existing bug, not a regression` | cut the `not X` tail, the label already says it | +| `the big migration`, `the huge refactor` | cut the size word, or give the size | +| `covers this path`, `hits that path` | name the branch: the function, condition, or flag | +| `on purpose` | put `intentionally` before the verb, or give the reason | +| `, by design.`, `, as intended.` | cut the tag, or give the reason | +| `, with your low call volume note` | put the aside in parentheses, or make it a clause | +| `quietly` | cut it, or say what is not logged or reported | +| `earns its keep`, `earns its place` | say what it does | +| `Want me to ...?` or `Do you want me to ...?` at the end of the text | cut the offer, or name the step that needs approval | + +| Flagged (tier 2) | Fix | +|---|---| +| `a real bug`, `the real gap`, `the real story` | cut `real` | +| `reads well`, `reads cleanly` | say what works | + +Flags a branch name in backticks after `from`, `into`, `onto`, `against`, or a similar word, or before `branch`. Prose files stay out of scope: docs often set branch names as code by convention. Set `options.branches` to match the repo's branch names. + +Every tier-1 row fired at most 0.02 times per 10,000 words in the README and commit baselines. The status-label row needs a verbless fragment at the start of a line or sentence, so `This is a known limitation, not a bug` stays with the `contrast` pack. The closing offer lives in `closing-offer.json` so it can be excluded alone. + +Fires only when the offer is the last sentence of the text. A question followed by a reason, such as `Do you want me to delete it? It has two unmerged commits.`, stays unflagged. A setup that asks before every test run can list `closing-offer` in `excludePacks`. + +## british-spelling + +Presets: `ryan`, `all`. Source: Measured on Claude Code transcripts: Opus 5.5 wrote `behaviour` in 31% of uses, `labelled` in 58%, and `grey` in 77%; npm READMEs used them in 11%, 20%, and 30%. + +British spellings in text that otherwise uses American English. + +| Flagged | Fix | +|---|---| +| `behaviour`, `behaviours`, `behavioural` | behavior | +| `colour`, `colours`, `coloured` | color | +| `favour`, `honour`, `neighbour`, `labour`, `flavour`, `humour`, `rumour`, `harbour`, `armour`, `vapour` | drop the `u`: `favor`, `honor`, `neighbor` | +| `labelled`, `modelling`, `travelled`, `signalled`, `levelled`, `channelled` | one `l`: `labeled`, `modeling` | +| `centre`, `metre`, `litre`, `fibre`, `theatre` | `-er`: `center`, `meter` | +| `-ise` and `-isation` forms such as `normalise`, `initialised`, `organisation` | `-ize`, `-ization` | +| `analyse`, `analysed`, `analysing` | `-yze`: `analyze` | +| `grey`, `greys`, `greyed`, `greying` | gray | +| `whilst` | while | +| `amongst` | among | +| `programme`, `programmes` | program | +| `licence[ds]?` | license | +| `defence`, `offence` | defense, offense | +| `catalogue[ds]?` | catalog | +| `artefact`, `artefacts` | artifact | +| `spelt`, `learnt` | spelled, learned | +| `per cent` | percent | + +A repo that writes British English lists `british-spelling` in `excludePacks`. `cancelled`, `judgement`, `acknowledgement`, `towards`, and `dialogue` are left out because American writers use them too. `analyses` is left out because it is also the plural of `analysis`. Elixir's `@behaviour` attribute sits in code, which the scan skips. + ## chatbot Presets: `default`, `ryan`, `technical`, `minimal`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. @@ -227,65 +290,3 @@ Qualifiers that leave the claim unowned. | `somewhat`, `to some extent` | cut, or measure | | `it could be argued`, `it could be said`, `it may be argued`, `it may be said`, `it might be argued`, `it might be said` | state the claim | | `potentially` (bare) | cut | - -## inflation - -Presets: `default`, `ryan`, `technical`, `all`. Source: avoid-ai-writing by Conor Bronsdon (MIT) and Ryan Yannelli's house rules. - -Wording that makes a change sound larger than it is. - -| Flagged | Fix | -|---|---| -| `marks a significant`, `marks a major`, `marks a new`, `marks a turning` | state what happened | -| `represents a significant`, `represents a major`, `represents a fundamental` | state what changed | -| `a significant step forward`, `a major step towards`, `a bold step toward`, `a big step forward`, `step forward for` | state what changed | -| `turning point`, `sea change`, `paradigm shift`, `game-changer`, `game-changing`, `watershed moment` | state what changed | -| `groundbreaking` | name the prior art it beats | -| `first of its kind` | cite the search | -| `revolutionary`, `revolutionize`, `revolutionizes`, `revolutionized` | describe the change | -| `the future of` | cut | -| `unprecedented` | name the precedent, or cut | -| `redefine the`, `redefine how`, `redefine what`, `redefines the`, `redefines how`, `redefines what`, `reshape the`, `reshapes the`, `reshaping the` | describe the change | -| `real utility`, `actual value`, `genuine impact`, `true change`, `real results` | drop the adjective | -| `despite challenges`, `despite these challenges`, `despite the challenges` | name the challenge | -| `continues to thrive`, `remains resilient` | cite the number | - -## lexical-diversity - -Presets: `statistical`, `all`. - -Flags text that reuses the same words instead of varying vocabulary. - -| Flagged | Fix | -|---|---| -| Flags text that reuses the same words instead of varying vocabulary. | `minWords = 150`, `minRatio = 0.55`, `segment = 100`, `tailSegment = 50` | - -Mean segmental type-token ratio over 100-word segments. Direction of the academic signal is contested, so it stays out of the ryan preset. - -## negative-parallelism - -Presets: `default`, `ryan`, `technical`, `all`. Source: Wikipedia:Signs of AI writing, WikiProject AI Cleanup catchphrases, tropes.fyi, refine.so. - -A chain of two or more negated clauses resolved by one affirmation. - -| Flagged | Fix | -|---|---| -| `Not X. Not Y. Just Z.` | state the one claim | -| `no X, no Y, just Z` | state the one claim | -| `not X, not Y, but Z` | state the one claim | -| `no X, no Y. Just Z.` | state the one claim | -| `isn't X. It's not Y. It's Z.` | state the one claim | - -The contrast pack owns the single "not X, it's Y" pair. This pack fires only on two or more stacked negations before the resolution. - -## outline-conclusion - -Presets: `ryan`, `all`. Source: Wikipedia:Signs of AI writing (outline-like conclusions) and tropes.fyi fractal summaries. - -A closing paragraph that re-lists the section headings instead of adding a new fact. - -| Flagged | Fix | -|---|---| -| A closing paragraph that re-lists the section headings instead of adding a new fact. | `minWords = 300`, `minHeadings = 3`, `minOverlap = 3`, `minRatio = 0.6`, `smallDoc = 4`, `maxParagraphWords = 120` | - -Needs 300 words, three headings, a closing paragraph that echoes three section titles, and a restatement cue. A closing paragraph that adds a new fact passes. diff --git a/plugins/concise/skills/concise-tune/SKILL.md b/plugins/concise/skills/concise-tune/SKILL.md index 771d26f..c4a95fc 100644 --- a/plugins/concise/skills/concise-tune/SKILL.md +++ b/plugins/concise/skills/concise-tune/SKILL.md @@ -1,6 +1,6 @@ --- name: concise-tune -description: Tune the concise plugin settings to match samples of the user's own writing. Use when the user shares writing samples, asks concise to match their style, or says concise flags text they write on purpose. +description: Tune the concise plugin settings to match samples of the user's own writing. Use when the user shares writing samples, asks concise to match their style, or says concise flags text they write intentionally. --- # Tune concise from writing samples diff --git a/plugins/concise/test/packs-hooks-tests.mjs b/plugins/concise/test/packs-hooks-tests.mjs index 4b74f61..99b200f 100644 --- a/plugins/concise/test/packs-hooks-tests.mjs +++ b/plugins/concise/test/packs-hooks-tests.mjs @@ -218,6 +218,17 @@ console.log("\npacks in the hooks (scope)"); assertAllowed("a comments-scoped pack stays quiet in prose", run(CHECK_EDIT, writeEvent(c, "doc.md", "We frobnicate it.\n"))); } +console.log("\npacks in the hooks (code spans through ctx.raw)"); + +{ + const c = project({ categories: ["claude-tells"] }); + const flagged = run(CHECK_BASH, bashEvent(c, "gh pr create --title x --body 'Open a PR from `staging` into `main`.'")); + assertDenied("a backticked branch name in a PR body is flagged", flagged); + includes("the flagged text keeps its backticks", flagged, "`staging`"); + const files = "gh pr create --title x --body 'Move the helper from `config.ts` into `utils.ts`.'"; + assertAllowed("a backticked file name stays quiet", run(CHECK_BASH, bashEvent(c, files))); +} + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : ""; diff --git a/plugins/concise/test/patterns-cases/claude.json b/plugins/concise/test/patterns-cases/claude.json new file mode 100644 index 0000000..af5fda3 --- /dev/null +++ b/plugins/concise/test/patterns-cases/claude.json @@ -0,0 +1,30 @@ +{ + "positive": { + "claude-tells": "The big migration can lose call data during the deploy.", + "british-spelling": "The plugin falls back to the old behaviour.", + "hidden-characters": "Test intake with a made\u2011up case." + }, + "nearMiss": { + "claude-tells": "The migration can lose call data during the deploy.", + "british-spelling": "The plugin falls back to the old behavior.", + "hidden-characters": "Test intake with a made-up case." + }, + "presets": { + "default": [ + "claude-tells", + "hidden-characters" + ], + "ryan": [ + "claude-tells", + "british-spelling", + "hidden-characters" + ], + "technical": [ + "claude-tells", + "hidden-characters" + ], + "git": [ + "hidden-characters" + ] + } +} diff --git a/plugins/concise/test/patterns-cases/rhetorical.json b/plugins/concise/test/patterns-cases/rhetorical.json index 08c9114..e5341d2 100644 --- a/plugins/concise/test/patterns-cases/rhetorical.json +++ b/plugins/concise/test/patterns-cases/rhetorical.json @@ -10,7 +10,7 @@ "undue-emphasis": "You must NEVER call this function from a signal handler.", "parallel-bullets": "- Improved caching\n- Improved logging\n- Improved error handling\n", "rule-of-three": "The API is simple, consistent, and well-documented, and the SDK is lightweight, typed, and dependency-free.\n\nThe parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer. The parser reads one token at a time from the input buffer.\n", - "outline-conclusion": "## Setup\n\nInstall the package with npm, then copy the sample token into a local env file. Most teams keep that file out of version control. If you already run a proxy, point the base URL at it before the first call. A wrong port here shows up later as a timeout, which wastes an afternoon. Windows users should set the cache directory by hand.\n\n## Usage\n\nCall run() with a path. It returns a promise that resolves to a report object; the report carries counts, offsets, and any warnings the reader produced. Streaming mode exists for very large inputs, though most callers never need it. Prefer the sync helper when your input fits in memory, because backpressure handling adds real complexity for no gain. The CLI wraps the same entry point.\n\n## Troubleshooting\n\nRead the log first. Almost every failure prints a stack trace with the offending line number, and the trace names the rule that rejected the input. Turn on verbose output when the trace looks empty. Corrupted caches cause the strangest reports, so clearing the cache directory is worth trying early. Network errors retry three times before they surface, which can make a fast failure look slow.\n\n## Limits\n\nTwo gigabytes is the practical ceiling for a single document. Beyond that, split the input by chapter and merge the reports afterwards. Nested archives are skipped rather than expanded, and symlinks outside the project root raise an error instead of following quietly. Nothing here is configurable yet, though a flag is planned for the next minor release.\n\n## Reporting a bug\n\nOpen an issue with the failing input attached, or a reduced case if the file is private. Version numbers matter more than stack traces here, since the parser changed shape twice last year. Maintainers usually reply within two working days.\n\n## Conclusion\n\nAs we've seen, setup takes one command, usage is a single function call, and troubleshooting just means reading the log file.\n" + "outline-conclusion": "## Setup\n\nInstall the package with npm, then copy the sample token into a local env file. Most teams keep that file out of version control. If you already run a proxy, point the base URL at it before the first call. A wrong port here shows up later as a timeout, which wastes an afternoon. Windows users should set the cache directory by hand.\n\n## Usage\n\nCall run() with a path. It returns a promise that resolves to a report object; the report carries counts, offsets, and any warnings the reader produced. Streaming mode exists for very large inputs, though most callers never need it. Prefer the sync helper when your input fits in memory, because backpressure handling adds real complexity for no gain. The CLI wraps the same entry point.\n\n## Troubleshooting\n\nRead the log first. Almost every failure prints a stack trace with the offending line number, and the trace names the rule that rejected the input. Turn on verbose output when the trace looks empty. Corrupted caches cause the strangest reports, so clearing the cache directory is worth trying early. Network errors retry three times before they surface, which can make a fast failure look slow.\n\n## Limits\n\nTwo gigabytes is the practical ceiling for a single document. Beyond that, split the input by chapter and merge the reports afterwards. Nested archives are skipped rather than expanded, and symlinks outside the project root raise an error instead of following them. Nothing here is configurable yet, though a flag is planned for the next minor release.\n\n## Reporting a bug\n\nOpen an issue with the failing input attached, or a reduced case if the file is private. Version numbers matter more than stack traces here, since the parser changed shape twice last year. Maintainers usually reply within two working days.\n\n## Conclusion\n\nAs we've seen, setup takes one command, usage is a single function call, and troubleshooting just means reading the log file.\n" }, "nearMiss": { "negative-parallelism": "No config file was found, so the defaults apply.",