diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 91e49f0..c46f235 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,8 +1,8 @@ { "name": "ru-text", "metadata": { - "description": "Russian text quality — ~1,044 rules for typography, info-style, editorial, UX writing, business correspondence, and AI-text cleanup", - "version": "1.10.1" + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand.", + "version": "2.0.0" }, "owner": { "name": "Arseniy Kamyshev", @@ -13,12 +13,14 @@ { "name": "ru-text", "source": "./", - "version": "1.10.1", - "description": "Russian text quality — ~1,044 rules for typography, info-style, editorial, UX writing, business correspondence, and AI-text cleanup", + "version": "2.0.0", + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand.", "category": "productivity", "homepage": "https://github.com/talkstream/ru-text", "tags": [ "russian", + "neuroslop", + "ai-text-detection", "typography", "editorial", "ux-writing", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d62608e..04a214e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ru-text", - "version": "1.10.1", - "description": "Russian text quality plugin — typography, information style, editorial standards, UX writing, business correspondence, and AI-text cleanup", + "version": "2.0.0", + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand.", "author": { "name": "Arseniy Kamyshev", "email": "nafigator@gmail.com", @@ -12,11 +12,13 @@ "license": "MIT", "keywords": [ "russian", + "neuroslop", + "ai-text-detection", "typography", + "proofreading", "editorial", "info-style", "ux-writing", - "content-design", "text-quality", "text-scoring" ] diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7d32d76..012fdfb 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -4,9 +4,9 @@ **Repo:** https://github.com/talkstream/ru-text **Site:** https://ru-text.org **Sponsors:** https://github.com/sponsors/talkstream -**Version:** 1.10.1 | **License:** MIT | **Platforms:** Claude Code, GitHub Copilot, Windsurf, Cursor, Cline, JetBrains (Junie), Continue.dev, Codex CLI, Gemini CLI, Google Antigravity, OpenClaw, Notion +**Version:** 2.0.0 | **License:** MIT | **Platforms:** Claude Code, GitHub Copilot, Windsurf, Cursor, Cline, JetBrains (Junie), Continue.dev, Codex CLI, Gemini CLI, Google Antigravity, OpenClaw, Notion -~1,044 independently formulated rules across 7 thematic areas. No verbatim quotes, full source attribution. +Over 2,000 independently formulated linguistic atoms across 7 thematic areas. No verbatim quotes, full source attribution. ## Priorities @@ -62,14 +62,15 @@ claude plugins marketplace update ru-text # refresh marketplace cache - Reference files >100 lines must have Table of Contents - SKILL.md description must stay under 250 chars (Claude truncates beyond that) - Plugin must follow its own typography rules (dogfooding) +- **The corpus size is quoted as a floor, in atoms, and the floor is machine-counted.** «Over 2,000 linguistic atoms» / «более 2 000 лингвистических атомов». Reproduce the count with `tools/extract-atoms.sh skills/ru-text | wc -l` (2219 on 28.07.2026), and find every file that states the floor with `grep -rlE '2[ ,\xc2\xa0]000 (linguistic atoms|лингвистических атомов)|2,000\+ atoms' --include='*.json' --include='*.md' . | grep -v node_modules` — 9 on 28.07.2026, and NOT a number to memorise. This sentence used to say «in eleven files», which was itself a hand-maintained count and was itself wrong: the true figure was nine, and `.codex-plugin/plugin.json` was still saying «~1,044 rules» on main while the sentence claimed the sweep was complete. The floor replaced «~1 044 rules», a figure nobody could reproduce. A floor is chosen on purpose: it survives the corpus growing, so adding a rule does not oblige anyone to re-stamp anything. Raise it only when the count clears the next thousand, and raise it everywhere in one commit ### plugin.json (Zod strict mode — unknown fields break the plugin silently) - Valid fields: `name`, `version`, `description`, `author` (object: name, email, url), `homepage`, `repository` (string URL), `license`, `keywords` (array) - NEVER add `tags`, `category`, `source` — these belong in marketplace.json only (issue #26555) - `repository` must be a string, NOT an object — `"https://..."` not `{"type":"git","url":"..."}` - After editing plugin.json: `claude plugins validate /path/to/ru-text` before committing -- Keep versions in sync across ALL manifests: .claude-plugin (plugin.json AND marketplace.json — two fields there), .codex-plugin, .cursor-plugin, gemini-extension.json, openclaw.plugin.json — **the hardcoded «Latest version» line in README.md and README.en.md**, and **the `**Version:**` header of this file**. The v1.10.1 gate caught the READMEs still advertising the previous release, and then caught this file doing the same -- Codex CLI is pre-1.0 (v0.118.0) — plugin.json schema may change between minor versions +- Keep versions in sync across ALL manifests: .claude-plugin (plugin.json AND marketplace.json — two fields there), .codex-plugin, .cursor-plugin, gemini-extension.json, openclaw.plugin.json — and **the `**Version:**` header of this file** — eight points, and `tools/check-version.sh --print` lists them. The READMEs no longer state the version in prose: they carry a badge that renders it live from the releases API, so there is nothing there to go stale. The v1.10.1 gate caught those two prose lines advertising the previous release, and then caught this file doing the same; the badge removes the first failure mode rather than re-checking it +- Codex CLI is pre-1.0 and moves fast: 0.144.0 locally, 0.145.0 on npm as `@openai/codex` (28.07.2026). The note here said v0.118.0 for months. The plugin.json schema may change between minor versions, so re-check it at each release rather than trusting this line ### Dev workflow (local plugin testing) - Update cache after source changes: `claude plugins marketplace update ru-text` @@ -84,13 +85,16 @@ claude plugins marketplace update ru-text # refresh marketplace cache - No build step, no dependencies — pure markdown plugin - awesome-claude-code (hesreallyhim): NEVER submit via gh CLI — web form only, 14-day cooldown on violation -- Anthropic marketplaces (verified 2026-04-30): ru-text IS in `anthropics/claude-plugins-community` (slug `ru-text@claude-community`, sha-pinned to a specific commit) and IS NOT in `anthropics/claude-plugins-official` (the directory backing claude.com/plugins). Single submission form for BOTH directories: clau.de/plugin-directory-submission → claude.ai/settings/plugins/submit or platform.claude.com/plugins/submit. Anthropic curators decide tier per submission. PRs opened directly to either repo are auto-closed (read-only mirrors of Anthropic's internal pipeline). Routine version updates need NO form (verified 2026-06-09): an automated nightly `bump-plugin-shas` action + periodic Bulk-sync re-pin every listed plugin to its upstream HEAD (gated by `validate-plugins`). The ru-text pin advanced on its own from `7932d7c` (Mar) to `13d1a03` with no re-submission; just push the new version to main and wait for the next sync. The form is needed ONLY for initial listing, official-tier promotion, or when auto-bump starts failing tightened policies. PRs to either repo are still auto-closed — no manual sha-bump path +- Anthropic marketplaces (verified 2026-04-30): ru-text IS in `anthropics/claude-plugins-community` (slug `ru-text@claude-community`, sha-pinned to a specific commit) and IS NOT in `anthropics/claude-plugins-official` (the directory backing claude.com/plugins). Single submission form for BOTH directories: clau.de/plugin-directory-submission → claude.ai/settings/plugins/submit or platform.claude.com/plugins/submit. Anthropic curators decide tier per submission. PRs opened directly to either repo are auto-closed (read-only mirrors of Anthropic's internal pipeline). **Do NOT assume a release reaches community-marketplace users: measure the pin.** The old note here said routine updates need no form because a nightly `bump-plugin-shas` re-pins every plugin to upstream HEAD, so one should just push to main and wait. That was written after watching the pin move once (`7932d7c` → `13d1a03`, Mar–Jun) and it does not generalise. Measured 2026-07-28: the pin sat at `44c2da9` = **v1.8.0**, three releases behind, and no `bump(ru-text)` appears in the last 300 commits to that repo's `marketplace.json`. The sweep is alive and runs daily, but it opens at most `max-bumps: 30` per-entry PRs against 2283 entries and walks them roughly alphabetically; ru-text is at index 1653, and the early-alphabet entries go stale again faster than the queue clears. ru-text is NOT on `.github/freeze-shas.txt` and passes `claude plugins validate`, so this is starvation, not rejection. There IS a targeted lever — `workflow_dispatch` with input `plugin: ru-text` — but only someone with `actions: write` on Anthropic's repo can pull it, which is why the ask goes in an issue (precedent: #1121, opened 2026-07-16 for the same failure on another plugin, no maintainer reply after twelve days). **After every release, check the pin and file or bump the issue; never write in user-facing docs that the pin advances within a day.** Also note the listing's DESCRIPTION («~1,040 rules», «Eight reference files») was entered by a curator and does not track our manifest — we cannot correct it from this repository. PRs to either repo are still auto-closed — no manual sha-bump path - Version bump in plugin.json is REQUIRED for users to get updates (Claude Code uses version for cache invalidation) - `${CLAUDE_PLUGIN_ROOT}` is a Claude-Code-only token. Valid contexts: `.claude-plugin/plugin.json`, hooks, MCP/LSP configs, and any other file consumed only by Claude Code. NEVER use it in `skills/ru-text/SKILL.md` body or other cross-platform skill content — Codex, Cursor, Windsurf, Cline, JetBrains Junie, Continue.dev, Gemini CLI, and GitHub Copilot do not substitute it and would render the literal `${CLAUDE_PLUGIN_ROOT}/...` string in their UI (regression fixed in v1.7.2). In skill bodies, use relative paths like `references/`; the existing Glob fallback in SKILL.md covers any nonstandard marketplace layout in Claude Code - `content/originals/` contains pre-compaction backups — gitignored, do not delete - marketplace.json does NOT support `$schema` or root-level `description` — use `metadata.description` -- Codex CLI: self-serve publishing NOT available (as of v0.118.0). Users install via `/plugins` interactive menu -- Codex install command may change — verify at developers.openai.com/codex/plugins before updating docs +- **Codex and ChatGPT now share a plugin system**, and we document only half of it: «Plugins are available with ChatGPT Work on the web and with ChatGPT Work or Codex in the ChatGPT desktop app. Codex CLI also has a plugin browser» (learn.chatgpt.com/docs/plugins, 28.07.2026). ChatGPT Work users install from the **Work** switcher → **Plugins** — an audience our docs never addressed. The CLI also has a NON-interactive install we never documented: `codex plugin add @`, verified against the live 0.144.0 binary +- **Codex ships `claude-plugins-official` as a default marketplace** (`codex plugin marketplace list` on a stock install shows it alongside openai-primary-runtime, openai-bundled and openai-curated). ru-text is NOT in it — we are in `claude-plugins-community`. Consequence worth weighing: promotion to Anthropic's official tier would deliver ru-text to Codex users with zero setup on their side, which makes the official-tier ask worth more than one channel +- Codex CLI: self-serve publishing IS now available — the submission portal at platform.openai.com/plugins explicitly accepts «a skills-only plugin that packages reusable workflows», which is what ru-text is (developers.openai.com/plugins/deploy/submission.md, verified 28.07.2026). The old note said it was unavailable as of v0.118.0. A first-party listing would also remove the marketplace-add prerequisite that the third-party catalogue imposes. Director's call whether to submit +- Codex docs moved and are now split across two hosts: install and usage at learn.chatgpt.com/docs/plugins (developers.openai.com/codex/plugins 308-redirects there), manifest and packaging at developers.openai.com/plugins/build/plugins. Append `.md` to any page URL for raw markdown +- Codex install is NOT a bare `/plugins` browse: a marketplace has to be configured first — `codex plugin marketplace add /` — and a new session started before the bundled skills load - awesome-codex-plugins (hashgraph-online): community Codex marketplace where ru-text is listed. Generator-driven — `scripts/generate_plugins_json.py` fetches each plugin's repo HEAD and derives the entry's icon from `interface.composerIcon` (icon lives in `.agents/plugins/marketplace.json`, NOT root `plugins.json`; icon file must be ≤50KB, SVG preferred/PNG accepted). Push `composerIcon` + the icon file to your HEAD BEFORE the mirror PR, else the icon is silently dropped. Hard CI gate: `validate-plugin-pr.py` (run locally with `--base-ref origin/main`). Icon added v1.7.3 (`assets/icon.png`), mirror PR #162 (issue #11). Full mechanics in project memory `reference_awesome_codex_plugins.md` - GitHub Copilot: reads `.github/skills/`, `.claude/skills/`, `.agents/skills/` — all three. Also `~/.copilot/skills/` global. Submit to github/awesome-copilot (staged branch, not main) - Windsurf: reads `.windsurf/skills/` and can read `.claude/skills/` if cross-agent enabled. Manual: Cascade > Customizations > Skills. No official directory; third-party: windsurf.run @@ -100,8 +104,8 @@ claude plugins marketplace update ru-text # refresh marketplace cache - SKILL.md is cross-platform (Claude Code + GitHub Copilot + Windsurf + Cursor + Cline + JetBrains Junie + Continue.dev + Codex + Gemini CLI) — do NOT duplicate it - Cursor: marketplace at cursor.com/marketplace/publish, manual review. `.cursor-plugin/plugin.json` ready - Gemini CLI: auto-discovery via `gemini-cli-extension` GitHub topic (added). Gallery at geminicli.com/extensions/ -- OpenClaw: natively reads `.claude-plugin/` as bundles. Native manifest `openclaw.plugin.json`. ClawHub: published at clawhub.ai/talkstream/ru-text (the listing tracks the latest release; do not pin a version in this note — it goes stale). Publish CLI: `npm i -g clawhub && clawhub login && clawhub skill publish ./skills/ru-text --slug ru-text --version `. The `--version` flag is REQUIRED — omitting it fails with `Error: --version must be valid semver` +- OpenClaw: natively reads `.claude-plugin/` as bundles. Native manifest `openclaw.plugin.json`. ClawHub: published at clawhub.ai/talkstream/ru-text (the listing tracks the latest release; do not pin a version in this note — it goes stale). Publish CLI: `npm i -g clawhub && clawhub login && clawhub skill publish ./skills/ru-text --slug ru-text --version `. **Always pass `--version` explicitly on a minor or major bump.** The old note here said the flag is REQUIRED and that omitting it fails with `Error: --version must be valid semver`. That is false, and false in the direction that ships the wrong number: on clawhub 0.23.1 the guard fires only when the flag IS supplied and malformed (`dist/cli/commands/publish.js:44-45`), and omitting it defaults to the registry's NEXT PATCH. Verified by dry run — without the flag it printed «Would publish ru-text@1.10.2» and exited 0, so publishing 2.0.0 without `--version 2.0.0` would silently ship a patch. `skill publish` reads no version from the skill folder or any repo manifest, so bumping `openclaw.plugin.json` is not a safety net. Rehearse with `--dry-run --no-input` before the real publish. Install and update refs are owner-qualified: `openclaw skills install @talkstream/ru-text`, `openclaw skills update @talkstream/ru-text` — bare slugs are only tolerated for already-installed or unambiguous skills - OpenClaw indexes ru-text as a **skill** (not plugin). Users install via `openclaw skills install ru-text`, NOT `openclaw plugins install` -- Cursor: standalone skill path is `~/.cursor/skills/ru-text`. Full plugin local testing: `~/.cursor/plugins/local/ru-text/` (requires `.cursor-plugin/plugin.json`). NOT `.agents/skills/` (Vercel/npx convention, Cursor doesn't read it). PR #8 confirmed the bug; `plugins/local/` confirmed by Cursor engineer in cursor/plugin-template#4 +- Cursor: standalone skill path is `~/.cursor/skills/ru-text`. Full plugin local testing: `~/.cursor/plugins/local/ru-text/` (requires `.cursor-plugin/plugin.json`). `.agents/skills/` IS read by Cursor now, at both project and user level — cursor.com/docs/skills lists `.agents/skills/`, `.cursor/skills/`, `~/.agents/skills/`, `~/.cursor/skills/`, plus compatibility with `.claude/skills/` and `.codex/skills/` (verified 28.07.2026; the older note here said the opposite). PR #8 confirmed the bug; `plugins/local/` confirmed by Cursor engineer in cursor/plugin-template#4 - NeuralDeep (neuraldeep.ru): listed and live — the April 2026 submission passed moderation. Catalogue at neuraldeep.ru/skills accepts «формат claude-skill и любые репозитории со SKILL.md»; install CLI `npx skillsbd add owner/repo/skill`. Web form at neuraldeep.ru/submit (GitHub OAuth required) - Notion: no plugin architecture. Two integration paths: (1) Notion AI Custom Skill template page in `notion/`, (2) MCP bridge (Claude Code + Notion MCP server). Notion AI Skills require Business/Enterprise plan. Template must be self-contained (no `${CLAUDE_PLUGIN_ROOT}` paths) diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 7601148..28f6da0 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ru-text", - "version": "1.10.1", - "description": "Russian text quality plugin — typography, information style, editorial standards, UX writing, business correspondence, and AI-text cleanup", + "version": "2.0.0", + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand.", "author": { "name": "Arseniy Kamyshev", "email": "nafigator@gmail.com", @@ -12,19 +12,22 @@ "license": "MIT", "keywords": [ "russian", + "neuroslop", + "ai-text-detection", "typography", + "proofreading", "editorial", "info-style", "ux-writing", "text-quality", "text-scoring" ], - "logo": "./logo-round.png", "skills": "./skills/", "interface": { "composerIcon": "./assets/icon.png", + "logo": "./logo-round.png", "displayName": "ru-text", - "shortDescription": "Russian text quality — ~1,044 rules for typography, info-style, editorial, UX writing", + "shortDescription": "Russian text quality for AI agents — neuroslop cleanup, typography, info-style, editorial, UX writing, business correspondence", "developerName": "Arseniy Kamyshev", "category": "Productivity", "websiteURL": "https://ru-text.org", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 2be707c..98c85b6 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "ru-text", "displayName": "ru-text", - "version": "1.10.1", - "description": "Russian text quality — ~1,044 rules for typography, info-style, editorial, UX writing, business correspondence, and AI-text cleanup. Auto-activates on Russian text.", + "version": "2.0.0", + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand. Auto-activates on Russian text.", "author": { "name": "Arseniy Kamyshev", "email": "nafigator@gmail.com" @@ -11,7 +11,10 @@ "logo": "../logo-round.png", "keywords": [ "russian", + "neuroslop", + "ai-text-detection", "typography", + "proofreading", "editorial", "info-style", "ux-writing", diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index 8d29d85..a3b904e 100644 --- a/.github/workflows/gates.yml +++ b/.github/workflows/gates.yml @@ -31,9 +31,12 @@ jobs: - name: Tools are executable and POSIX-parseable # A syntax error in a checker reads as a passing checker to anything that only # looks at whether the file exists. + # `find`, not the `tools/*.sh` glob: the glob stops at the top level, and a checker + # one directory down was invisible here and to tools/gates.sh both. Kept identical + # to the loop in tools/gates.sh — tools/selftest.sh fails when the two diverge. run: | set -eu - for f in tools/*.sh; do + for f in $(find tools -name '*.sh' -type f | sort); do [ -x "$f" ] || { echo "not executable: $f"; exit 1; } sh -n "$f" || { echo "syntax error: $f"; exit 1; } done @@ -45,6 +48,23 @@ jobs: - name: check-frozen — the paid-MCP corpus contract run: tools/check-frozen.sh + - name: check-version — one version, one description budget, one set of triggers + # The description is the entire trigger on hosts that have no instruction file. + # Losing a Russian phrase there stops the skill firing for the people it is for, + # silently, with every other gate still green. + run: tools/check-version.sh + + - name: check-dogfood — the numbers the product states about itself + # The catalogue size lives in seven files. Four of them went stale unnoticed + # the last time it moved, which is why the completeness guard exists. + run: tools/check-dogfood.sh + + - name: check-typography — the product obeys its own typography + # The README claims every dash, quote and space in it follows the plugin's own + # rules. Until this step that claim rested on someone remembering to ask a model, + # and in one afternoon a literal ~ and a missing non-breaking space both shipped. + run: tools/check-typography.sh + - name: No-loss gate — every atom of the pinned baseline still accounted for # The baseline is the v1.10.1 corpus, frozen as a historical artefact: it is NOT # regenerated when the corpus changes. That is the whole point — it is the "before" @@ -62,3 +82,8 @@ jobs: PIN tools/extract-atoms.sh skills/ru-text > /tmp/atoms-now.tsv tools/diff-atoms.sh tools/baseline/atoms-v1.10.1.tsv /tmp/atoms-now.tsv + + - name: The release assets can still be built + # Writes nothing. A renamed reference file leaves the skill installing and + # answering from whatever it still has — a failure with no symptom. + run: tools/build-release.sh --check diff --git a/.gitignore b/.gitignore index 18ff08a..728cc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ scratchpad/ .playwright-mcp/ ru-text-landing-*.png warn-callout.png +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 231bbe8..b4c5be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,213 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2026-07-30 + +### Breaking changes + +The same text can now come back with a different verdict. Nothing was removed and no command +changed its interface, but four changes move findings and labels, and anyone who pinned a +score or wrote a test against `/ru-score` output should re-run it before upgrading. + +- **A document can be held below «Хороший» by a single rule.** AD-14 (the piece is a chat + transcript) and AD-15 (the piece is addressed to a search engine) are charged to the whole + document and put a floor under the top two labels. A text that scored 8.4 and was called + «Хороший» in 1.10.1 keeps the 8.4 and loses the word. +- **The AD-7 register carve-out no longer covers an author writing about their own text.** + It protects a speaker *inside* the text — dialogue, quotation, a character. Conversational + self-description that passed under the old wording is now a finding. +- **Nine new tells and three new grammar sections mean more findings on unchanged text.** + AD-10…AD-16, plus §I verb government, §J gerund phrases with a mismatched subject and §K + context-dependent homophones. Text that was clean against 1.10.1 can be clean against 2.0.0 + and still score lower, because the Structure and Precision dimensions now see more. +- **The per-domain rule counts are gone from both READMEs.** If you quoted «96 typography + rules» from this project, there is no longer a number there to quote — see «Changed» for + why, and quote `tools/extract-atoms.sh skills/ru-text | wc -l` instead. + +### Added +- **Seven tells of machine-written Russian**, measured against the golden set before they + shipped: AD-10 declared sincerity · AD-11 mandatory tricolon · AD-12 hollowed mechanism · + AD-13 phantom attribution · AD-14 chat transcript as the artifact · AD-15 search-engine + addressee · AD-16 additive pseudo-pair. AD-14 and AD-15 are the first rules in this set + charged to the **document** rather than to a fragment — the defect is the shape of the + piece, and no local edit removes it. Nine further candidates were rejected on the record; + two of those rejections matter most, because even sentence rhythm and vocabulary poverty + are what detectors mistake for machine text and also what dry regulatory prose and + non-native Russian look like. +- **The README asks the agent to install the skill, instead of teaching the human to.** The + «Быстрый старт» section was thirteen platform-by-platform recipes, 195 of the file's 411 + lines. It is now one sentence to hand to an AI agent — «Установи навык + https://github.com/talkstream/ru-text глобально и вызывай его для любых задач с русским + текстом» — because an agent knows where its own platform keeps skills better than an + instruction written a year ago does. The README lost half its length. + The decision was tested rather than argued: three fresh agents were handed nothing but that + sentence, in sandboxes, role-framed as three different platforms' agents. Two installed + correctly; one put Codex's copy in `~/.codex/skills` on the strength of a December-2025 + blog post, while OpenAI's current documentation says user skills live in + `$HOME/.agents/skills`. All three read this repository's README as their first source — + which is why the per-platform material was moved rather than deleted. +- **`INSTALL.md` / `INSTALL.en.md`** — everything the one-liner does not carry, organised + around the fact the audit turned up: `~/.agents/skills/` is read by Codex, Cursor, Windsurf + AND GitHub Copilot, so this is one shared path plus exceptions rather than thirteen silos. + Also there: the Claude Desktop and Notion click-paths, which no agent can drive; the four + negative facts trial cannot discover (ru-text is absent from the Cursor marketplace, cloud + sessions do not inherit the plugin, the community pin trails by months, `npx skills add` + installs three skills); and the update story a one-shot install does not have. +- **`tools/install-paths.tsv`** — where each platform loads a skill from, with the vendor URL + and the date it was read. One source for the INSTALL tables and for the probe's assertions, + so the two cannot drift apart. +- **`tools/probe-install.sh`** — the gate that replaces reading prose against vendor docs. It + builds a sandbox, prints the one-line prompt, and afterwards judges the disk: did the skill + land at a path this platform documents, did a copy land anywhere it does not, are the bytes + this corpus, did all ten reference files arrive. The second of those is the one that + matters — an agent usually writes several copies, so a check that only looked for a hit + would have blessed the `~/.codex/skills` failure. The script deliberately does not run the + agent: a shell cannot start another vendor's agent, and one that pretended to would be a + gate testing itself. Five selftest cases, one per outcome. +- **AD-17, a comma welded to a dash.** Raised by the director on reading a junction in this + project's own README: «люди так не пишут в живой жизни, даже профессионалы языка». The rule is + honest about its footing — Rozental §64 PERMITS the junction, Lebedev's Ководство §143 does not + discuss combining marks, and nothing was found from Ilyakhov, so the rule claims no source + forbids it. It observes that living prose avoids what the norm allows, which is what `addenda.md` + is for. Five carve-outs, and the first is load-bearing: the trigger is a JUNCTION, not the + character pair, so direct speech («„Хороший вопрос“, — ответил инженер») is two constructions and + never flagged. +- **§A.1 of `editorial-punctuation.md` gains the particle rule.** A restrictive particle in front + of a conjunction moves the comma left rather than removing it: «вызывай ru-text, только когда я + прошу». The gap was found by a check that had to reach outside the corpus to catch it (ПАС + §116–117, Розенталь §33.6) — nothing in the corpus could have. +- **A model may call the check again, and it now starts cheap.** `ru-check` and `ru-score` carried + `disable-model-invocation: true`, which solved a real cost problem — a 15–20k-token check firing + on any Russian text in sight — by making the tool unreachable to the agent that was supposed to + run it. The flag is gone, the descriptions now carry the Russian phrases a person actually says + («вычитай», «прогони ru-text», «оцени текст»), and the cost problem is solved where it lives: + an explicit request reads the whole corpus, while a self-initiated run starts with triage — the + neuroslop index plus the stop-word catalogue, ~3k tokens against ~51k — and escalates on + evidence. Triage may report only what a single line decides; a neuroslop tell is never a triage + finding, because every AD rule's carve-outs live in the full file. +- **A grammar layer the corpus had been missing**, in `editorial-grammar.md`: §I verb + government from a **closed list** of the verbs and prepositions that are actually confused, + plus mismatched government across coordinated members · §J the gerund phrase whose subject + is not the subject of the sentence · §K context-dependent homophones. An **open** case + check was written first and then deliberately rejected: over a whole text it flags the + ordinary variation of a fluent writer, which makes it a detector of non-native Russian + rather than a grammar rule. +- **Nine golden cases for the new rules, and four carve-out controls** (#29). The set grew + from 13 texts to 22. Cases 12, 13, 19 and 22 assert **zero** findings and are built only + from constructions the rules exempt — clean prose, the AD-1…AD-9 carve-outs, the 2026 + carve-outs, and the grammar ones. They are what would catch the rules firing on honest + writing, and all four now measure zero. +- **A floor under the label in `scoring.md`.** A document charged with AD-14 or AD-15 is + never labelled «Эталонный» or «Хороший», whatever the arithmetic says. This is not a cap: + the number is still printed as it computed, and the report names the rule that held the + label down. The arithmetic can be right and the word on top of it still false. +- **Four checkers and a release builder.** `tools/gates.sh` runs CI's sequence as one + command, which is what lets the pre-push hook run it at all: with no `package.json` and no + `pyproject`, this repository looked to that hook like a project with no tests, and the + selftest now fails when the two copies of that sequence drift. `check-version.sh` holds the + version at ten points, the skill description inside its budget, the six Russian trigger + phrases inside the head of it, and the size SKILL.md is advertised at. `check-dogfood.sh` + holds the numbers this product states about itself against the corpus, with a completeness + guard that names any file stating one that nobody registered. `build-release.sh` builds the + two release assets from tracked files only, refuses a dirty tree, and proves each asset by + unpacking it. The selftest grew from 45 cases to 81. +- **`tools/atom-map.tsv` gains its first sixteen rows** — the first in the repository's + history. Nine record the `scoring.md` lines whose scope the new rules extend; one records + AD-7.5, whose rationale says in plain words that the meaning was changed on purpose. + +### Changed +- **AD-7.5 narrowed.** The register carve-out now protects a speaker *inside* the text — + dialogue, quotation, a character — and not an author writing about their own text in a + conversational tone. The assistant register is a monologue written to sound like speech, + and it fell straight through the old wording. AD-7 also gains the trigger forms a model + actually reaches for: «скажу честно», «если честно», «не буду врать». +- **The corpus size is now quoted as a machine-counted floor.** «~1 044 rules» was a + hand-maintained figure nobody could reproduce; it is replaced in eleven files by «over + 2,000 linguistic atoms» / «более 2 000 лингвистических атомов» — the unit this + repository's own no-loss gate counts, reproducible with + `tools/extract-atoms.sh skills/ru-text | wc -l` (2219 at this release). A floor rather than + a figure, so that adding a rule does not oblige anyone to re-stamp eleven files. +- **The per-domain rule counts are gone from both READMEs.** The «Домены» table quoted seven + figures — 96, 197, 88, 171, 217, 128, 138 — which sum to 1035: the retired «~1 044 rules», + split up. v1.10.1 recounted them by hand and corrected four; one release later three of + the recounted ones were wrong again (57 comma traps against a stated 56, 59 button labels + against a stated 58), and four of the seven domain figures appear nowhere in the corpus at + all. The table now names the reference file for each section instead, so a reader who wants + a number can open the file and count. The one number that stayed — «Стоп-слова (92 записи)» + — is the one `check-dogfood.sh` verifies against §B on every run. + +### Fixed +- **Both control texts of the golden set asserted zero findings and had never been run.** + Both failed on their first run — seven findings and six, every one a real typographic + defect in the fixture, which contained no non-breaking spaces at all while claiming to be + written by the rules of the corpus. +- **Three defects in the new rules, caught by the measurement rather than by reading.** A + document-level charge replaced ten ordinary findings with three and the text came out + looking cleaner; density replaced the per-instance findings with a line about density; + and AD-16 absorbed a pleonasm that lived in Grammar, scoring its target text 1.1 **higher** + than before the rule against it existed. All three are now rules in their own right, the + last of them stating the general principle: a new rule must never make its target score + better. +- **The «Техническое качество» section advertised a SKILL.md that no longer existed.** It + claimed 587 words where the file holds 583, and 9 reference files where `references/` + holds 10. A claim offered as evidence has to be measured, so `check-version.sh` now reads + the file and compares both READMEs against it. It compares the numbers rather than the + sentence: Russian inflects the noun with the numeral — 583 слова, 587 слов, 581 слово — + and a literal-string check would go red on a correct line the day the count crossed a + declension boundary. +- **The note about the community marketplace was wrong, and wrong in the direction that + costs users the release.** Since #19 both READMEs have said the pin «advances automatically + with up to a day's lag». It does not. The pin is moved by a nightly sweep that opens at + most thirty pull requests per run against a catalogue of more than two thousand entries, + walking it roughly in alphabetical order; ru-text sits about 72% of the way down that list + and has not been bumped once in the marketplace's last 300 commits to its manifest. Both + READMEs now describe the mechanism, tell the reader that `claude plugins list` shows what + they actually have, and point at `npx skills add talkstream/ru-text` or a source install + for anyone who needs the current version today. +- **Every install channel the README advertises was run against its vendor's current + documentation, and eight of them had drifted.** The instructions were written once and + never re-checked; the platforms moved. + - **Claude Desktop does not take the CLI's commands.** `/plugin` is terminal-only; in the + app the path is the **+** button → **Plugins** → **Add plugin**. And «one install works + in CLI, Desktop, VS Code, JetBrains and Web» was false on Web: a user-scope install does + not reach a cloud session, which needs `enabledPlugins` in the repository's + `.claude/settings.json`. WSL sessions have no plugins at all. + - **Codex needs a marketplace added before `/plugins` shows anything** — `codex plugin + marketplace add /` — and a new session before the bundled skills load. + Neither step was in the README. + - **ru-text is not in the Cursor marketplace.** The README told users to search for it + there; the catalogue lists 216 plugins and none of them is ru-text. The manual copy, + previously the fallback, is now the instruction. + - **Both Antigravity paths had moved.** Global is `~/.gemini/config/skills/`, which all + three Antigravity products read; per-project is `.agents/skills/` — plural — with the + old singular still accepted for compatibility. + - **OpenClaw refs are owner-qualified**: `@talkstream/ru-text`, as ClawHub's own page + instructs. Bare slugs are tolerated only for already-installed or unambiguous skills. + - **`npx skills add` does less than the README implied.** It installs three skills, not + one. Without `-y` it opens an interactive picker and, pasted into a script, installs + nothing. It writes `.windsurf/skills`, `.junie/skills` and `.continue/skills` only when + those directories already exist, so on a fresh project it leaves those three platforms + unserved — their sections now lead with the manual copy. And it is project-scoped: a + user-level install (`-g`) has to be updated with `-g`, or the update reports success + while the old copy stays. +- **`.codex-plugin/plugin.json` put `logo` at the top level**, where the Codex manifest + schema does not define it; it belongs inside `interface`. Moved. +- **The Notion template inventory was missing a section and quoting a stale word count.** + The template grew an «AI-Text Tells (Neuroslop)» section in June that the «What's included» + list never mentioned, and the «~1,450 words» figure had drifted to 1,865. The list now + names the section, and the word count is gone rather than re-stamped — same reasoning as + the domain table. +- **The convention file's own claim about the corpus floor was wrong in the way it warns + against.** It said the floor is stated «in eleven files». Nine files state it, and + `.codex-plugin/plugin.json` on `main` was still advertising «~1,044 rules» while the + sentence claimed the sweep was complete. The count is replaced by the `grep` that + reproduces it. +- **The ClawHub publish note was false in the direction that ships the wrong version.** It + said `--version` is required and that omitting it fails. On clawhub 0.23.1 the guard fires + only when the flag is present and malformed; omitted, the flag defaults to the registry's + next patch. A dry run without it printed «Would publish ru-text@1.10.2» and exited 0 — so + publishing 2.0.0 without `--version 2.0.0` would have silently shipped a patch release. + ## [1.10.1] - 2026-07-25 Corrects a safety claim that was never enforced, and folds in the documentation and distribution work diff --git a/INSTALL.en.md b/INSTALL.en.md new file mode 100644 index 0000000..432623c --- /dev/null +++ b/INSTALL.en.md @@ -0,0 +1,188 @@ +# Installing ru-text by hand + +**Languages:** [Русский](INSTALL.md) | English + +You usually do not need this file. The [README](README.en.md) carries one sentence you can +hand to an AI agent, and it installs the skill itself. Come here in three cases: no agent at +hand, the agent got it wrong, or the platform installs through its own tool rather than a +file copy. + +Every path below comes from the vendor's documentation and is recorded machine-readably in +[`tools/install-paths.tsv`](tools/install-paths.tsv), with the source URL and the date it was +last read. To check that a skill actually landed where it should: +`tools/probe-install.sh check `. + +## The shared directory + +Four platforms converged on one location: **`~/.agents/skills/`** for a user-level install and +**`.agents/skills/`** inside a project. + +| Platform | Reads `~/.agents/skills/` | Reads `.agents/skills/` | +|---|---|---| +| Codex CLI | yes | yes | +| Cursor | yes | yes | +| Windsurf | yes | yes | +| GitHub Copilot | yes | yes | +| Google Antigravity | no | yes | + +So one install covers most of the field: + +```bash +git clone https://github.com/talkstream/ru-text.git +mkdir -p ~/.agents/skills +cp -r ru-text/skills/ru-text ~/.agents/skills/ru-text +``` + +A skill copied into `~/.agents/skills/` is read by three Codex surfaces: the CLI itself, the ChatGPT desktop app in Codex mode, and the IDE extension. Plugins are not available in the IDE extension but standalone skills are, so copying reaches further than installing as a plugin. + +Windows (PowerShell): + +```powershell +git clone https://github.com/talkstream/ru-text.git +New-Item -ItemType Directory -Force "$env:USERPROFILE\.agents\skills" | Out-Null +Copy-Item -Recurse ru-text\skills\ru-text "$env:USERPROFILE\.agents\skills\ru-text" +``` + +## Exceptions + +These platforms either do not read the shared directory, or read more than it. + +| Platform | Directory | Scope | +|---|---|---| +| Google Antigravity | `~/.gemini/config/skills/` | user; read by Antigravity, Antigravity IDE and Antigravity CLI | +| Windsurf | `~/.codeium/windsurf/skills/` | user, Cascade's native directory | +| Windsurf | `.windsurf/skills/` | project | +| Cursor | `~/.cursor/skills/` | user | +| Cursor | `.cursor/skills/` | project | +| GitHub Copilot | `~/.copilot/skills/` | user | +| GitHub Copilot | `.github/skills/` | project | +| JetBrains Junie | `.junie/skills/` | project, the only option | +| Continue.dev | `.continue/skills/` | project | +| Cline | `.cline/skills/` | project | + +Junie does not read the shared directory at all — it needs `.junie/skills/` specifically. + +## Platforms with their own installer + +Copying files here is pointless: the platform has its own mechanism. + +### Claude Code + +``` +/plugin marketplace add anthropics/claude-plugins-community +/plugin install ru-text@claude-community +``` + +Those are **terminal CLI** commands. In the Claude Desktop app, plugins install through the +interface: the **+** button next to the prompt box → **Plugins** → **Add plugin**, where the +marketplace can be added as well. One install serves the CLI, the app (local and SSH +sessions), VS Code and JetBrains. + +### Codex and ChatGPT + +They share plugins: "Plugins are available with ChatGPT Work on the web and with ChatGPT Work +or Codex in the ChatGPT desktop app. Codex CLI also has a plugin browser" +([learn.chatgpt.com/docs/plugins](https://learn.chatgpt.com/docs/plugins)). On ChatGPT web and +in the ChatGPT desktop app, plugins install from the interface: the **Work** switcher → +**Plugins**. What follows is about Codex CLI. + +Add the marketplace first, then install. Both commands are non-interactive: + +```bash +codex plugin marketplace add hashgraph-online/awesome-codex-plugins +codex plugin add ru-text@awesome-codex-plugins +``` + +The plugin browser works too: `/plugins` in a session, find ru-text, install. Either way, +**start a new session**: a plugin's bundled skills are loaded at session start. + +Codex ships with the `claude-plugins-official` marketplace already configured, which +`codex plugin marketplace list` shows. ru-text is not in it yet: it is listed in Anthropic's +community catalogue, not the official one. + +### Gemini CLI + +```bash +gemini extensions install https://github.com/talkstream/ru-text +``` + +### OpenClaw + +```bash +openclaw skills install @talkstream/ru-text +``` + +Owner-qualified, not a bare slug: bare slugs are accepted only for already-installed or +unambiguous skills. Published on [ClawHub](https://clawhub.ai/talkstream/ru-text). + +### Notion + +Two paths; details in [notion/README.md](notion/README.md). + +**The Notion AI skill** requires a Business or Enterprise plan. Copy +[the template page](notion/ru-text-notion-skill.md) into Notion, open the page menu (three +dots) → **Use with AI** → **Use as AI skill**. Then select text and pick «ru-text» from the +menu, or type `@ru-text` in an agent chat. + +**Notion via MCP** works with Claude Code and does not depend on the plan: install ru-text in +Claude Code, connect the [Notion MCP server](https://developers.notion.com/guides/mcp/get-started-with-mcp), +and ask Claude Code to read and edit pages. + +### NeuralDeep + +A Russian-language skills catalogue: + +```bash +npx skillsbd add talkstream/ru-text/ru-text +``` + +The command writes the skill to `/.skills/ru-text`, which **no agent reads**, so move the +directory afterwards to wherever your platform looks (see the tables above). The catalogue +pins no version and always installs the current state of `main`. + +## What an agent cannot work out for itself + +Four facts trial and error cannot discover, because they are negative. + +**ru-text is not in the Cursor marketplace.** `/add-plugin` exists and works, but the search +finds nothing: we walked the whole catalogue and ru-text is not in it. Install by copying. + +**Claude Code cloud sessions do not inherit the plugin.** A user-scope install does not carry +over. Declare the plugin under `enabledPlugins` in the repository's `.claude/settings.json`, +and it is installed at session start. WSL sessions have no plugins at all. + +**The Anthropic community-marketplace pin trails the release, by months.** The marketplace +pins the plugin to a specific commit rather than tracking releases. The pin is moved by a +nightly sweep that updates up to 30 plugins per run against a catalogue of more than 2,000 +entries, walking it roughly alphabetically. `claude plugins list` shows what you have. If you +need the current version now, install by copying or from source. + +**`npx skills add talkstream/ru-text` installs three skills**, not one: `ru-text`, `ru-check` +and `ru-score`. In Claude Code the latter two are slash commands; elsewhere they are +standalone skills. Without `-y` the command opens an interactive picker in an ordinary +terminal, so in a script it installs nothing. The install is project-scoped; add `-g` for a +user-level one. It populates `.windsurf/skills`, `.junie/skills` and `.continue/skills` only +when those directories already exist — it never creates them. + +## Updating + +A one-shot install has no update mechanism: the agent installed the skill and forgot about +it. Come back every few months. + +For a copy install, re-run the same command; it overwrites the directory — including any +edits you made. + +```bash +npx skills add talkstream/ru-text -y # skills CLI, project scope; installs three skills +npx skills add talkstream/ru-text -y -g # the same, user scope +codex plugin marketplace upgrade +gemini extensions update ru-text +openclaw skills update @talkstream/ru-text +claude plugins marketplace update claude-community +claude plugins update ru-text@claude-community +``` + +The update goes to the same scope as the install: if you installed with `-g`, update with `-g` +too, or a project-scoped run reports success while the user-level copy stays old. + +The current version and the list of changes are in the [CHANGELOG](CHANGELOG.md). diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..0b978fd --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,188 @@ +# Установка ru-text вручную + +**Языки:** Русский | [English](INSTALL.en.md) + +Обычно этот файл не нужен. В [README](README.md) есть одна фраза, которую достаточно дать +ИИ-агенту, и он поставит навык сам. Сюда стоит заглянуть в трёх случаях: агента под рукой +нет, агент ошибся, или площадка ставит навык не файлом, а своим установщиком. + +Все пути ниже взяты из документации вендоров и записаны машиночитаемо в +[`tools/install-paths.tsv`](tools/install-paths.tsv) — с адресом источника и датой, когда +его читали. Проверить, что навык лёг куда надо, можно командой +`tools/probe-install.sh check <песочница> <площадка>`. + +## Общий каталог + +Четыре площадки сошлись на одном месте: **`~/.agents/skills/`** для установки на уровне +пользователя и **`.agents/skills/`** внутри проекта. + +| Площадка | Читает `~/.agents/skills/` | Читает `.agents/skills/` | +|---|---|---| +| Codex CLI | да | да | +| Cursor | да | да | +| Windsurf | да | да | +| GitHub Copilot | да | да | +| Google Antigravity | нет | да | + +Поэтому одна установка закрывает почти всё: + +```bash +git clone https://github.com/talkstream/ru-text.git +mkdir -p ~/.agents/skills +cp -r ru-text/skills/ru-text ~/.agents/skills/ru-text +``` + +Навык, скопированный в `~/.agents/skills/`, читают три поверхности Codex: сам CLI, приложение ChatGPT в режиме Codex и IDE-расширение. Плагины в IDE-расширении недоступны, а отдельные навыки — да, так что копирование достаёт дальше, чем установка плагином. + +Windows (PowerShell): + +```powershell +git clone https://github.com/talkstream/ru-text.git +New-Item -ItemType Directory -Force "$env:USERPROFILE\.agents\skills" | Out-Null +Copy-Item -Recurse ru-text\skills\ru-text "$env:USERPROFILE\.agents\skills\ru-text" +``` + +## Исключения + +Эти площадки в общий каталог не смотрят или смотрят не только туда. + +| Площадка | Каталог | Область | +|---|---|---| +| Google Antigravity | `~/.gemini/config/skills/` | пользователь; читают Antigravity, Antigravity IDE и Antigravity CLI | +| Windsurf | `~/.codeium/windsurf/skills/` | пользователь, родной каталог Cascade | +| Windsurf | `.windsurf/skills/` | проект | +| Cursor | `~/.cursor/skills/` | пользователь | +| Cursor | `.cursor/skills/` | проект | +| GitHub Copilot | `~/.copilot/skills/` | пользователь | +| GitHub Copilot | `.github/skills/` | проект | +| JetBrains Junie | `.junie/skills/` | проект, единственный вариант | +| Continue.dev | `.continue/skills/` | проект | +| Cline | `.cline/skills/` | проект | + +Junie в общий каталог не смотрит вовсе — ему нужен именно `.junie/skills/`. + +## Площадки со своим установщиком + +Здесь копировать файлы бессмысленно: у платформы есть собственный механизм. + +### Claude Code + +``` +/plugin marketplace add anthropics/claude-plugins-community +/plugin install ru-text@claude-community +``` + +Это команды **терминального CLI**. В приложении Claude Desktop плагины ставятся через +интерфейс: кнопка **+** рядом с полем ввода → **Plugins** → **Add plugin**; там же +добавляется маркетплейс. Одна установка обслуживает CLI, приложение (локальные и +SSH-сессии), VS Code и JetBrains. + +### Codex и ChatGPT + +Плагины у них общие: «Plugins are available with ChatGPT Work on the web and with ChatGPT +Work or Codex in the ChatGPT desktop app. Codex CLI also has a plugin browser» +([learn.chatgpt.com/docs/plugins](https://learn.chatgpt.com/docs/plugins)). В вебе +и в десктопном приложении ChatGPT плагины ставятся из интерфейса: переключатель **Work** → +**Plugins**. Ниже — про Codex CLI. + +Сначала подключите маркетплейс, потом ставьте. Обе команды неинтерактивные: + +```bash +codex plugin marketplace add hashgraph-online/awesome-codex-plugins +codex plugin add ru-text@awesome-codex-plugins +``` + +Можно и через браузер плагинов: `/plugins` в сессии, найти ru-text, установить. В обоих +случаях **начните новую сессию**: навыки плагина подхватываются при старте. + +Codex из коробки уже подключает маркетплейс `claude-plugins-official`, что видно по +`codex plugin marketplace list`. ru-text там пока нет: он в community-каталоге Anthropic, +а не в официальном. + +### Gemini CLI + +```bash +gemini extensions install https://github.com/talkstream/ru-text +``` + +### OpenClaw + +```bash +openclaw skills install @talkstream/ru-text +``` + +Ссылка с владельцем, а не голый слаг: голый принимается только для уже установленных или +однозначных навыков. Плагин опубликован на [ClawHub](https://clawhub.ai/talkstream/ru-text). + +### Notion + +Два пути, подробности — в [notion/README.md](notion/README.md). + +**Навык Notion AI** требует тарифа Business или Enterprise. Скопируйте +[шаблон-страницу](notion/ru-text-notion-skill.md) в Notion, откройте меню страницы (три +точки) → **Use with AI** → **Use as AI skill**. Дальше выделяете текст и выбираете «ru-text» +из меню, либо пишете `@ru-text` в чате агента. + +**Notion через MCP** работает с Claude Code и не зависит от тарифа: поставьте ru-text в +Claude Code, подключите [Notion MCP-сервер](https://developers.notion.com/guides/mcp/get-started-with-mcp) +и просите Claude Code читать и править страницы. + +### NeuralDeep + +Русскоязычный каталог навыков: + +```bash +npx skillsbd add talkstream/ru-text/ru-text +``` + +Команда кладёт навык в `<текущий каталог>/.skills/ru-text` — оттуда его **не читает ни один +агент**, так что после установки перенесите каталог туда, где ваша площадка его ищет +(см. таблицы выше). Каталог версий не закрепляет и всегда ставит текущее состояние ветки +`main`. + +## Что агент не узнает сам + +Четыре факта, которые нельзя открыть перебором, потому что они отрицательные. + +**В маркетплейсе Cursor ru-text нет.** Команда `/add-plugin` существует и работает, но +поиск ничего не найдёт: каталог мы перебрали целиком, нашего плагина в нём нет. Ставьте копированием. + +**Облачные сессии Claude Code плагин не наследуют.** Установка на уровне пользователя туда +не переносится. Объявите плагин в поле `enabledPlugins` файла `.claude/settings.json` в +репозитории — тогда он ставится при старте сессии. В WSL-сессиях плагины недоступны вовсе. + +**Пин в community-маркетплейсе Anthropic отстаёт от релиза, и надолго.** Маркетплейс +закрепляет плагин на конкретном коммите, а не следит за релизами. Закрепление двигает ночной +сводный прогон, обновляющий до 30 плагинов за раз при более чем 2 тыс. записей в каталоге, и +обход алфавитный. Что стоит у вас, покажет `claude plugins list`. Нужна свежая версия +сразу — ставьте копированием или из исходников. + +**`npx skills add talkstream/ru-text` ставит три навыка**, а не один: `ru-text`, `ru-check` и +`ru-score`. В Claude Code последние два — слэш-команды, на остальных площадках отдельные +навыки. Без флага `-y` в обычном терминале команда открывает интерактивное меню, поэтому в +скрипте не поставит ничего. Установка проектная; для пользовательской добавьте `-g`. Каталоги +`.windsurf/skills`, `.junie/skills` и `.continue/skills` команда наполняет, только если они +уже существуют. Сама она их не создаёт. + +## Обновление + +У разовой установки нет механизма обновления: агент поставил навык и забыл о нём. Раз в +несколько месяцев стоит вернуться. + +Копирование — повторите ту же команду, она перезапишет каталог. Помните, что перезапишет и +ваши правки, если вы их вносили. + +```bash +npx skills add talkstream/ru-text -y # skills CLI, проектная область; ставит три навыка +npx skills add talkstream/ru-text -y -g # он же, пользовательская +codex plugin marketplace upgrade +gemini extensions update ru-text +openclaw skills update @talkstream/ru-text +claude plugins marketplace update claude-community +claude plugins update ru-text@claude-community +``` + +Обновление идёт в ту же область, что и установка: если ставили с `-g`, обновляйте тоже с +`-g`, иначе проектный запуск отчитается об успехе, а пользовательская копия останется старой. + +Свежую версию и список изменений смотрите в [CHANGELOG](CHANGELOG.md). diff --git a/README.en.md b/README.en.md index 6a87048..9208d98 100644 --- a/README.en.md +++ b/README.en.md @@ -1,378 +1,155 @@ # ru-text -[![Version](https://img.shields.io/github/v/release/talkstream/ru-text?label=version&color=2ea44f)](https://github.com/talkstream/ru-text/releases/latest) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Platforms](https://img.shields.io/badge/platforms-12-blue)](#quick-start) [![Claude Code Plugin](https://img.shields.io/badge/Claude_Code-Plugin-blue?logo=anthropic)](https://github.com/anthropics/claude-plugins-community) [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-30363D?logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/talkstream) [![GitHub stars](https://img.shields.io/github/stars/talkstream/ru-text?style=flat&label=stars)](https://github.com/talkstream/ru-text/stargazers) [![Last commit](https://img.shields.io/github/last-commit/talkstream/ru-text/main?label=updated)](https://github.com/talkstream/ru-text) +[![Version](https://img.shields.io/github/v/release/talkstream/ru-text?label=version&color=2ea44f)](https://github.com/talkstream/ru-text/releases/latest) [![GitHub stars](https://img.shields.io/github/stars/talkstream/ru-text?style=flat&label=stars)](https://github.com/talkstream/ru-text/stargazers) [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-30363D?logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/talkstream) -**Languages:** [Русский (primary)](README.md) | English +**Languages:** [Русский](README.md) | English -**Russian text quality plugin for Claude Code, Codex, Notion, Cursor, GitHub Copilot, and [7 more platforms](#quick-start)** — typography, information style, editorial standards, UX writing, and business correspondence. +Your AI agent already writes Russian. ru-text takes care of how that text looks and reads: guillemets and em dashes appear on their own, bureaucratic phrasing goes, and the references on editing, UX copy and business correspondence load when they are needed. -~1,044 independently formulated rules informed by 16 canonical Russian-language sources. All formulations are original — no verbatim quotes, full attribution. +## Install -## Acknowledgments +Hand this sentence to your AI agent: -This plugin exists because a handful of people decided that Russian text on the internet deserves better. They wrote the books, built the tools, maintained the guides, and set the standards that thousands of editors, writers, and designers now rely on every day. Their work fundamentally changed how Russian text is written, formatted, and read on screens. I am deeply grateful to every one of them. If this plugin saves you time, please buy their books and use their tools — they earned it. +> Установи навык https://github.com/talkstream/ru-text глобально и вызывай его для любых задач с русским текстом: вычитка, типографика, очистка от нейрослопа, редактура, UX-тексты, деловая переписка. +The agent takes it from there: it knows where its platform keeps skills better than any instruction written a year ago. It works in Claude Code, Codex and ChatGPT, Cursor, GitHub Copilot, Gemini CLI, Google Antigravity, Windsurf, Continue.dev, Cline, JetBrains Junie, OpenClaw and Notion. -## What it does +Then start a new session — skills are loaded when a session starts, not in the one that installed them. -ru-text gives your AI coding assistant a deep understanding of Russian text quality. It auto-activates when the assistant produces or edits Russian text, applying typography rules instantly and loading domain-specific knowledge on demand. +Installing by hand, an agent that got it wrong, or a platform with its own installer — all of that is in [INSTALL.en.md](INSTALL.en.md): the directories of twelve platforms with the vendor URL each came from, the click-paths for Claude Desktop and Notion (Notion installs by clicking, not by agent), and four things an agent cannot discover by trying. -Works with Claude Code (CLI and Desktop), Codex CLI, Notion, Cursor, GitHub Copilot, Gemini CLI, Google Antigravity, Windsurf, Continue.dev, Cline, JetBrains (Junie), and OpenClaw. +## What it looks like -- **~1,044 rules** across 7 domains, packed into 9 reference files + addenda -- **Auto-activation** — no need to remember to turn it on -- **Covers everything** — from em dashes and guillemets to UX microcopy and business email tone -- **Non-dogmatic** — your explicit style request always overrides default rules +Before: -## Use cases +> В целях повышения эффективности взаимодействия между подразделениями было принято решение о проведении мероприятий по оптимизации документооборота. Ответственными лицами осуществляется контроль за надлежащим исполнением поручений. -**This README.** Every dash, quote, and space you see here follows the plugin's own rules. This document was written with ru-text active. +After: -**UX microcopy.** Writing buttons, errors, empty states for a Russian app. The plugin loads 217 UX rules: "Отмена" not "Нет", error structure (what happened + what to do), placeholders as examples, not instructions. +> Чтобы отделы работали быстрее, мы упрощаем документооборот. За исполнением поручений следят Иванов и Петрова. -**Business email.** Drafting an email to colleagues or clients. The plugin kills bureaucratic language ("довожу до сведения" → "сообщаю"), structures subject + first sentence + call to action, and suggests respectful tone without being servile. +*"To make the departments work faster, we are simplifying our paperwork. Ivanov and Petrova follow up on assignments."* -**Landing page copy.** Writing an "About" section for an IT company. The plugin replaces cliches ("команда профессионалов", "индивидуальный подход") with specific facts and numbers. +Three things changed: verbal nouns became verbs, the passive voice named its actor, and "в целях" and "осуществляется" left — both are in the catalogue of 92 stop-words. -**README and documentation.** Writing docs for an open-source project in Russian. Proper typography (guillemets, em dashes, non-breaking spaces), clear inverted-pyramid structure. +## What it will fix -**Cleaning up AI-slop.** Text reads as machine-generated. The plugin catches the tells: manufactured antithesis ("не X, а Y" with no antecedent), preemptive self-praise ("чётко, по делу, без воды"), assistant-register filler ("Отличный вопрос!", "Надеюсь, помог"), and hollow openers ("давайте разберёмся", "погрузимся"). +**Buttons, errors, empty states.** "Отмена" ("Cancel") instead of "Нет" ("No"). An error says what happened and what to do. A placeholder is an example, not an instruction. -**Text quality scoring.** Want to know how your text measures up? `/ru-text:ru-score` evaluates text across 5 dimensions (typography, clarity, grammar, structure, reader precision) and returns a 0.0–10.0 score with specific issues per dimension. +**Emails and messages.** "Довожу до сведения" ("I hereby bring to your attention") becomes "сообщаю" ("I am letting you know"). Subject, first sentence and request move into place. The tone stays respectful without being obsequious. -**AI agent quality.** Building AI features in your product? Uncertain how the agent will phrase responses in Russian? ru-text ensures predictable, high-quality Russian text from any Claude-powered agent: consistent typography, no bureaucratic language, reader-first structure. +**Landing pages and documentation.** "Команда профессионалов" is replaced by something checkable. Typography, inverted-pyramid structure, links that say where they lead. -## Quick start +**Text that came out of a model** — yours or someone else's. ru-text knows seventeen tells of machine writing. Five are visible in the fragment itself: -Sections are ordered by April 2026 platform popularity among developers using AI assistants. +- manufactured antithesis ("не X, а Y" — "not X but Y") with no antecedent; +- preemptive self-praise ("чётко, по делу, без воды" — "crisp, on point, no fluff"); +- assistant-register filler ("Отличный вопрос!" — "Great question!", "Надеюсь, помог" — "Hope that helped"); +- hollow openers ("давайте разберёмся" — "let us figure this out", "погрузимся" — "let us dive in"); +- phantom attribution ("как показывают исследования" — "studies show") with no study behind it. -### Claude Code (CLI) +Nine more are of the same kind. Two others are charged to the whole document rather than to a fragment, because no local edit repairs them: a piece that stayed a chat transcript, and a piece written for a search engine instead of a reader. -```bash -# Add the community marketplace (one-time setup) -/plugin marketplace add anthropics/claude-plugins-community - -# Install the plugin -/plugin install ru-text@claude-community -``` - -Listed in the [Claude Code community marketplace](https://github.com/anthropics/claude-plugins-community). A listing in the [official Anthropic marketplace](https://claude.com/plugins) is planned. - -### Claude Code (Desktop) - -Same install commands as the CLI — open the in-app `/plugin` prompt in the Claude Desktop app and run the two commands above. A single install is shared across CLI, Desktop, VS Code, JetBrains, and Web. - -### Codex CLI - -Inside a Codex session, use the interactive plugin browser: - -``` -/plugins -``` - -Search for “ru-text” and install. Alternatively, use the universal skills CLI (see below). - -### Notion - -Two integration paths — see [notion/README.md](notion/README.md) for details: - -**Notion AI Custom Skill** (standalone, Business/Enterprise plan): -1. Copy [the template page](notion/ru-text-notion-skill.md) into a Notion page -2. Designate the page as an AI skill -3. Select text and invoke “ru-text” from the AI menu - -**Notion via MCP** (with Claude Code, any plan): -1. Install ru-text in Claude Code -2. Connect the [Notion MCP server](https://developers.notion.com/guides/mcp/get-started-with-mcp) -3. Ask Claude Code to read, check, and update your Notion pages - -### Cursor - -Use the plugin command in Cursor Agent chat: - -``` -/add-plugin -``` - -Search for “ru-text” and install. If not listed in the marketplace, copy manually: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text ~/.cursor/skills/ru-text -``` - -Windows (PowerShell): - -```powershell -git clone https://github.com/talkstream/ru-text.git -Copy-Item -Recurse ru-text\skills\ru-text "$env:USERPROFILE\.cursor\skills\ru-text" -``` - -### GitHub Copilot +Where the device is legitimate — a quotation, an analysis of someone else's text, a legal formula, a list that genuinely has three items — is written into the rule itself. The carve-out is read before the finding is raised. -If ru-text is already installed for Claude Code in your project, Copilot detects it automatically. Otherwise: +## What it costs you in context -```bash -npx skills add talkstream/ru-text -``` +The corpus is large — roughly 86,000 tokens, and nearly half of that is the single reference +on machine-written text. Running all of it over every paragraph would be wasteful at your +expense, so ru-text does not. -Or copy manually: +What stays in context permanently is only the always-on skill, about 1,700 tokens: the +typography table and the top stop-words. Everything else loads when it is needed. -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .github/skills/ru-text -``` +The full check runs when you ask for it — «вычитай», «прогони ru-text», or a gate in your own +discipline that names ru-text. Then the whole corpus is read, because that is what you asked +for. -Works in VS Code, Visual Studio, and JetBrains IDEs with Copilot. +When the agent checks itself, unprompted, it starts with a fast pass instead: the index of +tells and the stop-word catalogue, about 3,000 tokens rather than 51,000. It catches +typography and stop-words — the things a single line decides. If it finds a trace of machine +writing, or five findings accumulate, it expands into the full check on its own. The fast +pass always labels itself as fast and never passes for the full one. -### Gemini CLI +The fast pass does **not** judge the tells of machine writing. Each of them carries carve-outs +naming where the device is legitimate, and those live only in the full reference. Spotting a +trace is a reason to expand, not a reason to raise a finding. -```bash -gemini extensions install https://github.com/talkstream/ru-text -``` +## Boundaries -### Google Antigravity +**Your request outranks the rules.** Ask for a casual, academic, legal, SEO or literary style and ru-text adapts. These are quality defaults, not requirements. -Antigravity reads the SKILL.md format natively. Copy the skill into the global skills folder to make it available across all projects: +**Nothing is rewritten silently.** A check returns the corrected version plus a list of changes; a file is edited in place only when you ask for that. -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text ~/.gemini/antigravity/skills/ru-text -``` +**Someone else's words stay theirs.** Quoted material, code blocks and third-party text inside your document are reproduced as-is: a remark about them, perhaps; an edit to them, never. -For a single project, copy the skill into `/.agent/skills/ru-text` instead. Antigravity is young and the skills path varies by version — check the [official Antigravity Skills codelab](https://codelabs.developers.google.com/getting-started-with-antigravity-skills) for the current location. +**It turns off like any skill.** Through your platform: `/plugin` in Claude Code, or by deleting the skill directory elsewhere. -### Windsurf +## Scoring -```bash -npx skills add talkstream/ru-text -``` +`/ru-text:ru-score` gives a score from 0 to 10 across five dimensions: typography, clean language, grammar, structure, precision for the reader. Each dimension comes with specific issues and quoted fragments. -Or copy manually to the Windsurf skills directory: +The top labels have a floor the arithmetic does not override. A document that turned out to be a chat transcript, or one written for a search engine, is never labelled «Эталонный» (Benchmark) or «Хороший» (Good) whatever it scores — and the rubric names the rule that capped it. The number itself is printed as it computed. -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .windsurf/skills/ru-text -``` +`/ru-text:ru-check` does the analysis without a score: findings, the rule behind each, and a proposed replacement. -Invoke with `@ru-text` in Cascade chat. Also available via Cascade panel > Customizations > Skills. +Both are Claude Code's slash syntax. Elsewhere, just ask: "score this text with ru-text", "proofread this" — the rubric loads on demand. -### Continue.dev +## The corpus -If ru-text is already installed for Claude Code in your project, Continue detects it automatically. Otherwise: +Over 2,000 linguistic atoms: rules, wrong → right pairs, dictionary entries and carve-outs. That is a floor, not an exact count, and a command produces it: ```bash -npx skills add talkstream/ru-text +tools/extract-atoms.sh skills/ru-text | wc -l ``` -Or copy manually: +The corpus is spread over ten reference files, and they load on demand rather than at session start: a large body of rules costs no context until it is needed. Open any of them and count the rules yourself — that is more reliable than a number maintained by hand. -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .continue/skills/ru-text -``` +- [`typography.md`](skills/ru-text/references/typography.md) — quotes, dashes, non-breaking spaces, digit grouping, abbreviations +- [`info-style.md`](skills/ru-text/references/info-style.md) — the catalogue of 92 stop-words, text structure, facts over adjectives +- [`editorial-punctuation.md`](skills/ru-text/references/editorial-punctuation.md) — complex sentences, comma traps, introductory words +- [`editorial-grammar.md`](skills/ru-text/references/editorial-grammar.md) — agreement, pleonasms, verb government, gerunds, homophones +- [`ux-writing.md`](skills/ru-text/references/ux-writing.md) — buttons, errors, empty states, forms, notifications, confirmation dialogs +- [`business-writing.md`](skills/ru-text/references/business-writing.md) — emails, messengers, tone, meeting notes +- [`anti-patterns.md`](skills/ru-text/references/anti-patterns.md) — wrong-to-right pairs, grouped by severity +- [`addenda.md`](skills/ru-text/references/addenda.md) — seventeen tells of machine writing, with their carve-outs +- [`scoring.md`](skills/ru-text/references/scoring.md) — the scoring rubric: dimensions, weights, floors +- [`sources.md`](skills/ru-text/references/sources.md) — sources and attribution -Works in both VS Code and JetBrains extensions. +## Updating -### Cline +A one-shot install has no mechanism of its own: the agent installed the skill and forgot about it. Every few months, ask it to run the install again; what changed is in the [CHANGELOG](CHANGELOG.md), and the per-platform commands are in [INSTALL.en.md](INSTALL.en.md#updating). -If ru-text is already installed for Claude Code in your project, Cline detects it automatically. Otherwise: +One thing worth knowing up front: the ru-text pin in the Claude Code community marketplace trails the current version by months. `claude plugins list` shows the version you have; if it is old, install the skill by copying — [the same file](INSTALL.en.md#what-an-agent-cannot-work-out-for-itself) explains how. -```bash -npx skills add talkstream/ru-text -``` - -Or copy manually: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .cline/skills/ru-text -``` - -Enable skills in Cline settings: Features > Enable Skills. - -### JetBrains (Junie) - -```bash -npx skills add talkstream/ru-text -``` - -Or copy manually: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .junie/skills/ru-text -``` - -Works in IntelliJ IDEA, PyCharm, WebStorm, GoLand, PhpStorm, RubyMine, RustRover, Rider, CLion, and Android Studio. - -### OpenClaw - -```bash -openclaw skills install ru-text -``` - -Available on [ClawHub](https://clawhub.ai/talkstream/ru-text). Works with any LLM provider and messaging channel OpenClaw supports. - -### Any platform via skills CLI - -```bash -npx skills add talkstream/ru-text -``` - -### From source - -```bash -git clone https://github.com/talkstream/ru-text.git -``` - -Then add the repo as a plugin source per your platform's docs. - -Start writing Russian text — the plugin takes over automatically. If ru-text makes your products better, consider [sponsoring](https://github.com/sponsors/talkstream) continued development. - -## Updating ru-text - -Latest version — **v1.10.1** (see the [CHANGELOG](CHANGELOG.md)). Check your installed version in Claude Code with `claude plugins list`. - -**Primary method** — for skill-based platforms (GitHub Copilot, Windsurf, Continue.dev, Cline, JetBrains Junie, Google Antigravity, manual Cursor install). Re-run the install — the command pulls the latest version from the repository and overwrites the skill: - -```bash -npx skills add talkstream/ru-text -``` - -**Claude Code (CLI and Desktop).** Refresh the marketplace cache first, then update the plugin: - -```bash -claude plugins marketplace update claude-community -claude plugins update ru-text@claude-community -``` - -Restart Claude Code (or run `/reload-plugins`) to apply the change. You can also do this from the `/plugin` menu, "Installed" tab. Note: the community marketplace pins the plugin to a specific version and advances the pin automatically with up to a day's lag — if the version doesn't change right after a release, that's expected; give the marketplace time to re-pin. - -**Gemini CLI:** - -```bash -gemini extensions update ru-text -``` - -**OpenClaw:** - -```bash -openclaw skills update ru-text -``` - -**Codex CLI.** Open `/plugins`, find ru-text, and update. - -**Manual copy.** If you installed the skill manually (`git clone` + `cp`), repeat your platform's install steps — they overwrite the skill with the latest version. - -## Domains - -| Domain | Rules | What it covers | -|---|---|---| -| Typography | 96 | Quotes (guillemets, lapki), dashes, non-breaking spaces, digit grouping, special characters, abbreviations | -| Information style | 197 | Stop-words (92 entries), text structure, facts over adjectives, register, T-Zh editorial principles | -| Editorial: punctuation | 88 | Complex sentences, 56 comma-trap constructions, introductory words, semicolons | -| Editorial: grammar | 171 | Capitalization, agreement, 50+ pleonasms, list formatting, clean language principles | -| UX writing | 217 | 58 button labels, error messages, empty states, forms, notifications, dialogs, onboarding | -| Business writing | 128 | Email structure, messenger etiquette, tone, 41 clean phrase patterns, meeting notes | -| Anti-patterns | 138 | Wrong-to-right pairs organized by severity: bureaucratic language, passive voice, bloat | +## Sources and credits -## Commands +These books, guides and tools taught me how to work with Russian text. If ru-text saves you time, buy their books and use their tools. -| Command | Description | -|---|---| -| `/ru-text` | Activate the skill manually (auto-activation covers most cases) | -| `/ru-text:ru-check` | Run a comprehensive text quality check on provided text or recent output | -| `/ru-text:ru-score` | Score text quality on a 0.0–10.0 scale across 5 dimensions | +**Typography and layout.** Artyom Gorbunov, "Typography and Layout" · [Bureau Gorbunov's Tips](https://bureau.ru/soviet/) · A. Milchin and L. Cheltsova, "The Publisher's and Author's Handbook" · [Ilya Birman's typography layout](https://ilyabirman.ru/typography-layout/) · [Type.today journal](https://type.today) -## Style priority +**Information style.** Maxim Ilyakhov, "Write, Shorten" and "Clear and Understandable" · [T—Zh editorial policy](https://journal.tinkoff.ru/manual/) · [Kontur guides](https://guides.kontur.ru) · [Yandex Gravity UI](https://gravity-ui.com) -If you explicitly request a specific style — casual, academic, SEO, literary, legal — your prompt overrides the default rules. The plugin provides quality defaults, not mandates. +**Writing and language.** M. Ilyakhov and L. Sarycheva, "New Rules of Business Correspondence" · Nora Gal, "[Living Word and Dead Word](http://lib.ru/TRANSLATORS/NORA_GAL/slowo.txt)" · D. Rozental's reference books · Artemy Lebedev, "[Mandership](https://www.artlebedev.ru/kovodstvo/)" · [Ozon's UX writing practices](https://habr.com/ru/companies/ozontech/articles/821383/) · GOST R 7.0.12-2011 and GOST 7.12-93 -## Technical quality +The full list, with what each source contributed, is in [`sources.md`](skills/ru-text/references/sources.md). -Built to Anthropic's Claude Code plugin specs: -- SKILL.md: 587 words, 90 lines (guideline: under 2,000 words, under 500 lines) -- 9 reference files load on demand, never at session start -- ~1,044 rules organized into 7 thematic areas with progressive disclosure +Nearby tools: [Glavred](https://glvrd.ru), [Lebedev's Typograf](https://www.artlebedev.ru/typograf/), [Orfogrammka](https://orfogrammka.ru). ## Intellectual property notice -This plugin is an independent, original work by Arseniy Kamyshev. - -The rules and principles contained herein represent the author's personal -understanding of Russian typography, editorial, and writing standards, gained -from years of professional practice and study of published sources listed below. - -All formulations are original. No text is quoted verbatim from any source. -The underlying principles (typography rules, grammar norms, editorial methods) -are not subject to copyright under Article 1259(5) of the Russian Civil Code, -17 USC §102(b), and the Berne Convention. - -The authors and publishers of the listed sources have not endorsed, reviewed, -or approved this plugin. Source references are provided for reader convenience -and further study. +ru-text is an independent work of authorship by Arseniy Kamyshev. The rules in it are the author's own understanding of Russian typography and editorial standards, formed over years of practice and of reading the sources listed above. All formulations are original; nothing is quoted verbatim. The underlying principles — typographic rules, grammatical norms, editorial technique — are not subject to copyright: Article 1259(5) of the Russian Civil Code, 17 USC §102(b), the Berne Convention. -Product names mentioned are trademarks of their respective owners, used here -for informational purposes only. +The authors and publishers of the sources listed have not endorsed or reviewed this plugin. The links are for the reader's convenience. Product names belong to their respective owners. -## Roadmap +## What's next -Next steps for expanding ru-text to new audiences: - -- **Telegram Bot** — text quality checking and /ru-score via Telegram -- **Browser Extension** — Russian text quality in any web text field (Chrome, Firefox) -- **WordPress Plugin** — typography and quality scoring in the Gutenberg editor - -Contributions and ideas welcome — [open an issue](https://github.com/talkstream/ru-text/issues) or [start a discussion](https://github.com/talkstream/ru-text/discussions). - -## Sources and credits - -### Typography - -| # | Source | Contribution | Link | -|---|---|---|---| -| 1 | **Artyom Gorbunov "Typography and Layout"** (2017) | Core typography rules: dashes, quotes, spacing, screen typography | [bureau.ru/projects/book-typography/](https://bureau.ru/projects/book-typography/) | -| 2 | **Bureau Gorbunov "Tips"** (2005–present, 4809+ tips) | Practical micro-advice on typography, editing, design | [bureau.ru/soviet/](https://bureau.ru/soviet/) | -| 3 | **A. Milchin, L. Cheltsova "Publisher's and Author's Handbook"** (2021, 6th ed.) | Punctuation, abbreviations, number formatting, editorial conventions | [store.artlebedev.com](https://store.artlebedev.com) | -| 4 | **Ilya Birman — Typography Layout** (2007–present) | Keyboard layout for typing correct typographic characters | [ilyabirman.ru/typography-layout/](https://ilyabirman.ru/typography-layout/) | -| 5 | **Type.today — Journal** (2016–present) | Cyrillic typeface design, font pairing, readability | [type.today](https://type.today) | - -### Information style and clear writing - -| # | Source | Contribution | Link | -|---|---|---|---| -| 6 | **Maxim Ilyakhov "Write, Shorten"** (2017, updated 2025) | Foundation of info-style: removing filler, fighting bureaucratic language, reader-first writing | [book.glvrd.ru](https://book.glvrd.ru) | -| 7 | **Maxim Ilyakhov "Clear and Understandable"** (2019) | Advanced info-style: text structure, persuasion, visual-textual integration | [book.glvrd.ru](https://book.glvrd.ru) | -| 8 | **T-Zh editorial policy** (2017–present, 56+ pages) | Tone of voice, formatting, numbers, business writing standards | [journal.tinkoff.ru/manual/](https://journal.tinkoff.ru/manual/) | -| 9 | **Kontur Guides** (2020–present) | UX writing for B2B software: interface text, errors, onboarding | [guides.kontur.ru](https://guides.kontur.ru) | -| 10 | **Yandex Gravity UI** (2023–present) | Design system with content guidelines for Russian UI text | [gravity-ui.com](https://gravity-ui.com) | - -### Writing and language - -| # | Source | Contribution | Link | -|---|---|---|---| -| 11 | **M. Ilyakhov, L. Sarycheva "New Rules of Business Correspondence"** (2018) | Email structure, respectful tone, messenger etiquette | [book.glvrd.ru](https://book.glvrd.ru) | -| 12 | **Nora Gal "Living Word and Dead Word"** (1972, reprints) | Original critique of bureaucratic language, nominalization abuse, passive voice | [lib.ru](http://lib.ru/TRANSLATORS/NORA_GAL/slowo.txt) | -| 13 | **D. Rozental — Spelling and Style References** (1960s–2000s) | Authoritative Russian grammar, punctuation, orthography baseline | widely available | -| 14 | **Artemy Lebedev "Mandership"** (1998–present) | Screen typography, dashes and quotes, design-text readability | [artlebedev.ru/kovodstvo/](https://www.artlebedev.ru/kovodstvo/) | -| 15 | **Ozon UX Writing Practices** (2021–present) | UX writing at scale: buttons, notifications, errors, product copy | [habr.com](https://habr.com/ru/companies/ozontech/articles/821383/) | -| 16 | **GOST R 7.0.12-2011, GOST 7.12-93** (Rosstandart) | Official standards for bibliographic abbreviations in Russian | GOST databases | - -### Online tools - -- **Glavred** ([glvrd.ru](https://glvrd.ru)) — checks text for info-style quality, highlights filler, scores 0–10 -- **Lebedev Typograf** ([typograf.artlebedev.ru](https://www.artlebedev.ru/typograf/)) — auto-fixes typography: quotes, dashes, non-breaking spaces -- **Orfogrammka** ([orfogrammka.ru](https://orfogrammka.ru)) — grammar, spelling, and punctuation checker +A Telegram bot, a browser extension, a WordPress plugin. Ideas and corrections go to [issues](https://github.com/talkstream/ru-text/issues) or [discussions](https://github.com/talkstream/ru-text/discussions). ## Author -**Arseniy Kamyshev** — [nafigator@gmail.com](mailto:nafigator@gmail.com) — [Telegram](https://t.me/nafigator) — [GitHub](https://github.com/talkstream) - -## Support - -I have spent my life working on social projects. This is where I make the biggest difference for people and communities, so I **always** need financial support. If ru-text makes your products better, consider [sponsoring me on GitHub](https://github.com/sponsors/talkstream). +Arseniy Kamyshev — [nafigator@gmail.com](mailto:nafigator@gmail.com) · [Telegram](https://t.me/nafigator) · [GitHub](https://github.com/talkstream) -## License +I work on social projects, and the work on ru-text is sustained by the people it has proven useful to. If it makes your products better — [GitHub Sponsors](https://github.com/sponsors/talkstream). -[MIT](LICENSE) | [Privacy Policy](PRIVACY_POLICY.md) +[MIT](LICENSE) · [Privacy policy](PRIVACY_POLICY.md) · the plugin makes no network calls and collects no data. diff --git a/README.md b/README.md index ae5d9e2..ff38fb9 100644 --- a/README.md +++ b/README.md @@ -1,381 +1,156 @@ # ru-text -[![Version](https://img.shields.io/github/v/release/talkstream/ru-text?label=version&color=2ea44f)](https://github.com/talkstream/ru-text/releases/latest) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Platforms](https://img.shields.io/badge/platforms-12-blue)](#быстрый-старт) [![Claude Code Plugin](https://img.shields.io/badge/Claude_Code-Plugin-blue?logo=anthropic)](https://github.com/anthropics/claude-plugins-community) [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-30363D?logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/talkstream) [![GitHub stars](https://img.shields.io/github/stars/talkstream/ru-text?style=flat&label=stars)](https://github.com/talkstream/ru-text/stargazers) [![Last commit](https://img.shields.io/github/last-commit/talkstream/ru-text/main?label=updated)](https://github.com/talkstream/ru-text) +[![Version](https://img.shields.io/github/v/release/talkstream/ru-text?label=version&color=2ea44f)](https://github.com/talkstream/ru-text/releases/latest) [![License](https://img.shields.io/github/license/talkstream/ru-text?label=license&color=blue)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/talkstream/ru-text?style=flat&label=stars)](https://github.com/talkstream/ru-text/stargazers) -**Языки:** Русский | [English](README.en.md) +[English](README.en.md) · [Установка](INSTALL.md) · [Что изменилось](CHANGELOG.md) · [Источники](skills/ru-text/references/sources.md) -> 🌍 **Reading in English?** This page is in Russian on purpose — ru-text is a tool for Russian text quality, so its home page speaks Russian first. You're very welcome here, and the **full English version is one click away → [README in English](README.en.md)**. Everything below is mirrored there. +ИИ-агент уже записывает ваши мысли по-русски, и это видно: прямые кавычки вместо ёлочек, дефис вместо тире, «в целях повышения эффективности», «Отличный вопрос!». Мысль ваша, а звучит как машина. -**Плагин для Claude Code, Codex, Notion, Cursor, GitHub Copilot и [ещё 7 платформ](#быстрый-старт) — качество русского текста** — типографика, информационный стиль, редактура, UX-тексты и деловая переписка. +ru-text — навык вычитки русского текста для ИИ-агентов. Он работает внутри агента и чистит это по ходу дела: типографику, канцелярит и семнадцать примет машинного текста (нейрослоп). На каждую правку даёт цитату и правило, по которому она сделана. -~1 044 авторских правил, основанных на изучении 16 канонических русскоязычных источников. Все формулировки оригинальные — без дословного цитирования, с полной атрибуцией. +Ваши слова, стиль и тон он не трогает: это не ошибки. И ваш файл он не перепишет, пока вы сами не попросите. -## Благодарности +## Нейрослоп -Этот плагин существует потому, что несколько человек решили: русский текст в интернете заслуживает лучшего. Они написали книги, создали инструменты, поддерживали гайды и задали стандарты, на которые сегодня опираются тысячи редакторов, авторов и дизайнеров. Их работа изменила то, как пишут, оформляют и читают русский текст на экранах. Я глубоко благодарен каждому из них. Если этот плагин сэкономит вам время — пожалуйста, купите их книги и пользуйтесь их инструментами. Они это заслужили. +Ловить модель по словарю бесполезно: словарь у неё наш. Выдаёт её манера. Она хвалит ваш вопрос и спорит с тем, чего никто не говорил. +Вот пять предложений, в которых нет ни одного факта: -## Что делает плагин +> Отличный вопрос! Сейчас всё объясню — коротко, без воды и по делу. Скажу честно: тут есть нюанс. Давайте разберёмся, как это работает. Дело не в скорости, а в предсказуемости. -ru-text даёт вашему ИИ-ассистенту глубокое понимание качества русского текста. Плагин активируется автоматически при создании или редактировании текста на русском: типографика применяется мгновенно, доменные справочники подгружаются по мере необходимости. +Тот же ответ, когда автору есть что сказать: -Работает с Claude Code (CLI и Desktop), Codex CLI, Notion, Cursor, GitHub Copilot, Gemini CLI, Google Antigravity, Windsurf, Continue.dev, Cline, JetBrains (Junie) и OpenClaw. +> Под нагрузкой система замедляется предсказуемо: очередь растёт линейно до 800 запросов в секунду, дальше отказы. Вот замеры. -- **~1 044 правил** в 7 доменах, упакованных в 9 справочных файлов + дополнения -- **Автоактивация** — не нужно помнить о включении -- **Полное покрытие** — от длинного тире и кавычек-ёлочек до UX-микрокопирайтинга и тона деловой переписки -- **Не догма** — ваш явный запрос стиля всегда приоритетнее правил по умолчанию +Приёмы по порядку: -## Сценарии использования +- «Отличный вопрос!» — сервисная реплика ассистента; +- «коротко, без воды и по делу» — похвала себе за краткость; +- «скажу честно» — объявленная искренность; +- «давайте разберёмся» — пустой зачин; +- «дело не в скорости, а в предсказуемости» — ложная антитеза: отрицается то, чего никто не утверждал. -**Этот README.** Каждое тире, кавычка и пробел в этом документе соответствуют правилам самого плагина. Текст написан с подключённым ru-text. +Первый вариант я вижу каждый день — в чужих README и в своих черновиках. Он читается гладко и ничего не сообщает. -**UX-микрокопирайтинг.** Пишете кнопки, ошибки, пустые состояния для русскоязычного приложения. Плагин подгружает 217 UX-правил: «Отмена» вместо «Нет», структура ошибок (что случилось + что делать), плейсхолдеры как примеры, а не инструкции. +Прежде чем сделать замечание, навык смотрит оговорки. В цитате, в разборе чужого текста и в юридической формуле приём законен. А триада — любимый у модели ритм из трёх — законна, когда элементов правда три. -**Деловое письмо.** Набрасываете email коллегам или клиентам. Плагин убивает канцелярит («довожу до сведения» → «сообщаю»), структурирует тему + первое предложение + призыв к действию, подсказывает уважительный тон без заискивания. +Всего в справочнике семнадцать таких примет. Две из них предъявляются документу целиком, потому что правкой на месте не лечатся: текст, оставшийся стенограммой диалога с нейросетью, и текст, который писали под поисковый запрос. -**Текст для лендинга.** Пишете блок «О компании» для IT-студии. Плагин заменяет штампы («команда профессионалов», «индивидуальный подход») на конкретные факты и цифры. +Канцелярит навык разбирает так же: «в целях повышения эффективности взаимодействия» становится «чтобы отделы работали быстрее», «осуществляется контроль» — «следит Петрова». Отглагольные существительные снова становятся глаголами, у безличного контроля появляется фамилия, а «в целях» и «осуществлять» уходят: они в каталоге из 92 стоп-слов. Фактов навык не сочиняет: фамилию он спросит у вас. -**README и документация.** Пишете документацию для open-source проекта на русском. Правильная типографика (ёлочки, тире, неразрывные пробелы), ясная структура «перевёрнутой пирамиды». +## Установка -**Вычистка нейрослопа.** Текст похож на сгенерированный. Плагин ловит характерные приметы машинного текста: ложную антитезу («не X, а Y» без посылки), непрошенную самопохвалу («чётко, по делу, без воды»), сервисные реплики ассистента («Отличный вопрос!», «Надеюсь, помог») и пустые зачины («давайте разберёмся», «погрузимся»). +Дайте эту фразу своему ИИ-агенту: -**Оценка качества текста.** Хотите знать, насколько хорош ваш текст? `/ru-text:ru-score` оценивает текст по 5 измерениям (типографика, чистота языка, грамотность, структура, точность для читателя) и возвращает балл от 0 до 10 с конкретными замечаниями по каждому измерению. +> Установи навык https://github.com/talkstream/ru-text глобально и вызывай его для любых задач с русским текстом: вычитка, типографика, очистка от нейрослопа, редактура, UX-тексты, деловая переписка. -**Качество ИИ-агентов.** Встраиваете ИИ-инструменты в продукт и не уверены, как именно ответит агент на русском? ru-text гарантирует предсказуемо высокое качество текста от любого Claude-агента: единообразная типографика, никакого канцелярита, структура от читателя. +Обычно этого достаточно: где его площадка держит навыки, агент знает лучше, чем инструкция, написанная год назад. Работает в Claude Code, Codex и ChatGPT, Cursor, GitHub Copilot, Gemini CLI, Google Antigravity, Windsurf, Continue.dev, Cline, JetBrains Junie, OpenClaw и Notion. -## Быстрый старт +Навыки подхватываются при старте сессии, поэтому начните новую. -Разделы упорядочены по популярности платформ среди разработчиков с ИИ-ассистентами на апрель 2026 года. +Проверьте на живом тексте: дайте агенту абзац и попросите «вычитай». В ответе придут исправленный вариант и список правок. Списка нет — навык не поднялся, смотрите [INSTALL.md](INSTALL.md). -### Claude Code (CLI) +Ставите руками, агент промахнулся или площадка ставит навык своим установщиком — это в [INSTALL.md](INSTALL.md). Там каталоги площадок с адресами вендорской документации; клик-пути для Claude Desktop и Notion; четыре факта, которые агент перебором не откроет. -```bash -# Добавить community marketplace (один раз) -/plugin marketplace add anthropics/claude-plugins-community - -# Установить плагин -/plugin install ru-text@claude-community -``` - -Опубликован в [community-маркетплейсе Claude Code](https://github.com/anthropics/claude-plugins-community). Размещение в [официальном маркетплейсе Anthropic](https://claude.com/plugins) запланировано. - -### Claude Code (Desktop) - -Команды установки те же, что и для CLI: откройте поле `/plugin` в приложении Claude Desktop и выполните две команды выше. Одна установка работает сразу в CLI, Desktop, VS Code, JetBrains и Web. - -### Codex CLI - -В сессии Codex используйте интерактивный браузер плагинов: - -``` -/plugins -``` - -Найдите «ru-text» и установите. Или используйте универсальный skills CLI (см. ниже). - -### Notion - -Два способа интеграции — подробности в [notion/README.md](notion/README.md): - -**Навык Notion AI** (автономный, тариф Business/Enterprise): -1. Скопируйте [шаблон-страницу](notion/ru-text-notion-skill.md) в Notion -2. Назначьте страницу навыком AI -3. Выделите текст и вызовите «ru-text» из меню AI - -**Notion через MCP** (с Claude Code, любой тариф): -1. Установите ru-text в Claude Code -2. Подключите [Notion MCP-сервер](https://developers.notion.com/guides/mcp/get-started-with-mcp) -3. Попросите Claude Code прочитать, проверить и обновить страницы Notion - -### Cursor - -Используйте команду плагинов в чате Cursor Agent: - -``` -/add-plugin -``` - -Найдите «ru-text» и установите. Если плагин не найден в маркетплейсе, скопируйте вручную: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text ~/.cursor/skills/ru-text -``` - -Windows (PowerShell): - -```powershell -git clone https://github.com/talkstream/ru-text.git -Copy-Item -Recurse ru-text\skills\ru-text "$env:USERPROFILE\.cursor\skills\ru-text" -``` - -### GitHub Copilot - -Если ru-text уже установлен для Claude Code в вашем проекте, Copilot обнаружит его автоматически. Иначе: - -```bash -npx skills add talkstream/ru-text -``` - -Или скопируйте вручную: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .github/skills/ru-text -``` - -Работает в VS Code, Visual Studio и JetBrains IDE с Copilot. - -### Gemini CLI - -```bash -gemini extensions install https://github.com/talkstream/ru-text -``` - -### Google Antigravity - -Antigravity читает формат SKILL.md нативно. Скопируйте навык в глобальную папку навыков — он станет доступен во всех проектах: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text ~/.gemini/antigravity/skills/ru-text -``` - -Для конкретного проекта скопируйте навык в `<проект>/.agent/skills/ru-text`. Antigravity молод, и путь к навыкам зависит от версии — актуальный смотрите в [официальном codelab по навыкам Antigravity](https://codelabs.developers.google.com/getting-started-with-antigravity-skills). - -### Windsurf - -```bash -npx skills add talkstream/ru-text -``` - -Или скопируйте вручную: - -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .windsurf/skills/ru-text -``` +## Что решаете вы -Вызов через `@ru-text` в чате Cascade. Также доступен через панель Cascade > Customizations > Skills. +**Правила уступают вашей просьбе.** Скажете «пиши разговорно» — будет разговорно. Академический, юридический, SEO, литературный — то же. Это умолчания, ваша прямая просьба их отменяет. -### Continue.dev +**Мандат выдаёте вы.** Фраза для установки просит вызывать навык для любых задач с русским текстом. Нужен мандат поуже — так и напишите: «вызывай ru-text, только когда я прошу вычитку». Агент исполняет вашу формулировку. -Если ru-text уже установлен для Claude Code в вашем проекте, Continue обнаружит его автоматически. Иначе: +**Ничего не переписывается молча.** При проверке навык возвращает исправленный вариант и список изменений; правит файл, только если вы прямо об этом попросили. -```bash -npx skills add talkstream/ru-text -``` +**Чужой текст остаётся чужим.** Цитаты, код, чужие фрагменты внутри вашего документа воспроизводятся как есть: замечание — возможно, правка — никогда. -Или скопируйте вручную: +**Выключается одной командой.** В Claude Code — `/plugin`, на остальных площадках — удалить каталог навыка. -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .continue/skills/ru-text -``` +Иногда агент прогоняет вычитку сам, не спросив: так настроены его автопроверки. Такой проход начинается с быстрой проверки, и она никогда не выдаёт себя за полную вычитку. -Работает в расширениях для VS Code и JetBrains. +## Что он правит ещё -### Cline +Кнопки, ошибки, пустые состояния: кнопка называет действие — «Отмена» вместо «Нет»; ошибка говорит, что случилось и что делать дальше; плейсхолдер — пример, а не инструкция. -Если ru-text уже установлен для Claude Code в вашем проекте, Cline обнаружит его автоматически. Иначе: +Письма и сообщения: тема письма начинается с дела — «Согласовать бюджет на Q3»; первое предложение несёт просьбу или вывод; «довожу до сведения» разворачивается в «сообщаю». Тон уважительный, без заискивания. -```bash -npx skills add talkstream/ru-text -``` +Лендинги и документация: вместо «команды профессионалов» — то, что можно проверить; вывод сверху, по «перевёрнутой пирамиде»; ссылка говорит, куда ведёт. -Или скопируйте вручную: +## Оценка текста -```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .cline/skills/ru-text -``` +В Claude Code есть две команды. `/ru-text:ru-score` снимает с текста показания: балл от 0 до 10 по пяти шкалам — типографика, чистота языка, грамотность, структура, точность для читателя. К каждому замечанию идёт цитата из вашего текста и правило, по которому оно сделано, так что балл можно проверить построчно и оспорить. `/ru-text:ru-check` делает разбор без оценки: находки, правило на каждую, предложенная замена. На остальных площадках попросите словами: «оцени этот текст по ru-text», «вычитай» — рубрика подгрузится сама. -Включите навыки в настройках Cline: Features > Enable Skills. +Вместе с баллом рубрика печатает, чего она не мерила: фактическую точность, попадание в аудиторию, авторский голос и оригинальность, эффективность, соответствие брифу. Голос в балл не входит: 8,0 у осторожного текста и 8,0 у резкого значат одно и то же. -### JetBrains (Junie) +Два верхних ярлыка — «Эталонный» и «Хороший» — не достаются документу, который остался стенограммой чата или писался для поисковика: такой текст бывает чист в каждой фразе и бесполезен целиком. Рубрика назовёт правило, которое ограничило ярлык. Балл при этом печатается как посчитался. -```bash -npx skills add talkstream/ru-text -``` +## Корпус -Или скопируйте вручную: +Более 2 000 лингвистических атомов: правил, пар «плохо → хорошо», словарных статей и исключений. Это пол, а не точное число, и он считается командой: ```bash -git clone https://github.com/talkstream/ru-text.git -cp -r ru-text/skills/ru-text .junie/skills/ru-text +tools/extract-atoms.sh skills/ru-text | wc -l ``` -Работает в IntelliJ IDEA, PyCharm, WebStorm, GoLand, PhpStorm, RubyMine, RustRover, Rider, CLion и Android Studio. +Раньше это число я вписывал руками. Оно разъехалось по файлам, я правил его сразу в девяти — и в записи об этой правке ошибся даже в числе файлов. Теперь его печатает скрипт. -### OpenClaw - -```bash -openclaw skills install ru-text -``` +Корпус разложен по 10 справочникам, и они подгружаются по запросу: при старте сессии контекст свободен. Откройте любой и посчитайте правила сами. -Доступен на [ClawHub](https://clawhub.ai/talkstream/ru-text). Работает с любым LLM-провайдером и каналом, который поддерживает OpenClaw. +- [`typography.md`](skills/ru-text/references/typography.md) — кавычки, тире, неразрывные пробелы, разрядка чисел, сокращения +- [`info-style.md`](skills/ru-text/references/info-style.md) — каталог из 92 стоп-слов, структура текста, факты вместо оценок +- [`editorial-punctuation.md`](skills/ru-text/references/editorial-punctuation.md) — сложные предложения, запятые-ловушки, вводные слова +- [`editorial-grammar.md`](skills/ru-text/references/editorial-grammar.md) — согласование, плеоназмы, управление, деепричастия, омофоны +- [`ux-writing.md`](skills/ru-text/references/ux-writing.md) — кнопки, ошибки, пустые состояния, формы, уведомления, диалоги подтверждения +- [`business-writing.md`](skills/ru-text/references/business-writing.md) — письма, мессенджеры, тон, заметки к встречам +- [`anti-patterns.md`](skills/ru-text/references/anti-patterns.md) — пары «неправильно — правильно» по степени серьёзности +- [`addenda.md`](skills/ru-text/references/addenda.md) — 17 примет машинного текста с оговорками +- [`scoring.md`](skills/ru-text/references/scoring.md) — рубрика оценки: измерения, веса, полы +- [`sources.md`](skills/ru-text/references/sources.md) — источники и атрибуция -### Любая платформа через skills CLI +## Сколько это стоит контекста -```bash -npx skills add talkstream/ru-text -``` +ru-text не гоняет корпус целиком на каждый абзац: это было бы расточительством за ваш счёт. -### Из исходников +Постоянно в контексте висит один файл — всегда включённый навык. Это 4 килобайта: таблица типографики и верхушка стоп-слов. Справочники лежат рядом и подгружаются, когда до них дошло дело. -```bash -git clone https://github.com/talkstream/ru-text.git -``` +Скажете «вычитай» или «прогони ru-text» — он читает весь корпус. -Затем добавьте репозиторий как источник плагинов по документации вашей платформы. +Когда агент проверяет себя сам, идёт быстрый проход по индексу примет и каталогу стоп-слов. Он ловит типографику и стоп-слова — то, что решается по одной строке. Наберётся пять замечаний или мелькнёт след машинного текста — проход сам разворачивается в полную вычитку. -Начните писать на русском — плагин подключится сам. Если ru-text делает ваши продукты лучше, поддержите [разработку](https://github.com/sponsors/talkstream). +Приметы машинного текста быстрый проход не судит: у каждой есть оговорка, где приём законен, а оговорки живут только в полном справочнике. Увидел след — это повод развернуться. ## Обновление -Свежая версия — **v1.10.1** (что нового — в [CHANGELOG](CHANGELOG.md)). Узнать свою версию в Claude Code: `claude plugins list`. - -**Основной способ** — для платформ на основе навыков (GitHub Copilot, Windsurf, Continue.dev, Cline, JetBrains Junie, Google Antigravity, ручная установка в Cursor). Повторите установку — команда подтянет последнюю версию из репозитория и перезапишет навык: - -```bash -npx skills add talkstream/ru-text -``` - -**Claude Code (CLI и Desktop).** Сначала обновите кэш маркетплейса, затем плагин: +У разовой установки нет механизма обновления: агент поставил навык и забыл о нём. Сигнал один: релизы репозитория, Watch → Custom → Releases. Пришло письмо о релизе — попросите агента повторить установку. Что изменилось, написано в [CHANGELOG](CHANGELOG.md), команды для каждой площадки — в [INSTALL.md](INSTALL.md#обновление). -```bash -claude plugins marketplace update claude-community -claude plugins update ru-text@claude-community -``` +Закрепление ru-text в community-маркетплейсе Claude Code отстаёт от свежей версии на месяцы. Свою версию покажет `claude plugins list`; если она старая, поставьте навык копированием: три команды в [INSTALL.md](INSTALL.md#общий-каталог). -Перезапустите Claude Code (или выполните `/reload-plugins`), чтобы изменения вступили в силу. То же самое можно сделать в меню `/plugin`, вкладка «Installed». Важно: community-маркетплейс закрепляет плагин на конкретной версии и подтягивает свежую с задержкой до суток — если сразу после релиза версия не сменилась, это нормально, дайте маркетплейсу обновить закрепление. - -**Gemini CLI:** - -```bash -gemini extensions update ru-text -``` - -**OpenClaw:** - -```bash -openclaw skills update ru-text -``` - -**Codex CLI.** Откройте `/plugins`, найдите ru-text и обновите. - -**Ручное копирование.** Если вы устанавливали навык вручную (`git clone` + `cp`), повторите шаги установки своей платформы — они перезапишут навык свежей версией. - -## Домены - -| Домен | Правил | Что покрывает | -|---|---|---| -| Типографика | 96 | Кавычки (ёлочки, лапки), тире, неразрывные пробелы, разрядка чисел, спецсимволы, сокращения | -| Информационный стиль | 197 | Стоп-слова (92 записи), структура текста, факты вместо оценок, регистр, редполитика Т–Ж | -| Редактура: пунктуация | 88 | Сложные предложения, 56 запятых-ловушек, вводные слова, точка с запятой | -| Редактура: грамматика | 171 | Заглавные буквы, согласование, 50+ плеоназмов, оформление списков, принципы чистого языка | -| UX-тексты | 217 | 58 надписей на кнопках, ошибки, пустые состояния, формы, уведомления, диалоги, онбординг | -| Деловая переписка | 128 | Структура писем, этикет мессенджеров, тон, 41 паттерн чистых формулировок, заметки к встречам | -| Антипаттерны | 138 | Пары «неправильно — правильно», сгруппированные по серьёзности: канцелярит, пассивный залог, многословие | - -## Команды - -| Команда | Описание | -|---|---| -| `/ru-text` | Активировать навык вручную (в большинстве случаев достаточно автоактивации) | -| `/ru-text:ru-check` | Запустить комплексную проверку качества текста | -| `/ru-text:ru-score` | Оценить качество текста по шкале 0–10 в 5 измерениях | - -## Приоритет стиля - -Если вы явно запрашиваете определённый стиль — разговорный, академический, SEO, литературный, юридический — ваш запрос приоритетнее правил по умолчанию. Плагин задаёт качественные настройки, а не жёсткие требования. - -## Техническое качество - -Собран по спецификациям плагинов Claude Code от Anthropic: -- SKILL.md: 587 слов, 90 строк (рекомендация: до 2 000 слов, до 500 строк) -- 9 справочных файлов загружаются по запросу, не при старте сессии -- ~1 044 правил, разбитых на 7 тематических блоков с прогрессивным раскрытием - -## Уведомление об интеллектуальной собственности - -Данный плагин является самостоятельным авторским произведением Арсения Камышева. - -Правила и принципы, содержащиеся в плагине, представляют собой личное -понимание автором стандартов русской типографики, редактуры и текста, -сформированное за годы профессиональной практики и изучения опубликованных -источников, перечисленных ниже. - -Все формулировки оригинальны. Ни один текст не цитируется дословно. -Лежащие в основе принципы (правила типографики, нормы грамматики, методы -редактуры) не являются объектами авторского права в силу ст. 1259(5) ГК РФ, -17 USC §102(b) и Бернской конвенции. - -Авторы и издатели перечисленных источников не одобряли, не рецензировали -и не утверждали данный плагин. Ссылки на источники приводятся для удобства -читателя и дальнейшего изучения. - -Названия продуктов являются товарными знаками соответствующих -правообладателей и используются исключительно в информационных целях. - -## Дорожная карта - -Следующие направления развития ru-text: +## Источники и благодарности -- **Telegram-бот** — проверка качества текста и /ru-score через Telegram -- **Расширение для браузера** — проверка русского текста в любом текстовом поле (Chrome, Firefox) -- **Плагин для WordPress** — типографика и оценка качества в редакторе Gutenberg +Эти книги, гайды и инструменты научили меня работать с русским текстом. Если ru-text экономит вам время — купите их книги и пользуйтесь их инструментами. -Идеи и предложения приветствуются — [создайте issue](https://github.com/talkstream/ru-text/issues) или [начните обсуждение](https://github.com/talkstream/ru-text/discussions). +**Типографика и вёрстка.** Артём Горбунов, «Типографика и вёрстка» · [Советы Бюро Горбунова](https://bureau.ru/soviet/) · А. Э. Мильчин, Л. К. Чельцова, «Справочник издателя и автора» · [Типографская раскладка Ильи Бирмана](https://ilyabirman.ru/typography-layout/) · [Журнал Type.today](https://type.today) -## Источники и благодарности +**Информационный стиль.** Максим Ильяхов, «Пиши, сокращай» и «Ясно, понятно» · [Редполитика Т—Ж](https://journal.tinkoff.ru/manual/) · [Гайды Контура](https://guides.kontur.ru) · [Яндекс Gravity UI](https://gravity-ui.com) -### Типографика +**Язык и письмо.** Артемий Лебедев, «[Ководство](https://www.artlebedev.ru/kovodstvo/)» · Нора Галь, «[Слово живое и мёртвое](http://lib.ru/TRANSLATORS/NORA_GAL/slowo.txt)» · справочники Д. Э. Розенталя · М. Ильяхов, Л. Сарычева, «Новые правила деловой переписки» · [UX-практики Ozon](https://habr.com/ru/companies/ozontech/articles/821383/) · ГОСТ Р 7.0.12-2011 и ГОСТ 7.12-93 -| # | Источник | Вклад | Ссылка | -|---|---|---|---| -| 1 | **Артём Горбунов «Типографика и вёрстка»** (2017) | Базовые правила типографики: тире, кавычки, пробелы, экранная типографика | [bureau.ru/projects/book-typography/](https://bureau.ru/projects/book-typography/) | -| 2 | **Бюро Горбунова «Советы»** (2005–н. в., 4809+ советов) | Практические микросоветы по типографике, редактуре, дизайну | [bureau.ru/soviet/](https://bureau.ru/soviet/) | -| 3 | **А. Э. Мильчин, Л. К. Чельцова «Справочник издателя и автора»** (2021, 6-е изд.) | Пунктуация, сокращения, оформление чисел, редакционные конвенции | [store.artlebedev.com](https://store.artlebedev.com) | -| 4 | **Илья Бирман — Типографическая раскладка** (2007–н. в.) | Клавиатурная раскладка для набора типографически корректных символов | [ilyabirman.ru/typography-layout/](https://ilyabirman.ru/typography-layout/) | -| 5 | **Type.today — Журнал** (2016–н. в.) | Кириллический шрифтовой дизайн, подбор шрифтовых пар, удобочитаемость | [type.today](https://type.today) | +Полный список с указанием вклада каждого источника — в [`sources.md`](skills/ru-text/references/sources.md). -### Информационный стиль и ясное письмо +Рядом стоят и инструменты: [Главред](https://glvrd.ru), [Типограф Лебедева](https://www.artlebedev.ru/typograf/), [Орфограммка](https://orfogrammka.ru). -| # | Источник | Вклад | Ссылка | -|---|---|---|---| -| 6 | **Максим Ильяхов «Пиши, сокращай»** (2017, обн. 2025) | Основа инфостиля: удаление воды, борьба с канцеляритом, письмо от читателя | [book.glvrd.ru](https://book.glvrd.ru) | -| 7 | **Максим Ильяхов «Ясно, понятно»** (2019) | Продвинутый инфостиль: структура текста, убеждение, визуально-текстовая интеграция | [book.glvrd.ru](https://book.glvrd.ru) | -| 8 | **Редполитика Т–Ж** (2017–н. в., 56+ страниц) | Тон, форматирование, числа, стандарты делового письма | [journal.tinkoff.ru/manual/](https://journal.tinkoff.ru/manual/) | -| 9 | **Контур — Гайды** (2020–н. в.) | UX-тексты для B2B-софта: интерфейсные тексты, ошибки, онбординг | [guides.kontur.ru](https://guides.kontur.ru) | -| 10 | **Яндекс — Gravity UI** (2023–н. в.) | Дизайн-система с гайдлайнами для русскоязычных UI-текстов | [gravity-ui.com](https://gravity-ui.com) | +## Правовая справка -### Язык и письмо +ru-text — самостоятельное авторское произведение Арсения Камышева. Правила в нём — личное понимание автором стандартов русской типографики и редактуры, сложившееся за годы практики и чтения перечисленных источников. Формулировки оригинальные, дословных цитат нет. Сами принципы (правила типографики, нормы грамматики, приёмы редактуры) авторским правом не охраняются: ст. 1259(5) ГК РФ, 17 USC §102(b), Бернская конвенция. -| # | Источник | Вклад | Ссылка | -|---|---|---|---| -| 11 | **М. Ильяхов, Л. Сарычева «Новые правила деловой переписки»** (2018) | Структура писем, уважительный тон, этикет мессенджеров | [book.glvrd.ru](https://book.glvrd.ru) | -| 12 | **Нора Галь «Слово живое и мёртвое»** (1972, переиздания) | Первоисточник критики канцелярита, злоупотребления номинализацией, пассивного залога | [lib.ru](http://lib.ru/TRANSLATORS/NORA_GAL/slowo.txt) | -| 13 | **Д. Э. Розенталь — Справочники по правописанию** (1960-е–2000-е) | Авторитетная база по грамматике, пунктуации, орфографии | широко доступны | -| 14 | **Артемий Лебедев «Ководство»** (1998–н. в.) | Экранная типографика, тире и кавычки, читаемость дизайн-текста | [artlebedev.ru/kovodstvo/](https://www.artlebedev.ru/kovodstvo/) | -| 15 | **Ozon — UX Writing Practices** (2021–н. в.) | UX-тексты в масштабе: кнопки, уведомления, ошибки, продуктовые тексты | [habr.com](https://habr.com/ru/companies/ozontech/articles/821383/) | -| 16 | **ГОСТ Р 7.0.12-2011, ГОСТ 7.12-93** (Росстандарт) | Официальные стандарты библиографических сокращений | базы ГОСТ | +Авторы и издатели перечисленных источников этот плагин не одобряли и не рецензировали. Ссылки даны для удобства читателя. Названия продуктов принадлежат их правообладателям. -### Онлайн-инструменты +## Дальше -- **Главред** ([glvrd.ru](https://glvrd.ru)) — проверяет текст на качество инфостиля, подсвечивает воду, оценивает от 0 до 10 -- **Типограф Лебедева** ([typograf.artlebedev.ru](https://www.artlebedev.ru/typograf/)) — автоматически исправляет типографику: кавычки, тире, неразрывные пробелы -- **Орфограммка** ([orfogrammka.ru](https://orfogrammka.ru)) — проверка грамматики, орфографии и пунктуации +Дальше хочу телеграм-бота и расширение для браузера. Про плагин для WordPress пока только думаю. Идеи и замечания — в [issues](https://github.com/talkstream/ru-text/issues) или [обсуждениях](https://github.com/talkstream/ru-text/discussions). ## Автор -**Арсений Камышев** — [nafigator@gmail.com](mailto:nafigator@gmail.com) — [Telegram](https://t.me/nafigator) — [GitHub](https://github.com/talkstream) - -## Поддержка - -Я всю жизнь занимаюсь социальными проектами. В этой сфере я максимально эффективен для людей и общества, поэтому **всегда** нуждаюсь в финансовой поддержке. Если ru-text делает ваши продукты лучше, поддержите меня через [GitHub Sponsors](https://github.com/sponsors/talkstream). +Арсений Камышев — [nafigator@gmail.com](mailto:nafigator@gmail.com) · [Telegram](https://t.me/nafigator) · [GitHub](https://github.com/talkstream) -## Лицензия +Если ru-text сэкономил вам вычитку — [GitHub Sponsors](https://github.com/sponsors/talkstream). Нашли правило, которое неправо? Напишите в [issues](https://github.com/talkstream/ru-text/issues). Корпус растёт и от таких находок, и в CHANGELOG они подписаны именами. -[MIT](LICENSE) | [Политика конфиденциальности](PRIVACY_POLICY.md) +[MIT](LICENSE) · [Политика конфиденциальности](PRIVACY_POLICY.md) · плагин не делает сетевых запросов и не собирает данные. Эта страница вычитана текущей версией ru-text. diff --git a/docs/roadmap-v3-formative.md b/docs/roadmap-v3-formative.md new file mode 100644 index 0000000..115e5c2 --- /dev/null +++ b/docs/roadmap-v3-formative.md @@ -0,0 +1,81 @@ +# v3.0 — the formative direction + +Everything ru-text does today is a refusal. Stop-words, канцелярит, anti-patterns, the +sixteen tells of machine-written prose: the corpus is a list of what not to write, and it +is good at it. v3.0 adds the other half — what a living, literate Russian sentence *does*, +stated so that a checker can point at it. + +This file is the backlog for that direction. Nothing here is scheduled and nothing here is +a rule yet. It exists so the direction survives the session that named it. + +## Why, and on what evidence + +The author ran the formative approach on a different project — guidance written as what to +do rather than what to avoid — and reports it as an unambiguous quality win. That is one +practitioner's experience, deliberately recorded as such: it is the reason to open the +direction, not the evidence that any particular rule belongs in it. Each rule below still +has to earn its place through the same cycle every rule in this corpus went through — +evidence channels read blind to each other, a panel, a golden case, a judge. + +There is a second, measured reason. On 28.07.2026 a single probe of the "positive craft" +axis — repetition of a full noun phrase where the language would use a pronoun — found a +gap on the first attempt. One probe, one hole is not a coverage estimate, but it is not +nothing either: the refusal half has been swept many times, and this half has not been +swept at all. + +## What makes this hard, and why it is a separate version + +A refusal is cheap to check and cheap to be wrong about. «Осуществлять» is on a list or it +is not; a false positive costs the reader one dismissed line. A formative rule is the +opposite on both counts. «Use a concrete noun instead of a category noun» has no list to +match against, its judgement depends on what the sentence is doing, and a false positive +tells a competent writer their good sentence is wrong — which is how a tool loses a user. + +Three constraints follow, and they are why this is v3.0 and not a patch to v2.0: + +**Every formative rule must be demonstrable on a fragment.** If the guidance cannot be +shown by quoting six words and explaining the alternative in two lines, it is taste, and +taste does not belong in a corpus that claims to be checkable. This is the same bar the +neuroslop work used to reject nine of its own candidates. + +**The scoring model does not fit as it stands.** The five dimensions are built to subtract: +each anchor describes degrees of wrongness. A rule that rewards a well-chosen verb has +nowhere to land — adding points for virtue invites exactly the optimisation the five +orthogonal dimensions exist to prevent (see `scoring.md`, «Why context-aware evaluation»). +Whether the formative half scores at all, or only advises, is the first design question of +v3.0 and it precedes any rule. + +**The prohibitions of v2.0 must survive intact.** Two classes are off limits by the +director's standing decision and stay off limits here: dry academic and regulatory register +as such, and translated or non-native Russian. A formative rule that reads «write livelier» +lands on both of them harder than any refusal ever did. + +## Candidate areas, unranked and unearned + +Recorded as directions to investigate, not as rules. Each needs the full cycle before it is +anything. + +- **The concrete noun over the category noun** — «поставщик» where the text means «пекарня + на Мясницкой». Carve-out: legal and regulatory texts name categories on purpose. +- **The verb over the copula** — «является поставщиком» → «поставляет». Adjacent to the + existing §B entry for «является», but as a positive pattern rather than a banned word. +- **The named actor in the active voice** — not «было принято решение» but who decided. + Partly covered by the passive-voice anti-pattern; the formative half is the naming. +- **Word order carrying emphasis** — тема before рема, the stressed element last. Well + described in the tradition, and genuinely hard to check without misfiring on inversion + used deliberately. +- **Register fit of the individual word** — a word from the wrong stratum inside an + otherwise consistent sentence. +- **Sound defects of written prose** — accidental rhyme, a run of sibilants, a collision of + identical syllables across a word boundary. Objective, demonstrable, and absent from the + corpus. +- **Sentence-length variety** — flagged here with an explicit warning: even rhythm was + examined and **rejected** during the neuroslop work, because it is what dry regulatory + prose legitimately looks like. Any candidate in this area must first show how it differs + from the rejected one, or it is the rejected one. + +## What is NOT in this direction + +The sixteen tells of machine-written prose, the stop-word catalogue, the anti-patterns, the +typography and the punctuation. Those are v2.0 and they are finished. This file is only for +the half that says what to do. diff --git a/docs/roadmap-v3-grammar.md b/docs/roadmap-v3-grammar.md new file mode 100644 index 0000000..21f8fa3 --- /dev/null +++ b/docs/roadmap-v3-grammar.md @@ -0,0 +1,92 @@ +# v3.0 — the grammar layer the corpus does not have + +A separate file from `roadmap-v3-formative.md` on purpose. That one opens the formative +half — what a living sentence *does* — and declares the prohibitive half finished. This one +says the prohibitive half is **not** finished, in one specific place, and the two would +contradict each other under a single heading. + +## What was measured + +On 28.07.2026 four evidence channels were read blind to each other and every candidate was +screened by a second agent against the corpus, by command. The finding was not where it was +expected. Style is covered thoroughly — info-style, канцелярит, anti-patterns, sixteen tells +of machine prose, 96 typography rules, 88 punctuation rules. **Grammar in the narrow sense +is close to empty**: government, participial commas, declension of compound numerals and +surnames, verb aspect. Verified absent with greps recorded in the dossier. + +That matters because Г carries weight 0.20 and `scoring.md:77` calls it «hard (objective)». + +## The cost is not what it looks like + +The obvious reading — the tool is blind to «согласно приказа» — is wrong, and the arbiter +was right to correct it. ru-text is executed by a language model, and a model flags that +phrase with or without a rule. What is missing is the **support for the verdict**: a finding +with no rule ID, no golden anchor, no reproducible ground in the one dimension that claims +to be objective. + +So the work here is not to reproduce Rozental. It is to give the most frequent indisputable +errors something to stand on. + +## Shipped in v2.0 + +Three positions, one panel, one judge — the narrow package the arbiter allowed: + +- **A gerund phrase whose subject is not the sentence's subject.** «Подъезжая к станции, у + меня слетела шляпа». Mechanical test: the doer of the gerund and the grammatical subject + must be the same. Carve-outs: an impersonal clause with an infinitive («Подъезжая к + станции, следует смотреть в окно»), and the forms that have hardened into prepositions — + «судя по», «исходя из», «несмотря на», «начиная с». +- **Government, by closed list only.** «Согласно приказа» → «приказу», «оплатить за проезд» + → «оплатить проезд», «заведующий отделом», «преимущество перед», «уверенность в». A closed + list because the open form — checking every case after every preposition — is a detector of + non-native Russian, which this project is forbidden to build. +- **Different government under shared dependents.** «Организовать и руководить работой»: + two verbs, one object, incompatible cases. + +## Backlog, in the arbiter's order + +**Contextual homophones** — «в течение / в течении», «чтобы / что бы», «также / так же», +«ввиду / в виду». A closed list of about ten pairs with a decidable rule for each («в +течение» + genitive of time). Spellcheckers do not catch these because both spellings exist. +Conditionally in v2.0; first to drop if the release is overloaded. + +**Participial and adverbial commas.** «Файл загруженный вчера содержит ошибку». Genuinely +absent and genuinely an error — and deliberately deferred, because it is an open class +(uточнения, приложения, сравнительные оборот) with no natural boundary, and because every +comma becomes a finding. In an ordinary draft the commas would outnumber and bury the +stylistic findings, which turns an editor into a school tutor. If it is ever done, it needs +a way to report «twelve comma issues» as one finding. + +**Declension: compound numerals, «полтора», masculine surnames.** «С шестистами рублями» → +«шестьюстами»; «письмо Кравчук Игорю» → «Кравчуку». One channel only, and half the family +was amputated during screening: place names in -о and the gender of abbreviations were +dropped as contested. In written text digits hide most numeral errors, so the real frequency +is below the estimate. + +**Aspect and tense mismatch under coordinated predicates.** «Он открыл файл и сохранял +изменения». Passed one screen, and only with the trigger narrowed — in its raw form it flags +correct sentences, and a false positive inside a hard dimension costs more than a miss. + +**Paronyms, by closed list.** Надеть / одеть, представить / предоставить, роспись / подпись, +абонент / абонемент. The channels split two against two. The business pairs are objective +and do not touch the protected registers, but context decides — «роспись на стене» is +correct — so the list has to carry its condition with each pair. + +## Rejected, with the reason, so it is not re-proposed + +- **Wandering pronoun / ambiguous reference.** The channels split 2–2, and that split is + itself the evidence: the trigger is a judgement about whether context resolves the + reference. Same ground as the amphiboly rejection. +- **Syntactic ambiguity (amphiboly).** One of its two mechanical triggers is already taken + (genitive chains, `editorial-grammar.md:271`, `:351`); the other fires on masses of + unambiguous phrases, because «который» resolves to the nearest noun by default. +- **Contamination of set phrases** («играть значение»). Not covered by anything — and not + reproducible without a dictionary of set collocations. +- **Speech insufficiency** («отредактировал и опубликовал в блог»). Not covered either, and + rejected for reasons of its own rather than coverage. +- **Stacked «который» clauses**, **collisions of function words**, **accidental rhyme in + prose** — protected register, overlap with a rejected candidate, or not objective. + +Three of these were labelled «covered» in the first draft of the dossier. They are not +covered; they are rejected, which is a different thing and a more dangerous mislabel — a +reader concludes there is no hole. diff --git a/gemini-extension.json b/gemini-extension.json index 060a845..32bafbe 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,5 +1,5 @@ { "name": "ru-text", - "version": "1.10.1", - "description": "Russian text quality — ~1,044 rules for typography, info-style, editorial, UX writing, business correspondence, and AI-text cleanup" + "version": "2.0.0", + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand." } diff --git a/notion/README.md b/notion/README.md index e57dc4a..1fdc630 100644 --- a/notion/README.md +++ b/notion/README.md @@ -24,17 +24,18 @@ Turn any Notion page into a Russian text quality assistant. Requires Notion Busi ### What's included -The skill template contains ~1,450 words of condensed rules: +The skill template is a condensed subset of the corpus: - 12 typography rules (always applied) - 92 stop-words across 8 categories with replacements - 30 anti-patterns (bureaucratic language, passive voice, bloat, pleonasms) +- 9 AI-text tells (neuroslop) across 4 categories - Quality checklist -For the full set of ~1,044 rules across 7 domains, use the [complete plugin](https://github.com/talkstream/ru-text) with Claude Code, Codex CLI, Gemini CLI, Cursor, or OpenClaw. +For the full set of 2,000+ linguistic atoms across 7 domains, use the [complete plugin](https://github.com/talkstream/ru-text) with Claude Code, Codex CLI, Gemini CLI, Cursor, or OpenClaw. ## Option B: Notion via MCP (Claude Code) -Use the full ru-text plugin with Notion pages through Claude Code and the official Notion MCP server. This gives you access to all ~1,044 rules with progressive loading. +Use the full ru-text plugin with Notion pages through Claude Code and the official Notion MCP server. This gives you access to the whole corpus with progressive loading. ### Setup @@ -89,17 +90,18 @@ Score the text quality of my Notion page "About us" using /ru-text:ru-score. ### Что входит -Шаблон навыка содержит ~1450 слов сжатых правил: +Шаблон навыка — сжатая выборка из корпуса: - 12 правил типографики (применяются всегда) - 92 стоп-слова в 8 категориях с заменами - 30 антипаттернов (канцелярит, пассивный залог, многословие, плеоназмы) +- 9 примет ИИ-текста (нейрослоп) в 4 категориях - Чеклист качества -Полный набор из ~1 044 правил в 7 доменах доступен через [полную версию плагина](https://github.com/talkstream/ru-text) для Claude Code, Codex CLI, Gemini CLI, Cursor или OpenClaw. +Полный набор из более чем 2 000 лингвистических атомов в 7 доменах доступен через [полную версию плагина](https://github.com/talkstream/ru-text) для Claude Code, Codex CLI, Gemini CLI, Cursor или OpenClaw. ## Способ Б: Notion через MCP (Claude Code) -Полный плагин ru-text для работы со страницами Notion через Claude Code и официальный Notion MCP-сервер. Доступны все ~1 044 правил с прогрессивной загрузкой. +Полный плагин ru-text для работы со страницами Notion через Claude Code и официальный Notion MCP-сервер. Корпус доступен целиком, с прогрессивной загрузкой. ### Настройка diff --git a/notion/ru-text-notion-skill.md b/notion/ru-text-notion-skill.md index 461bad3..6448565 100644 --- a/notion/ru-text-notion-skill.md +++ b/notion/ru-text-notion-skill.md @@ -2,7 +2,7 @@ You are a Russian text quality assistant. When the user selects text and invokes this skill, apply the rules below: fix typography automatically, flag stop-words and anti-patterns, suggest improvements. If the user explicitly requests a specific style (casual, academic, SEO, literary), their request overrides these defaults. -Independent reference by Arseniy Kamyshev. This template includes the most common entries from each category. Full version with ~1,044 rules across 7 domains: [github.com/talkstream/ru-text](https://github.com/talkstream/ru-text) +Independent reference by Arseniy Kamyshev. This template includes the most common entries from each category. Full version with 2,000+ linguistic atoms across 7 domains: [github.com/talkstream/ru-text](https://github.com/talkstream/ru-text) ## Typography Rules @@ -279,4 +279,4 @@ Before delivering Russian text, verify: --- -*ru-text — independently formulated rules for Russian text quality. Full version (~1,044 rules, 7 domains): [github.com/talkstream/ru-text](https://github.com/talkstream/ru-text). Author: Arseniy Kamyshev. License: MIT.* +*ru-text — independently formulated rules for Russian text quality. Full version (2,000+ atoms, 7 domains): [github.com/talkstream/ru-text](https://github.com/talkstream/ru-text). Author: Arseniy Kamyshev. License: MIT.* diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 61ee8f5..8b64697 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,8 +1,8 @@ { "id": "ru-text", "name": "ru-text", - "version": "1.10.1", - "description": "Russian text quality plugin — typography, information style, editorial standards, UX writing, business correspondence, and AI-text cleanup", + "version": "2.0.0", + "description": "Russian text quality for AI agents — neuroslop cleanup, typography, information style, editorial standards, UX writing, business correspondence. 2,000+ linguistic atoms, loaded on demand.", "configSchema": { "type": "object", "additionalProperties": false diff --git a/skills/ru-check/SKILL.md b/skills/ru-check/SKILL.md index d769ad1..2e2646a 100644 --- a/skills/ru-check/SKILL.md +++ b/skills/ru-check/SKILL.md @@ -1,19 +1,22 @@ --- name: ru-check -description: Run a comprehensive Russian text quality check on provided text or recent output +description: > + Full Russian text quality check against the whole corpus. Triggers: вычитай, вычитай через + ru-text, прогони ru-text, проверь текст по ru-text, ru-check, полная вычитка. Use when the + user asks to proofread Russian text, or when a project gate names ru-text. Returns findings + with the rule behind each and a proposed replacement; never edits a file. Self-initiated + runs start with a fast triage and escalate on evidence; an explicit request always runs the + full corpus. allowed-tools: Read, Grep, Glob disallowed-tools: Write, Edit, NotebookEdit, Bash, PowerShell, Monitor context: fork user-invocable: true -disable-model-invocation: true --- # Russian Text Quality Check Review the text provided in $ARGUMENTS (or the most recent Russian text output if no arguments) using the ru-text skill. -## Check order - ## Where the reference files live This skill reads the corpus that ships with the **ru-text** skill, which is installed @@ -26,6 +29,47 @@ alongside it. Locate that folder once, then read the named files from it: against remembered rules instead of the corpus is not this command, and reporting one as the other is the failure this whole product exists to prevent. +## Two depths, one command + +This check runs at one of two depths. Nobody chooses a depth; these rules do. + +**Full** — the «Check order» below, whole. Run it whenever a person asked for this check +(«вычитай», «прогони ru-text», a gate that names ru-text, a golden-set run) or triage +escalated. An explicit request is never answered with triage. + +**Triage** — for self-initiated runs only: you produced or encountered Russian text and are +checking it out of discipline, with no instruction naming ru-text. Load three things and +nothing else: + +1. The ru-text `SKILL.md` — its inline typography table and top stop-words. Skip if it is + already in context, which on an always-on host it usually is. +2. The «Neuroslop index» section of `addenda.md`: from the `## Neuroslop index` heading to + the next `##`. Not the rest of the file — the rest is ten times the size. +3. Section «B. Каталог стоп-слов» of `info-style.md`: from its `## B.` heading to the next + `##`. Not the rest of the file. + +Then check the text: typography mechanically (straight quotes, a hyphen doing a dash's work, +`...` for an ellipsis, an ordinary space after в, к, с, о, у, и, а — verify by codepoint, not +by eye); catalog stop-words including inflected forms, judging every candidate line yourself +(«данные» the noun is not «данный» the stop-word); index tells by eye. + +Triage may **report** only what a single line decides: typography and confirmed catalog hits. +**A neuroslop tell is never a triage finding.** Every AD rule carries carve-outs that live +only in the full file, and «не X, а Y» with a real antecedent is ordinary prose — flagging it +from an index alone is the false positive this command would lose the most trust for. A tell +seen in triage is an escalation trigger and nothing else. + +**Escalate to full** when any one of these holds: a neuroslop candidate appeared · five +findings are confirmed · the text is bound for a reader (a deliverable, a publication, a +client). Escalating is silent — continue into the full procedure as though it had been asked +for. + +A triage report names itself: «Быстрая проверка: типографика и стоп-слова. Полная вычитка по +корпусу не выполнялась.» Reporting triage as the full check is the failure this product +exists to prevent. No search tool on this host → no triage: run the full check. + +## Check order + 1. **Typography** — read `typography.md`, then apply: - Quotes: «» primary, „" nested - Dashes: — (em) in text, – (en) in ranges, - (hyphen) in compounds only @@ -53,6 +97,20 @@ alongside it. Locate that folder once, then read the named files from it: - Preemptive virtue qualifier (AD-7) — «без воды», «чётко, по делу» - Assistant-register meta-commentary (AD-8) — «Отличный вопрос!», «Надеюсь, это помогло» - Hollow openers (AD-9) — «давайте разберёмся», «погрузимся», «важно понимать, что» + - Declared sincerity (AD-10) — «честный разбор», «давайте будем честны»: honesty predicated of the piece. + The same reflex on a single statement — «скажу честно: дедлайн сорван» — is AD-7 + - Mandatory tricolon (AD-11) — «инновационный, трансформирующий, прорывной» + - Hollowed mechanism (AD-12) — «зависит от различных факторов», «свои особенности» + - Phantom attribution (AD-13) — «исследования показывают», «эксперты отмечают» + - Chat transcript as the artifact (AD-14) — the document's skeleton is a dialogue + - Search-engine addressee (AD-15) — the query phrase where a pronoun would serve + - Additive pseudo-pair (AD-16) — «не только X, но и Y» where Y adds nothing + + The list above is a prompt for the eye, not the rule set. Two of these — AD-14 and AD-15 + — are charged to the **document**, so ask them of the piece as a whole and not of any + one sentence. Every rule has carve-outs that decide as many cases as the triggers do; + they are in `addenda.md`, and a finding raised without checking them is the false + positive this command costs the most trust for. ## Output format diff --git a/skills/ru-score/SKILL.md b/skills/ru-score/SKILL.md index d80de4b..27c776b 100644 --- a/skills/ru-score/SKILL.md +++ b/skills/ru-score/SKILL.md @@ -1,11 +1,14 @@ --- name: ru-score -description: Score Russian text quality on a 0.0–10.0 scale across 5 dimensions +description: > + Score Russian text 0.0–10.0 across five dimensions: typography, clean language, grammar, + structure, precision for the reader. Triggers: оцени текст, ru-score, оценка качества + текста, насколько хорош текст, балл за текст. Use when the user wants a number rather than + a list of findings. Never edits a file. allowed-tools: Read, Grep, Glob disallowed-tools: Write, Edit, NotebookEdit, Bash, PowerShell, Monitor context: fork user-invocable: true -disable-model-invocation: true --- # Russian Text Quality Score diff --git a/skills/ru-text/SKILL.md b/skills/ru-text/SKILL.md index daab96c..f426b4c 100644 --- a/skills/ru-text/SKILL.md +++ b/skills/ru-text/SKILL.md @@ -1,9 +1,9 @@ --- name: ru-text description: > - Use when writing, editing, or reviewing Russian-language text, or when user - mentions ru-text. Covers typography, info-style, editorial, UX writing, business - correspondence, AI-text cleanup. Auto-activates on Russian text output. + Russian text quality. Triggers: вычитай, проверь текст, поправь, отредактируй, + причеши, ru-text. Also any Russian output. Typography, info-style, editorial, + UX writing, business correspondence, AI-text cleanup. metadata: openclaw: always: true @@ -21,6 +21,8 @@ Credits and recommended reading: `references/sources.md` **Reviewing vs. rewriting**: when *checking* or proofreading existing text or a file, return the corrected version plus a list of changes — do not silently overwrite the source file. Rewrite a file in place only when the user explicitly asks. +**Someone else's words stay theirs**: quoted material, code blocks and third-party text inside the user's document are reproduced as-is. Report an issue you see in them if it matters; never rewrite them. + ## Always-On: Typography Apply these rules to ALL Russian text output without exception. diff --git a/skills/ru-text/references/addenda.md b/skills/ru-text/references/addenda.md index 9ee692d..7233713 100644 --- a/skills/ru-text/references/addenda.md +++ b/skills/ru-text/references/addenda.md @@ -8,6 +8,8 @@ additions. ## Table of Contents +- [Neuroslop index](#neuroslop-index) +- [Two rules that govern all the others](#two-rules-that-govern-all-the-others) - [AD-1. Excessive em dashes](#ad-1-excessive-em-dashes-избыточные-тире) - [AD-2. Excessive parcellation](#ad-2-excessive-parcellation-избыточная-парцелляция) - [AD-3. Patronizing explanation](#ad-3-patronizing-explanation-разжёвывание-очевидного) @@ -17,7 +19,14 @@ additions. - [AD-7. Preemptive virtue qualifier](#ad-7-preemptive-virtue-qualifier-непрошенная-оговорка-без-воды) - [AD-8. Assistant-register meta-commentary](#ad-8-assistant-register-meta-commentary-сервисные-реплики-ассистента) - [AD-9. Hollow opener](#ad-9-hollow-opener-пустой-зачин) -- [Neuroslop index](#neuroslop-index) +- [AD-10. Declared sincerity](#ad-10-declared-sincerity-объявленная-искренность) +- [AD-11. Mandatory tricolon](#ad-11-mandatory-tricolon-обязательная-триада) +- [AD-12. Hollowed mechanism](#ad-12-hollowed-mechanism-выхолощенность) +- [AD-13. Phantom attribution](#ad-13-phantom-attribution-неопределённая-атрибуция) +- [AD-14. Chat transcript as the artifact](#ad-14-chat-transcript-as-the-artifact-диалог-с-нейросетью-вместо-текста) +- [AD-15. Search-engine addressee](#ad-15-search-engine-addressee-адресат--поисковик-а-не-читатель) +- [AD-16. Additive pseudo-pair](#ad-16-additive-pseudo-pair-не-только-x-но-и-y) +- [AD-17. Comma welded to a dash](#ad-17-comma-welded-to-a-dash-запятая-сомкнутая-с-тире) ## Neuroslop index @@ -29,16 +38,50 @@ unverifiable and fast-dating. | Tell | Canonical home | |---|---| | Manufactured antithesis — «не X, а Y» / «не просто X, а Y» with no antecedent | AD-6 | +| Declared sincerity — «честный разбор», «давайте будем честны», «рассказываю как есть» | AD-10 | +| Mandatory tricolon — «инновационный, трансформирующий, прорывной» | AD-11 | +| Hollowed mechanism — «зависит от различных факторов», «свои особенности» | AD-12 | +| Phantom attribution — «исследования показывают», «эксперты отмечают» | AD-13 | +| Chat transcript as the artifact — «Я: … / Модель: …» as the document's skeleton | AD-14 | +| Search-engine addressee — the query phrase repeated where a pronoun would serve | AD-15 | +| Additive pseudo-pair — «не только X, но и Y» where Y adds nothing | AD-16 | | Preemptive virtue qualifier — «без воды», «чётко, по делу», «коротко и ясно» | AD-7 | | Assistant-register meta-commentary — «Отличный вопрос!», «Надеюсь, это помогло» | AD-8 | | Hollow opener — «давайте разберёмся», «погрузимся», «важно понимать, что» | AD-9 | | Excessive em dashes — staccato dash rhythm | AD-1 | +| Comma welded to a dash — «…, — …» inside one sentence | AD-17 | | Throat-clearing stop-words — «стоит отметить, что», «нельзя не отметить» | `info-style.md` §B | | Empty universal preamble — «в современном мире», «не секрет, что» | `info-style.md` §B | | Unproven-claim adjectives — «качественный», «надёжный», «эффективный» | `info-style.md` §B | | Generic conclusion — «таким образом, подводя итог» | `anti-patterns.md` | | Artificial liveliness — exclamation stacks and emoji as a substitute for detail | `info-style.md` §F | +## Two rules that govern all the others + +**Density raises severity; it never replaces the findings.** Several rules here treat a +cluster as their main signal — AD-1, AD-2, AD-6, AD-9, AD-11, AD-16. Once a rule has +decided that it fires, report every instance, individually, with its own quoted fragment, +and let the count govern how far the score moves. One line saying «два и более на текст» in +place of the four fragments is a loss of coverage wearing the clothes of a summary: the +person fixing the text is left without the list of lines to fix. + +This says nothing about **whether** a rule fires. Two of the six — AD-11 and AD-16 — put +density into the flag condition itself, so an isolated triple or an isolated pair is not a +finding at all; that is their own text and it stands. The rule here governs what happens +after a rule has decided to fire, never before. + +Nor is the score card a limit on what the check reports: `scoring.md` asks for one to +three issues per dimension in the scored output; that is the size of a table cell, not the +size of the check. `/ru-text:ru-check` lists what it found; `/ru-text:ru-score` quotes the +most telling of them per dimension. Neither number licenses dropping a finding. + +**A document-level charge adds; it never replaces.** AD-14 and AD-15 are charged to the +piece as a whole. Each absorbs only its own evidence — the repetitions that demonstrate it +— and absorbs nothing else. Every ordinary finding in that text stands: stop-words, +unproven claims, passive voice, punctuation, typography. Both statements exist because a +measured run did the opposite: it replaced ten ordinary findings with three document-level +ones, and the text came out looking better for it. + --- ## AD-1. Excessive em dashes (избыточные тире) @@ -272,6 +315,8 @@ AD-6.6. Single-count with neighbours. A phantom contrast can also read as AD-4 ( AD-6.7 (scope of strengthening). The trigger set now includes «не просто X, а Y» and «вопрос/суть не в X, а в Y». Two look-alikes are NOT auto-flagged: «не столько X, сколько Y» (a degree-narrowing, cf. AD-6.4) and «важно не X, а Y» without an antecedent (often a real reader priority). A cluster of manufactured pairs bites harder in **С — Structure** (see `scoring.md`); the carve-outs hold — asymmetric self-correction (AD-6.4) and the 0–2 legitimate antecedent-backed body pairs (AD-6.5) are never penalised, and the construction is never banned outright. +AD-6.8 (the quota is not a finding). AD-6.5 sets how many pairs a text may carry; it is not a line to report in place of them. Report each manufactured pair with its own fragment and let the count raise the weight in **С — Structure**. In a measured run a checker reported «два и более на ~150 слов» and dropped one of the pairs it was counting. + **Examples:** | Wrong | Correct | @@ -315,6 +360,7 @@ AD-6.7 (scope of strengthening). The trigger set now includes «не прост - «без воды» / «без виляния» / «без лишних слов» / «без обиняков» — a manner-flourish denying a vice of delivery - «честно говоря» / «начистоту» / «прямо скажем» / «не побоюсь этого слова» — announced sincerity +- «скажу честно» / «если честно» / «честно» / «по-честному» / «не буду врать» / «откровенно говоря» / «положа руку на сердце» — the same announced sincerity in the forms a model reaches for most. Where the sincerity is predicated of the whole piece rather than of one statement — «честный разбор», «давайте будем честны» — the home is AD-10; charge the fragment once (AD-10.6) - «и без всякой магии» / «без всяких фокусов» / «без нервов» appended to a claim about one's own product or method - «чётко, по делу» / «коротко и ясно» / «простыми словами» / «разложу по полочкам» / «на пальцах» — a positive-form flourish praising the author's own delivery (the polarity twin of «без воды») - a self-praise tricolon about one's own product or method — «быстро, качественно, надёжно» — where each adjective is itself an unproven claim (cf. `info-style.md` §B) and the rule-of-three stacking is the added tell @@ -330,7 +376,7 @@ AD-7.3. Allow informative «без»: «кофе без сахара», «раб AD-7.4. Allow a genuine epistemic qualifier that carries information: «строго говоря, это аппроксимация», «по сути» when it narrows the claim (cf. AD-6.4). These calibrate meaning; they do not advertise sincerity. -AD-7.5 (register). Conversational and literary registers: «честно говоря», «прямо скажем», «по правде сказать» as natural discourse markers of live speech or a character's voice — not a statement about the author's own product or method — are not flagged. The target is the *self-promotional* manner-flourish (AD-7.1, condition 1), not ordinary spoken connective tissue. When in doubt, apply AD-7.1: if the qualifier praises the author's own delivery, flag it; if it merely colours conversational tone, leave it (cf. AD-2.3, which likewise spares the literary register). +AD-7.5 (register — whose voice it is). The carve-out holds when the construction belongs to a **speaker inside the text**: a line of dialogue, a quotation, an interviewee, a character. «— Скажу честно, я не знаю, — ответил инженер» is speech and is not flagged. It does **not** hold when the author says it about their own text, in any register — an author addressing the reader in a conversational tone is still the author, and «скажу честно» in a lead is the self-promotional flourish AD-7.1 describes, not spoken connective tissue. This is a deliberate narrowing: the earlier wording exempted the conversational register as such, and the assistant register — a monologue written to sound like speech — fell straight through it, which is how a construction listed among the triggers went unflagged for a year. AD-10.4 is the canonical statement of this boundary; it governs both rules, and is stated there in full because a rule read on its own must decide on its own. AD-7.6. Single-count with neighbours. A «без [vice]» flourish can also be caught by AD-2 (parcellation / filler rhythm in Structure) — the «без воды» tail in «Расскажу последовательно, доступно, без воды» is one fragment, not two faults. Count one violation per fragment: charge it to AD-7 (**Ч — Clarity**) when the defect is the empty self-virtue, to AD-2 (**С — Structure**) when the defect is the staccato or filler rhythm. Never double-charge the same fragment across Ч and С. @@ -473,3 +519,487 @@ AD-9.7 (carve-out — dialogue register). «Давайте разберёмся **Severity:** Low. Secondary signal in the **С — Structure** dimension (supporting **Ч — Clarity**). A cluster lowers Structure; an isolated opener does not. Cannot trigger non-compensatory caps alone. **Acknowledged:** identified from corpus analysis of AI-generated Russian prose (2026-06); the hollow opener announces explanation instead of leading with the point and clusters densely in machine drafts. + + +--- + +## AD-10. Declared sincerity (объявленная искренность) + +**Problem:** the text asserts its own honesty as a property of itself — «честный разбор», «давайте +будем честны», «рассказываю как есть», «без прикрас», «вся правда о…». Sincerity is what a reader +infers from what a text does with an inconvenient fact; announced, it is a claim with no evidence, +and it takes the place of the evidence that would have earned it. AD-7 catches the same reflex +attached to one statement («скажу честно: дедлайн сорван»); AD-10 catches it when honesty is +predicated of the piece or of the block that follows. The label form is the louder of the two and is +graded accordingly, and giving it its own name is what lets the carve-out be stated once instead of +twice. + +**Sources:** +- Telling vs. showing, and self-praise as an empty signal: editorial practice, informed by Нора Галь «Слово живое и мёртвое» +- Platform moderation practice treats stylistic resemblance as sufficient grounds on its own, without proof of generation — Habr's site rules as they stood on 27.07.2026, the version announced in the platform's own post of 17.06.2026 (paraphrased, not quoted); this is external evidence that a style-level rule has a real object +- Adjacent to AD-7 (delivery-virtue qualifier) and AD-13 (phantom attribution): all three perform a stance instead of demonstrating it +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger constructions:** + +- «честный разбор» / «честный обзор» / «честно о…» / «вся правда о…» / «без прикрас» — the piece labelled honest, most often in a title +- «давайте будем честны» / «будем откровенны» / «скажем прямо» opening a paragraph or a section +- «рассказываю как есть» / «пишу как думаю» / «без купюр» / «начистоту» +- a title or lead whose only claim is the author's own sincerity +- the strongest signal: the label in a heading, where the reader has been given nothing yet against which to weigh it + +**Rules:** + +AD-10.1. Flag a construction when all three hold: (1) it predicates honesty, frankness or candour of the text, of the author's account, or of the block that follows; (2) the author is speaking about their own text; (3) it carries no fact — deleting it loses nothing. + +AD-10.2. Rewrite by deleting the label and letting the inconvenient fact do the work it was standing in for. «Честный разбор: почему сроки срываются» → «Почему сроки срываются» — and then name the reason. + +AD-10.3. Honesty is demonstrated specifically: an admitted mistake, a number that costs the author something, a stated limit of what is known. Where the demonstration is present the label is redundant; where it is absent the label is false. + +AD-10.4 (carve-out — the speaker inside the text). Not flagged when the construction belongs to a speaker inside the text: a line of dialogue, a quotation, an interviewee, a character. «— Скажу честно, я не знаю, — ответил инженер» is speech, not a label. This is the canonical statement of the boundary the narrowed AD-7.5 applies: the carve-out protects a voice inside the text, never the author speaking about their own text. + +AD-10.5 (carve-out — informative «честный»). «Честная цена» meaning a price with no hidden fees, «честный вес», «честный тест» meaning one whose method is published — here «честный» names a checkable property of the object, not a virtue of the prose (compare AD-7.3). + +AD-10.6 (single-count with AD-7). One fragment, one charge. AD-10 takes it when honesty is predicated of the text or frames what follows; AD-7 takes it when the qualifier modifies a single statement in the flow. Never both. + +**Examples:** + +| Wrong | Correct | +|---|---| +| Честный разбор: почему проекты срываются | Почему проекты срываются | +| Давайте будем честны — планировать умеет не каждая команда. | Из двенадцати команд в срок уложились три. | +| Рассказываю как есть, без прикрас. | (delete; then tell it) | +| Вся правда о подписках. | Что входит в подписку и что нет. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| — Скажу честно, я не знаю, — ответил инженер. | Speech of a person inside the text (AD-10.4). | +| Честная цена: 4 900 ₽, доставка входит. | «Честный» names a checkable property of the object (AD-10.5). | +| Мы ошиблись в расчёте на 30% и потеряли квартал. | Honesty demonstrated rather than announced (AD-10.3). | +| Строго говоря, это аппроксимация. | Epistemic qualifier carrying information (cf. AD-7.4). | + +**Severity:** Medium — the label form is the most conspicuous member of the declared-sincerity family and the one a reader notices first. Primary signal in the **Ч — Clarity** dimension (supporting **Ц — Reader precision**), reflected in the Ч rubric anchors. A label in a heading, or two instances in one text, materially lowers Ч; a single one in the body does not. Still cannot, by itself, trigger a non-compensatory cap. + +**Acknowledged:** identified in the 2026 neuroslop review, from a reader-complaint channel and from a probe the author supplied — «скажу честно». That exact form was in no trigger list; its close relative «честно говоря» was, among AD-7's, and the register carve-out then exempted it. So the family was named and the coverage was not, which is the same failure the probe exposed. + + +--- + +## AD-11. Mandatory tricolon (обязательная триада) + +**Problem:** enumerations that arrive in threes because three is the shape, not because the subject has three parts — «инновационный, трансформирующий, прорывной». The rule of three is a real device with a long rhetorical history; the tell is its automatic application. The giveaways are that the third member rewords the first two, that the members are of unequal rank, and above all that the pattern repeats: a text where every list is three items long is being written by a form, not about a subject. + +**Sources:** +- Expressive syntax and the cost of a figure applied mechanically: editorial practice, informed by Розенталь «Справочник по правописанию и стилистике» +- `info-style.md` §B — the unproven-claim adjectives that most often fill the three slots +- AD-7 already names the self-praise tricolon about one's own product; AD-11 is the general case and the two are kept apart by AD-11.5 +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger constructions:** + +- three adjectives in a row, none of them measurable — «инновационный, трансформирующий, прорывной» +- three noun phrases where the third rewords the first two +- every heading in a section followed by exactly three bullets +- «во-первых… во-вторых… в-третьих» where the third point restates the first +- the strongest signal: two or more forced triples in one text + +**Rules:** + +AD-11.1. Flag a triple when all three hold: (1) the subject does not have exactly three parts — a fourth could be added, or one dropped, with nothing lost; (2) the members are not distinct — one repeats or contains another; (3) deleting a member loses no fact. Density is part of the test, not a separate remark: a cluster of two or more such triples in one text is the primary signal, and an isolated triple is flagged only when every member is an unproven claim from `info-style.md` §B. + +AD-11.2. Rewrite by keeping what carries information: two members with distinct content, or one member with a number behind it. + +AD-11.3 (density raises severity; it never replaces the findings). Report **each** forced triple with its own fragment and let the cluster raise the weight in **С — Structure**. See «Two rules that govern all the others» for why. + +AD-11.4 (carve-out — the subject has three). An exhaustive enumeration — «рожковые, капсульные и автоматические» — three real steps, three measurements, three named parties: not flagged. This is AD-11.1's first test decided the other way: no fourth member could exist, so the three belong to the subject rather than to the form. + +AD-11.5 (single-count with AD-7). A triple that praises the author's own product or delivery — «быстро, надёжно, красиво» — is AD-7's self-praise tricolon and is charged there, in **Ч — Clarity**. AD-11 takes the forced triple that is not self-praise, in **С — Structure**. Never both for one fragment. + +AD-11.6 (single-count with info-style §B — the SAME fragment only). Where the members of the triple are §B unproven adjectives, count that fragment once: §B when the defect is the missing evidence, AD-11 when it is the forced count. Never both **for that fragment**. An unproven adjective elsewhere in the sentence — «хороший процесс должен быть прозрачным, предсказуемым и управляемым», where «хороший» is the subject and not a member — is a separate defect and is reported separately. Single-count means one charge per fragment, never one charge per sentence. + +AD-11.7 (carve-out — oratorical and literary register). The tricolon is a legitimate figure in speeches, manifestos and literary prose, where rhythm is part of the work (cf. AD-2.3, which likewise spares the literary register). + +**Examples:** + +| Wrong | Correct | +|---|---| +| Инструмент инновационный, трансформирующий и прорывной. | Инструмент собирает отчёт за 4 с вместо 3 мин. | +| Процесс должен быть прозрачным, предсказуемым и управляемым. | Процесс должен быть предсказуемым: срок известен на старте. | +| Он меняет подход к планированию, к оценке и к контролю. | Он меняет оценку сроков. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| Кофемашины бывают рожковые, капсульные и автоматические. | Exhaustive enumeration: no fourth type exists (AD-11.4). | +| Задержку определяют три вещи: расстояние, загрузка канала и версия протокола. | Three distinct mechanisms, each named (AD-11.4, cf. AD-12.3). | +| Сначала открываем конфиг, затем меняем порт, затем перезапускаем сервис. | Three real steps of a procedure. | +| Мы измеряли в Москве, Франкфурте и Сингапуре. | Three named places, each carrying its own datum. | + +**Severity:** Low. Secondary signal in the **С — Structure** dimension (supporting **Ч — Clarity**). A cluster lowers Structure; an isolated triple does not. Cannot trigger non-compensatory caps alone. + +**Acknowledged:** identified in the 2026 neuroslop review, from a reader-complaint channel where the forced three was named repeatedly and independently of any single model family. + + +--- + +## AD-12. Hollowed mechanism (выхолощенность) + +**Problem:** a claim that something matters, with the operative term replaced by a placeholder — «зависит от различных факторов», «есть целый ряд причин», «имеет свои особенности», «определённая специфика». The sentence has the shape of an explanation and none of its content: the reader cannot act on it, check it, or disagree with it, because nothing has been said. It is the most economical way for a paragraph to pass for informative. Not to be confused with AD-3: AD-3 says the same thing twice, AD-12 says nothing once. + +**Sources:** +- `info-style.md` §E — a claim without a number or a source is an opinion; §B lists «определённый» among the stop-words with the instruction to name the specific thing +- `info-style.md` §D — the worked contrast between «различные проблемы» and a problem named +- `editorial-grammar.md` §E.2 — «следует учитывать следующие факторы» is catalogued there as a tautology (the cognate pair «следует… следующие»): the neighbouring defect is the wording, the defect here is the emptiness +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger constructions:** + +- «различные факторы» / «ряд факторов» / «целый ряд причин» / «по ряду причин» +- «свои особенности» / «своя специфика» / «определённая специфика» / «известные нюансы» +- «многие аспекты» / «те или иные» / «в той или иной степени» / «разный эффект» +- «зависит от многого» / «всё индивидуально» / «зависит от ситуации» as a whole answer +- «играет важную роль» / «имеет большое значение» where neither the role nor the significance is named (cf. AD-13) + +**Rules:** + +AD-12.1. Flag a clause when all three hold: (1) it asserts influence, dependence, importance or difficulty; (2) the term carrying the mechanism is a placeholder rather than a name — «факторы», «аспекты», «особенности», «специфика», «моменты», «нюансы» are the common ones, and so is any bare quantifier or degree word standing in for the mechanism: «многое», «те или иные», «в той или иной степени», «разный», «большое значение». The list is open; the test is whether the word tells the reader WHICH; (3) nothing in the piece names what the placeholder stands for — not this sentence, not its neighbours, and not a section, table or source the sentence points to (AD-12.3). + +AD-12.2. Rewrite by naming: replace the placeholder with the two or three actual items. If they cannot be named, the sentence was not knowledge, and deleting it loses nothing. + +AD-12.3 (carve-out — the placeholder is cashed out, here or elsewhere). «Задержку определяют три вещи: расстояние до дата-центра, загрузка канала и версия протокола» — the placeholder is a colon away from its content. It counts just as much when the cash-out is deferred by an explicit pointer: «объясняется рядом факторов, разобранных в разделе 4», a reference to a table, an appendix, or a named source. That is the normal convention of technical, normative and academic writing, and the window is the whole document — the same reach AD-13.3 gives an attribution named once and referred to later. The defect is the placeholder that stays empty, not the one answered on another page. + +AD-12.4 (carve-out — honest uncertainty). A stated limit of knowledge that says what is unknown and why — «мы не знаем, откуда разброс: замеры шли на разном железе» — is information. The target is a pretence of an answer, never the admission that there is none. + +AD-12.5 (single-count with info-style §B). «Определённый» is already a §B stop-word. Charge the fragment once: §B when the defect is the empty determiner, AD-12 when it is the unnamed mechanism. Never both. + +AD-12.6 (single-count with AD-13). A clause can carry both defects in two different fragments — «исследования показывают, что влияют различные факторы» is AD-13 on the attribution and AD-12 on the mechanism, which is two fragments and two charges. One fragment is never charged to both. + +**Examples:** + +| Wrong | Correct | +|---|---| +| Результат зависит от различных факторов. | Результат зависит от версии протокола и от загрузки канала. | +| У каждого проекта своя специфика. | В этом проекте два подрядчика и общий репозиторий. | +| Многие аспекты процесса изучены недостаточно. | Мы не измеряли, как влияет размер команды. | +| Мотивация имеет большое значение. | Команды с недельным циклом обратной связи закрывали задачи на 20% быстрее. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| Задержку определяют три вещи: расстояние, загрузка канала и версия протокола. | The placeholder is cashed out in the same sentence (AD-12.3). | +| Мы не знаем, откуда разброс: замеры шли на разном железе. | Honest uncertainty, with the reason named (AD-12.4). | +| Специфика ГОСТ 8.417 в том, что единицы пишутся без точки. | «Специфика» followed by the specific thing. | + +**Severity:** Medium — a text made of these sentences is indistinguishable in shape from an informative one, which is exactly what makes it expensive. Primary signal in the **Ц — Reader precision** dimension (supporting **Ч — Clarity**), reflected in the Ц rubric anchors. Two or more hollowed clauses land Ц in the 5–6 band or lower. Cannot trigger a non-compensatory cap alone. + +**Acknowledged:** identified in the 2026 neuroslop review from a reader-complaint channel; checked against the corpus before proposing, where the pattern was found absent — the nearest neighbours are a tautology entry about the wording and a worked example about a different point. + + +--- + +## AD-13. Phantom attribution (неопределённая атрибуция) + +**Problem:** the gesture of citing, without a source — «исследования показывают», «эксперты отмечают», «учёные доказали», «по мнению аналитиков». The sentence borrows the authority of a body of work that is never identified, so the reader cannot go and look. This is worse than an unsupported claim, not better: a bare claim invites the question «откуда это?», while a dressed one discourages it. Distinct from the unproven-claim adjectives of `info-style.md` §B — there no source is offered at all, here a source is imitated. + +**Sources:** +- `info-style.md` §E — a number or claim without a source is an opinion, not a fact +- Adjacent to AD-10: both perform an authority instead of holding one +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger constructions:** + +- «исследования показывают» / «согласно исследованиям» / «учёные доказали» +- «эксперты отмечают» / «специалисты рекомендуют» / «аналитики прогнозируют» +- «принято считать» / «общеизвестно, что» / «как показывает практика» / «статистика говорит» +- «играет ключевую роль» — a rank assigned with nobody ranking and no criterion given. Where the defect is the missing mechanism rather than the missing source, AD-12 is the home; charge the fragment once +- the strongest signal: an attribution that no sentence in the piece ever redeems + +**Rules:** + +AD-13.1. Flag an attribution when the source is not identifiable anywhere in the text: no author, organisation, publication, date or link, here or elsewhere in the piece. + +AD-13.2. Rewrite by naming the source, or by dropping the attribution and owning the claim. «Исследования показывают, что удалёнка снижает продуктивность» → «По данным опроса N (2025, 1 200 респондентов), …» or «На наших четырёх проектах продуктивность упала». + +AD-13.3 (carve-out — a real source). Named, dated and checkable — «По данным Росстата за 2025 год» — is not flagged, and neither is a later reference to a source the piece named once. + +AD-13.4 (carve-out — consensus with a citation). Where the existence of a consensus is itself the point and a citation follows, «принято считать» is a legitimate framing move rather than a phantom. + +AD-13.5 (carve-out — marked personal experience). «По моему опыту», «у нас на проекте», «в трёх наших внедрениях» attribute to the author, who is identifiable and answerable. That is a source. + +AD-13.6 (single-count with info-style §B). An unproven adjective and a phantom attribution in one fragment are one charge: §B when the defect is the unevidenced claim, AD-13 when it is the imitated citation. Never both. + +**Examples:** + +| Wrong | Correct | +|---|---| +| Исследования показывают, что удалёнка снижает продуктивность. | По данным опроса Института X (2025, 1 200 респондентов), продуктивность упала на 8%. | +| Эксперты отмечают: рынок вырастет. | ЦБ в июльском прогнозе ждёт роста на 3%. | +| Учёные доказали, что кофе полезен. | (delete, or cite the study) | +| Мотивация играет ключевую роль. | Без обратной связи в первую неделю уходит каждый третий новичок. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| По данным Росстата за 2025 год, зарплата выросла на 12%. | Named, dated, checkable (AD-13.3). | +| По моему опыту, миграции такого размера занимают неделю. | Attributed to the author, who is answerable (AD-13.5). | +| Принято считать, что индексы ускоряют чтение; замер ниже это опровергает. | Consensus named in order to be tested, and it is (AD-13.4). | + +**Severity:** Medium — an imitated citation does more damage than a missing one, because it suppresses the reader's question. Primary signal in the **Ц — Reader precision** dimension (supporting **Ч — Clarity**), reflected in the Ц rubric anchors. Cannot trigger a non-compensatory cap alone. + +**Acknowledged:** identified in the 2026 neuroslop review from a reader-complaint channel; checked against the corpus before proposing, where the pattern had no entry of any kind. + + +--- + +## AD-14. Chat transcript as the artifact (диалог с нейросетью вместо текста) + +**Problem:** the published piece is the log of a conversation with a model. Its skeleton is a sequence of turns rather than an argument: questions become headings, answers become sections, and the piece ends when the conversation did. The reader is handed the raw material and asked to do the author's work on it. This is one of the set's two rules charged to the **document** rather than to a fragment — the other is AD-15 — and no local edit removes either, because the defect is the shape. + +**Sources:** +- Platform editorial policy names this artifact directly: Habr's site rules as they stood on 27.07.2026 (the version announced 17.06.2026) list, among material that should not be posted, pieces that are the author's dialogues with neural networks and pieces consisting only of model answers; a separate section of the same rules restricts unfinished drafts and streams of consciousness (paraphrased, not quoted) +- Distinct from AD-8, which flags an assistant reply leaking into monologue prose: that is a sentence, this is a skeleton (see AD-14.5) +- `info-style.md` §D — the inverted pyramid a transcript cannot have, because a conversation is ordered by turns +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger constructions:** + +- alternating speaker labels — «Я:» / «Модель:», «Пользователь:» / «Ответ:», «Промпт:» / «Результат:» +- the piece's headings are the questions that were asked +- prompt echo: each answer restates the question before answering it +- the closing is the end of the exchange — «Спасибо, теперь понятно» — rather than a conclusion +- no sentence in the piece is a claim of the author's own + +**Rules:** + +AD-14.1. Charge the document when both hold: (1) its structure is a sequence of exchanges rather than an exposition; (2) the author contributes no claim of their own — no thesis, no synthesis, no assessment of what the model said. + +AD-14.2. Rewrite by writing the piece: state what you learned, in the order a reader needs it, and keep from the transcript only what a reader must see in order to check you. + +AD-14.3 (level — the document charge ADDS, it never replaces). AD-14 is a property of the whole document: attach it once, not to an individual line, and a single quoted exchange inside an ordinary article is not this rule at all. The charge adds one finding and replaces none: every defect inside the transcript is reported as it would be otherwise (see «Two rules that govern all the others»). + +AD-14.4 (carve-out — the transcript as evidence). A log published as evidence for the author's own finding, with the finding stated and the log subordinate to it, is not flagged: a prompt-engineering write-up, a documented experiment, a bug report, a reproduction. The test is one deletion: remove the log — does a claim of the author's remain? If yes, the log was evidence. + +AD-14.5 (relationship to AD-8 — not a double charge). AD-8 flags individual assistant replies inside a text; AD-14 flags the text's shape. Repair every AD-8 flourish and an AD-14 document is still a transcript; repair the shape and the flourishes were never the problem. The two defects survive each other's repair, so both are charged — this is not one fragment counted twice. + +AD-14.6 (carve-out — human dialogue). An interview, a Q&A with a human respondent, a play, a chat-support excerpt, a quoted exchange: dialogue between people is a genre, not a tell (cf. AD-8.4). + +AD-14.7 (carve-out — the transcript was what was asked for). Where the user's brief is a transcript — documentation of a session, a teaching example showing prompts and outputs — the style priority stated in `SKILL.md` applies and these defaults do not override the request. + +**Examples:** + +| Wrong | Correct | +|---|---| +| **Я:** Объясни кеширование. **Модель:** Отличный вопрос! … | ETag сравнивает версию ресурса; при совпадении сервер отвечает 304. (then the author's own account) | +| Заголовок раздела: «А что такое ETag?» | Заголовок раздела: «ETag: когда сервер отвечает 304» | +| Закрытие: «Спасибо, теперь понятно.» | Закрытие: вывод, ради которого текст написан. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| Ниже — расшифровка: я задал модели один вопрос трижды и свёл ответы в таблицу. Разброс — вывод статьи. | The log is evidence for the author's own finding (AD-14.4). | +| «Хороший вопрос», — ответил инженер. | Quoted human dialogue (AD-14.6, cf. AD-8.4). | +| Интервью с разработчиком, вопрос — ответ. | An interview is a genre. | + +**Severity:** High — the highest in this addenda set, and it means something specific: the defect is not a fragment to repair but the form of the piece, so no sequence of local edits removes it. Primary signal in the **С — Structure** dimension (supporting **Ц — Reader precision**), reflected in the С rubric anchors: a document charged with AD-14 does not reach the upper Structure bands. It does **not** introduce a new non-compensatory cap — those stay reserved for the hard dimensions and the global floor (see `scoring.md`). Severity here says how far the dimension moves, not that a new cap exists. + +**Acknowledged:** identified in the 2026 neuroslop review from the editorial-policy channel, where a Russian platform names the artifact in its own rules rather than in commentary. + + +--- + +## AD-15. Search-engine addressee (адресат — поисковик, а не читатель) + +**Problem:** the implied reader is a crawler. Nothing in the text is ungrammatical and no single sentence is wrong; the piece is simply not addressed to a person. The query phrase is repeated verbatim where a language addressed to a reader would use a pronoun; sections are variants of one question rather than steps of an argument; and after reading, the reader still cannot do the thing the piece is about. Two platforms formulate the test the same way, and both formulate it as a question of **addressee** rather than of provenance. + +**Sources:** +- Habr's site rules as they stood on 27.07.2026 (the version announced 17.06.2026) name material aimed at search engines rather than living readers among the content it restricts (paraphrased, not quoted) +- Яндекс Вебмастер lists automatically generated *and* useless-to-the-user content among the examples its algorithms may restrict; the conjunction is operative — machine origin alone is not the stated violation (paraphrased, not quoted) +- `info-style.md` §A (полезное действие) and §D (main point first) — the two properties such a text lacks +- `scoring.md` notes that formula tools score an SEO text highly without any real quality; that is the observation, and this rule is the judgement it calls for +- **The sentence-level signature below is this corpus's own formulation.** Neither platform states what the tell looks like inside a sentence; only the criterion of addressee is theirs + +**Trigger constructions:** + +- the exact query phrase repeated verbatim in the title, in a heading, and again in the body +- headings that are variants of one another rather than steps — «как выбрать X», «какие бывают X», «сколько стоит X» +- the subject named by its full noun phrase where a pronoun or a short form would serve +- a «выводы» section that restates the headings and adds no number +- length without a fact: the piece answers a query and leaves the reader unable to act + +**Rules:** + +AD-15.1. Charge the document when all three hold: (1) a query phrase is repeated where the language would use a pronoun or a short form; (2) sections are variants of one question rather than steps of an argument; (3) after reading, a reader cannot do the thing the piece is about — no number, no criterion, no named option. + +AD-15.2. Rewrite by answering the question once and with specifics: name the options and the criterion by which one is chosen, with prices where they decide it. + +AD-15.3 (carve-out — the user's explicit brief). Where the user asks for SEO copy, the style priority stated in `SKILL.md` applies: their request overrides these defaults. State what the trade-off costs and write what was asked. + +AD-15.4 (carve-out — functional repetition). Technical writing repeats an identifier because a pronoun would be ambiguous — «поле `updated_at` меняется при каждой записи, поле `created_at` — нет». Reference works, glossaries and legal texts repeat the term by design. The defect is the addressee, not the repetition. + +AD-15.5 (carve-out — optimisation that still serves the reader). Keyword research legitimately shapes what a useful text covers. A piece that answers the query with facts is not flagged for having the query in its title. + +AD-15.6 (level — the document charge ADDS, it never replaces). A note on what the repetition is and is not: `editorial-grammar.md` §E defines tautology as ADJACENT cognates and pleonasm as words duplicating each other's meaning, so a query phrase repeated across four paragraphs is neither, and the corpus has no separate rule for distant lexical repetition. That repetition is AD-15's own evidence and belongs to AD-15 alone — do not borrow a Г finding the corpus does not carry. Like AD-14, this rule is charged to the document, and the repetitions are the evidence for that one charge rather than one AD-15 finding each. That is the **only** thing it absorbs. Every other defect in the same text is reported exactly as it would be without this rule: the stop-words and unproven claims in **Ч**, any pleonasm or tautology the corpus already catalogues in **Г** (`editorial-grammar.md` §E), the passive constructions, the missing полезное действие, the headings that are not theses. A summary that costs the writer the list of lines to fix is not a summary (see «Two rules that govern all the others»). + +**Examples:** + +| Wrong | Correct | +|---|---| +| Купить кофемашину в Москве: как выбрать кофемашину для дома | Как выбрать кофемашину для дома | +| Цена кофемашины зависит от типа кофемашины и производителя кофемашины. | Цена зависит от типа и производителя: рожковые — от 12 000 ₽, автоматические — от 40 000 ₽. | +| Раздел «Как выбрать кофемашину для дома: выводы», пересказывающий предыдущие заголовки | Раздел с рекомендацией: какая модель кому подходит и почему | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| Поле `updated_at` меняется при каждой записи, поле `created_at` — нет. | Repetition because a pronoun would be ambiguous (AD-15.4). | +| Заголовок «Как выбрать кофемашину» над текстом с ценами и критериями | The query is answered with facts (AD-15.5). | +| Глоссарий, где термин повторён в каждой статье | Reference format (AD-15.4). | + +**Severity:** High — like AD-14, the defect is the piece rather than a phrase in it, and the reader's loss is total: the text is about their question and gives them nothing. Primary signal in the **Ц — Reader precision** dimension (supporting **С — Structure**), reflected in the Ц rubric anchors: a document charged with AD-15 does not reach the upper Ц bands. It does **not** introduce a new non-compensatory cap (see `scoring.md`). + +**Acknowledged:** identified in the 2026 neuroslop review from the editorial-policy channel, where two platforms independently formulated the same test of implied addressee. + + +--- + +## AD-16. Additive pseudo-pair («не только X, но и Y») + +**Problem:** a pair joined by «не только… но и», «как… так и», «и… и», in which the second pole adds nothing — it rewords the first, or is contained in it, or both are empty. The construction promises an increment and delivers a rhythm. It is the neighbour of AD-6 and not the same rule: there an opposition with no antecedent, here an addition with no content. + +**Sources:** +- `editorial-grammar.md` — the corpus's only «не только… но и» entry is a predicate-agreement rule, a different subject entirely; the two do not collide +- Informed by a 2025 linguo-pragmatic study of AI-assisted Russian social-media posts (cf. Оломская, Юрова; CyberLeninka), which counts the construction among the clichés it treats as markers of generated text. **The density threshold is this corpus's own caution, not the study's finding:** the study measured 38 posts of one political-SMM channel, a genre that reaches for the construction anyway, so the frequency it reports cannot carry a per-sentence rule and the threshold keeps an isolated pair out of the flag +- Distinct from AD-6 (see AD-16.5) +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger constructions:** + +- «не только X, но и Y» where Y rewords X or is contained in it +- «как X, так и Y» with the same defect +- «и X, и Y» with synonymous poles +- a cluster of two or more such pairs in a short text — density is the signal + +**Rules:** + +AD-16.1. Flag a pair when all three hold: (1) **no increment** — Y states nothing X did not; (2) **not a contrast** — this is addition, so it is not AD-6 and must not be charged there; (3) **deletable** — drop «не только» and one pole, and no fact is lost. Density belongs to the test: two or more such pairs in one text is the signal, and an isolated pair is not flagged. + +AD-16.2. Rewrite as the single claim that survives, and give it something to stand on: «не только удобный, но и комфортный» → name what the interface does, in one step, with a number if there is one. + +AD-16.3 (carve-out — a real pair). «Не только в Москве, но и в Новосибирске: 12 мс и 47 мс» — AD-16.1's increment test decided the other way: each pole carries its own number, and deleting either loses it. + +AD-16.4 (carve-out — scope). «Не только для физлиц, но и для ИП» defines who is covered. Scope-widening is operative content, frequently legal, and is never flagged. + +AD-16.5 (single-count with AD-6). A fragment is charged once. AD-6 takes a symmetric opposition whose negated pole has no antecedent; AD-16 takes an addition whose second pole has no content. If a construction reads as both, it is almost always AD-6 — check for a rejected pole before charging here. + +AD-16.6 (single-count with info-style §B — the SAME fragment only). Where both poles are §B unproven adjectives, count that fragment once: §B for the missing evidence, AD-16 for the empty pair. An unproven adjective outside the pair stays its own finding; single-count is per fragment, never per sentence. + +AD-16.7 (density raises severity; it never replaces the findings). Report **each** pair with its own fragment and let the count raise the weight. See «Two rules that govern all the others». + +AD-16.8 (the pair and the synonymy are two defects, not one). «Не только удобный, но и комфортный» carries both: the poles are synonyms, which is a pleonasm and belongs where pleonasms have always belonged — `editorial-grammar.md` §E, in **Г — Grammar** — and the frame adds nothing, which is AD-16, in **С — Structure**. They are separable, and that is the test: «Сервис удобный и комфортный» is the pleonasm with no pair; «не только быстрее, но и дешевле» with two real numbers behind it is neither. Where both hold, report both. In a measured run, folding the pleonasm into AD-16 scored the same text **higher than before the rule against it existed** — Grammar recovered its points because the finding had moved to a Low signal in another dimension. A new rule against a text must never make that text score better. + +**Examples:** + +| Wrong | Correct | +|---|---| +| Сервис не только удобный, но и комфортный. | Сервис открывает отчёт в два клика вместо семи. | +| Платформа помогает не только экономить время, но и тратить его меньше. | Платформа сокращает сборку отчёта с 3 мин до 4 с. | +| Мы предлагаем как качественные решения, так и решения высокого уровня. | Мы делаем интеграции с 1С и с SAP. | +| Обновление затрагивает не только интерфейс, но и внешний вид. | Обновление меняет интерфейс. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| Мы измеряли не только в Москве, но и в Новосибирске: 12 мс и 47 мс. | Both poles carry data (AD-16.3). | +| Скидка действует не только для физлиц, но и для ИП. | Scope-widening: operative content (AD-16.4). | +| Не только студенты, но и преподаватель присутствовал. | The corpus's agreement example: a different rule about a different thing. | + +**Severity:** Low. Secondary signal in the **С — Structure** dimension (supporting **Ч — Clarity**). Density is the signal; an isolated pair is not flagged at all. Cannot trigger non-compensatory caps alone. + +**Acknowledged:** identified in the 2026 neuroslop review from the linguistic channel; checked against the corpus before proposing, where the construction appears exactly once and as a rule about predicate agreement. + +## AD-17. Comma welded to a dash (запятая, сомкнутая с тире) + +**Problem:** a clause closes with a comma and the next thought opens with an em dash, so the two +marks end up side by side inside one sentence: «перечисление, у которого правда три элемента, — +выписано в самом правиле». Each mark is correct on its own. Together they are a junction a +person almost never reaches, because a person restructures the sentence first. A model does not +restructure — it applies both rules and prints both marks. The tell is not an error of grammar; +it is the fingerprint of rule-following without an ear. + +**Sources:** +- Rozental, §64 «Запятая и тире»: «При „встрече“ внутри предложения запятой и тире сначала + ставится запятая, а затем тире». **The norm permits the junction** — this rule does not + contradict the reference, it observes that living prose avoids what the norm allows +- Checked and NOT found: Lebedev's Ководство §143 «Знаки препинания в нестандартных ситуациях» + does not discuss combining marks at all, and no statement on the subject was found from + Ilyakhov. **No source forbids the construction, and this rule does not claim one does** +- `editorial-punctuation.md` — the corpus has no rule producing the junction: it emerges from + two correct rules applied together, which is why nothing caught it +- Independently formulated from the 2026 review of AI-generated Russian prose + +**Trigger construction:** a comma immediately followed by an em dash, `, —`, inside a sentence, +where the comma closes a subordinate clause, a participial phrase or an aside, and the dash +opens a predicate, an aside or a summary. + +**What to do:** remove the need for one of the marks. Split the sentence, or reorder it so the +junction never forms. Do not break the norm by deleting the comma and keeping the dash — that +produces an error where there was only an infelicity. + +AD-17.1 (the trigger is a JUNCTION, not the pair of characters). Two marks that each belong to a +different construction are not this rule. Direct speech is the case that matters: in «„Хороший +вопрос“, — ответил инженер» the comma closes the quoted reply and the dash introduces the +author's words. That is the punctuation of direct speech, prescribed and universal, and it is +**never** flagged. Every occurrence of `, —` in this corpus outside this rule's own examples is of that kind — direct speech, in AD-7.5 and AD-10.4 and their tables. + +AD-17.2 (carve-out — homogeneous subordinate clauses before a main clause). Rozental §40 and §46 +describe a construction where comma-plus-dash works as a single mark: a run of homogeneous +subordinate clauses closing before the main clause. «Кто виноват, кто прав, — судить не нам». +Not flagged. + +AD-17.3 (carve-out — a book, or a text edited as one). The director's scope: the tell is about +the register of everyday professional writing. In literary prose, in a printed book, or when the +task is explicitly the copy-editing of one, the junction is a legitimate authorial rhythm and is +left alone. Where the genre is unclear, ask rather than flag. + +AD-17.4 (carve-out — quotation). A junction inside quoted material stays. See «Someone else's +words stay theirs» in `SKILL.md`: an issue in a third party's text may be reported, never edited. + +AD-17.5 (single-count with AD-1). A sentence can carry both: AD-1 counts em dashes per paragraph, +AD-17 takes one junction. Where a paragraph is over the dash limit **and** holds a junction, +report AD-1 for the density and AD-17 for the junction — the fixes differ, and removing the +junction usually removes one dash, which is why the order matters: fix AD-17 first, then re-count +for AD-1. + +**Examples:** + +| Wrong | Correct | +|---|---| +| Перечисление, у которого три элемента, — выписано в самом правиле. | Перечисление из трёх элементов выписано в самом правиле. | +| Команда наполняет каталоги, только если они уже существуют, — сама она их не создаёт. | Команда наполняет каталоги, только если они уже существуют. Сама она их не создаёт. | +| Отчёт, собранный за ночь, — на столе у заказчика. | Отчёт, собранный за ночь, лежит на столе у заказчика. | + +**Counter-examples (do NOT flag):** + +| Acceptable | Reason | +|---|---| +| «Хороший вопрос», — ответил инженер. | Direct speech: two constructions, not a junction (AD-17.1). | +| — Скажу честно, я не знаю, — ответил инженер. | Same, with a dialogue dash (AD-17.1). | +| Кто виноват, кто прав, — судить не нам. | Homogeneous subordinate clauses before the main clause (AD-17.2). | +| A junction inside a passage being copy-edited as a book. | Literary register (AD-17.3). | + +**Severity:** Low. Secondary signal in the **С — Structure** dimension. A single junction is +worth a remark and no more; the signal is density, and the fix is a rewrite the author would +have made anyway. Cannot trigger non-compensatory caps alone. + +**Acknowledged:** raised by the director, 29.07.2026, on reading a junction in this project's own +README: normative, and still a tell — «люди так не пишут в живой жизни, даже профессионалы +языка». The research behind it, including what the named sources do **not** say, is recorded in +`~/.claude/plans/ru-text-v2/comma-dash-research.md`. diff --git a/skills/ru-text/references/editorial-grammar.md b/skills/ru-text/references/editorial-grammar.md index 5216e21..969f02e 100644 --- a/skills/ru-text/references/editorial-grammar.md +++ b/skills/ru-text/references/editorial-grammar.md @@ -7,6 +7,9 @@ - [F. List homogeneity](#f-list-homogeneity) - [G. Numbers in text](#g-numbers-in-text) - [H. Clean language principles (inspired by the anti-bureaucratic tradition, cf. N. Gal, M. Ilyakhov)](#h-clean-language-principles-inspired-by-the-anti-bureaucratic-tradition-cf-n-gal-m-ilyakhov) +- [I. Government (управление)](#i-government-управление) +- [J. Gerund phrases (деепричастные обороты)](#j-gerund-phrases-деепричастные-обороты) +- [K. Contextual homophones](#k-contextual-homophones) - [Sources](#sources) ## C. Capitalization @@ -275,6 +278,17 @@ Do not mix words and digits for same-type values within one sentence/paragraph. ### H.2. Dead constructions -> live replacements +Six of the entries below also stand in the stop-word catalogue of `info-style.md` §B — «в +настоящее время», «вышеуказанный», «на сегодняшний день», «надлежащий», «нижеследующий», +«принять решение». §B is their canonical home: it is the list a checker walks, and it is +the block frozen by the corpus contract. Count such a fragment once, in **Ч — Clarity**, +and do not report it twice because it appears in two files. + +The other thirty-six are here and nowhere else. Measured on 28.07.2026 with the same parser +`check-frozen.sh` uses: 42 entries here, 92 in §B, 6 in both. A plan step that would have +replaced this section with a pointer to §B was written on the assumption that it mostly +duplicated the catalogue; it does not, and following it would have deleted thirty-six rules. + dead|live произвести ремонт|отремонтировать осуществить доставку|доставить @@ -343,5 +357,136 @@ dead|live 6. Use simple words: "сделать" not "реализовать"; "начать" not "приступить к осуществлению" 7. Read aloud: if you stumble -- rewrite the sentence +## I. Government (управление) + +Which case a preposition or a verb requires. §D covers agreement — subject with predicate, +numeral with noun; this covers the other half, and until v2.0 the corpus had no rule for it +at all. + +**Closed list, deliberately.** The open form of this check — verify the case after every +preposition and every verb — is a detector of non-native Russian, and building one is +forbidden here: it would punish an accent rather than an error. What follows is a list of +specific pairs whose correct form is not in dispute. A construction that is not on the list +is not flagged by this section, however wrong it may look. + +### I.1. Prepositions and verbs that take a fixed case + +wrong|right|why +согласно приказа|согласно приказу|«согласно» takes the dative +благодаря поддержки|благодаря поддержке|dative +вопреки ожиданий|вопреки ожиданиям|dative +навстречу гостей|навстречу гостям|dative +оплатить за проезд|оплатить проезд|«оплатить» takes the accusative without a preposition +заплатить проезд|заплатить за проезд|paying FOR something is «заплатить за»; «заплатить долг» (repay) is also correct, so only this sense is listed +заведующий отдела|заведующий отделом|instrumental +командующий армии|командующий армией|instrumental +преимущество над конкурентами|преимущество перед конкурентами|«перед», not «над» +уверенность в победу|уверенность в победе|prepositional after «в» +вера в победе|вера в победу|accusative after «в» — the mirror of the line above +по приезду|по приезде|«по» in the sense of «after» takes the prepositional +по окончанию школы|по окончании школы|same +удивляться результату|удивляться результату|correct as written — listed so it is not «corrected» to the genitive +рад встрече|рад встрече|correct — dative after «рад», not genitive + +I.1.1. Flag only a pair from this list. The list is the rule; there is no general principle +behind it that a checker may extend by analogy. + +I.1.2. Some rows carry a CORRECT form, present so that a checker does not invent an error +where none exists; the third column says which. Getting a right form flagged as wrong costs +more trust than a miss. + +I.1.3. A pair whose norm is variable does not belong here, however common the error looks. +«Скучать по вам» and «скучать по вас» were listed until the gate checked them: the first is +the current norm and the row had them the wrong way round, and «превосходство перед» is +attested with a dated mark rather than wrong. A closed list of undisputed pairs cannot hold a +disputed one — the contradiction is with §I's own preamble, not with a source. + +I.1.4 (carve-out — whose grammar it is). Not flagged inside a quotation, a line of dialogue, +or an interviewee's speech: the error belongs to the speaker, and reporting it charges the +author for someone else's grammar. The same boundary AD-10.4 draws for declared sincerity, +stated here because a rule read on its own must decide on its own. It governs §I, §J and §K. + +### I.2. Different government under a shared dependent + +wrong|right +организовать и руководить работой|организовать работу и руководить ею +любить и заниматься спортом|любить спорт и заниматься им +надеяться и верить в успех|надеяться на успех и верить в него +контроль и управление процессом|контроль над процессом и управление им + +I.2.1. Two or more verbs (or verbal nouns) sharing one dependent, where they do not take the +same case. One of them silently gets the wrong form. + +I.2.2. Rewrite by giving the first verb its own object and replacing the second with a +pronoun; do not stack the cases. + +I.2.3. Not flagged when both words genuinely take the same case: «читать и перечитывать +книгу», «собрать и отправить отчёт». + +## J. Gerund phrases (деепричастные обороты) + +### J.1. The doer of the gerund must be the subject of the sentence + +wrong|right +Подъезжая к станции, у меня слетела шляпа.|Когда я подъезжал к станции, у меня слетела шляпа. +Прочитав письмо, ему стало ясно.|Прочитав письмо, он всё понял. +Открыв файл, была обнаружена ошибка.|Открыв файл, мы обнаружили ошибку. +Возвращаясь домой, начался дождь.|Когда я возвращался домой, начался дождь. + +J.1.1. The test is mechanical and needs no judgement, but it is about DOERS, not about +subjects: name who performs the gerund, and name who performs the main verb — or the +infinitive, participle or second gerund the phrase attaches to. The same person: correct. +Different: wrong. Stating it as «the grammatical subject» was an error of this corpus's own, +caught at the gate: a one-member definite-personal sentence has no grammatical subject at +all, and «Открыв файл, проверяем кодировку» is correct — the doer of both is «мы». + +J.1.2. Rewrite by making the doers agree, or by turning the gerund phrase into a subordinate +clause with «когда», «если», «после того как». + +J.1.3 (carve-out — the doer of an infinitive). «Подъезжая к станции, следует смотреть в окно», +«Готовя отчёт, нужно проверить цифры», «Просим клиентов, оплачивая счёт, указывать номер +договора». The condition is not «the clause is impersonal» — it is that the doer of the gerund +is the doer of the infinitive. That is why «Ознакомившись с резюме, кандидату было предложено +пройти тест» stays wrong: the reader of the CV is not the one taking the test. + +J.1.4 (carve-out — the forms that became prepositions), and only where they really have. +«Судя по отчёту», «исходя из данных», «несмотря на возражения», «начиная с понедельника», +«включая расходы» — the test is whether the word can be dropped without loss: «приступить +начиная с понедельника» → «приступить с понедельника», so «начиная» is a preposition here. +Where the verbal meaning is alive it is a gerund again and J.1.1 applies: «Включая новые +расходы в смету, бухгалтер пересчитал бюджет» — somebody is doing the including. + +J.1.5 (carve-out — the doer is implied by the form). A one-member sentence names no subject +and needs none: the imperative «Открыв файл, проверьте кодировку», the inclusive «Открыв +файл, давайте проверим кодировку», and the definite-personal «Открыв файл, проверяем +кодировку» are all correct, because the verb ending says who acts. + +J.1.6 (carve-out — the ones that stopped being gerunds). «Молча», «нехотя», «стоя», «сидя», +«лёжа», «не спеша», «шутя» work as adverbs of manner: they take no phrase, need no comma, and +have no doer to compare. «Он читал молча» is not a gerund phrase and §J does not look at it. + +## K. Contextual homophones + +Pairs a spellchecker cannot decide, because both spellings exist and only the meaning tells +them apart. A closed list, for the same reason §I is one. + +wrong context|right|the deciding question +в течении года|в течение года|a stretch of time → «в течение» (preposition) +в течение реки|в течении реки|the noun «течение» in the prepositional: a current, and also the course of an illness, of time, or a movement in art or politics — «в течении болезни», «в течении споров» +что бы сделать|чтобы сделать|«чтобы» when «бы» cannot be moved or dropped +чтобы ни случилось|что бы ни случилось|two words when «бы» belongs to «что» +так же как вчера|так же как вчера|correct: comparison, two words +он также пришёл|он также пришёл|correct: «и он пришёл» → one word +в виду плохой погоды|ввиду плохой погоды|«ввиду» = because of. Separate «в виду» is not only the fixed phrase: it also means «within sight of» — «эскадра шла в виду берега», where «ввиду» would read as «because of the shore» +иметь ввиду|иметь в виду|the fixed phrase is three words +не смотря на дождь|несмотря на дождь|«несмотря на» = although; «не смотря» = not looking +вследствие ошибки|вследствие ошибки|correct: «из-за»; «в следствие» only about an investigation + +K.1. Decide by the question in the third column, not by frequency. Both forms of each pair +are correct somewhere, which is why a spellchecker passes them and a reader trips. + +K.2. Correct rows are listed alongside wrong ones on purpose. A checker that has only seen +the error learns to flag the pair on sight. + ## Sources See [sources.md](sources.md) diff --git a/skills/ru-text/references/editorial-punctuation.md b/skills/ru-text/references/editorial-punctuation.md index a1bdb0b..15023e9 100644 --- a/skills/ru-text/references/editorial-punctuation.md +++ b/skills/ru-text/references/editorial-punctuation.md @@ -13,6 +13,8 @@ Subordinate clause is separated by comma. Conjunctions: **что, который Base rule: comma before the conjunction starting the subordinate clause. +A restrictive or intensifying particle standing in front of the conjunction takes the comma with it: the boundary moves left, it does not disappear. `вызывай ru-text только когда я прошу` -> `вызывай ru-text, только когда я прошу`. Particles: только, лишь, исключительно, как раз, даже, ведь, всё же. (ПАС §116–117; Розенталь §33.6.) The mistake is dropping the comma altogether because the particle looks like part of the main clause — it is not, and the clause it introduces is still subordinate. + When comma before "что" is NOT needed: construction|comma?|rule diff --git a/skills/ru-text/references/scoring.md b/skills/ru-text/references/scoring.md index bc07b84..21b45a7 100644 --- a/skills/ru-text/references/scoring.md +++ b/skills/ru-text/references/scoring.md @@ -1,7 +1,7 @@ # Scoring: text quality assessment (0–10) Analytic rubric for evaluating Russian text quality across 5 dimensions. -Context-aware LLM evaluation against ~1 044 independently formulated rules (informed by 16 published sources; see sources.md). +Context-aware LLM evaluation against a corpus of over 2,000 independently formulated linguistic atoms (informed by 16 published sources; see sources.md). ## Table of Contents @@ -33,7 +33,7 @@ Context-aware LLM evaluation against ~1 044 independently formulated rules (in Formula-based tools (Glavred, Turgenev, Advego) count surface features: stop-words, sentence length, keyword density. Removing «очень» raises the score without improving the text. An SEO text scored 9.1 on Glavred without any real quality. -This rubric evaluates text contextually against ~1 044 rules. «Очень» in quoted direct speech is not penalized. «Инновационный» without evidence is penalized. Five orthogonal dimensions cannot be optimized simultaneously without genuinely improving the text. +This rubric evaluates text contextually against the whole corpus. «Очень» in quoted direct speech is not penalized. «Инновационный» without evidence is penalized. Five orthogonal dimensions cannot be optimized simultaneously without genuinely improving the text. The only way to raise the score is to write better. @@ -50,7 +50,7 @@ The only way to raise the score is to write better. ### T — Typography (weight 0.15) -Type: hard (objective). Rule source: [typography.md](typography.md) (96 rules). +Type: hard (objective). Rule source: [typography.md](typography.md). | Score | Criteria | |---|---| @@ -62,11 +62,11 @@ Type: hard (objective). Rule source: [typography.md](typography.md) (96 rules). ### Ч — Clarity (weight 0.25) -Type: soft (expert). Rule sources: [info-style.md](info-style.md) (197 rules), [anti-patterns.md](anti-patterns.md) (138 rules). Secondary signals (low weight): [addenda.md](addenda.md) AD-3 (patronizing explanation), AD-5 (subject-predicate mismatch, with technical-context exception), AD-6 (manufactured antithesis), AD-7 (preemptive virtue qualifier), AD-8 (assistant-register meta-commentary). +Type: soft (expert). Rule sources: [info-style.md](info-style.md), [anti-patterns.md](anti-patterns.md). Primary signal from [addenda.md](addenda.md): AD-10 (declared sincerity — **Medium**: honesty predicated of the text itself. A label in a heading, or two instances in one text, lands this dimension in the 5–6 band or lower; a single instance in the body does not). Secondary signals (low weight): AD-3 (patronizing explanation), AD-5 (subject-predicate mismatch, with technical-context exception), AD-6 (manufactured antithesis), AD-7 (preemptive virtue qualifier), AD-8 (assistant-register meta-commentary), AD-11 (mandatory tricolon — a supporting signal here; its home is С, and a triple that praises the author's own delivery goes to AD-7, never to both — AD-11.5). | Score | Criteria | |---|---| -| 9–10 | No stop-words, no bureaucratic language, no clichés. Active voice. Every word works. No false intensifiers or unsubstantiated claims | +| 9–10 | No stop-words, no bureaucratic language, no clichés. Active voice. Every word works. No false intensifiers or unsubstantiated claims. Honesty is demonstrated by what the text does with an inconvenient fact, never announced («честный разбор», «давайте будем честны») | | 7–8 | 1–3 minor stop-words or passive constructions. Overall tone is clean | | 5–6 | Noticeable stop-words (5+), several passive constructions, 1–2 clichés. Readable but loose | | 3–4 | Bureaucratic language dominates: deverbal nouns, genitive chains, split predicates | @@ -74,7 +74,7 @@ Type: soft (expert). Rule sources: [info-style.md](info-style.md) (197 rules), [ ### Г — Grammar (weight 0.20) -Type: hard (objective). Rule sources: [editorial-grammar.md](editorial-grammar.md) (171 rules), [editorial-punctuation.md](editorial-punctuation.md) (88 rules). +Type: hard (objective). Rule sources: [editorial-grammar.md](editorial-grammar.md) — including §I government, §J gerund phrases and §K contextual homophones — and [editorial-punctuation.md](editorial-punctuation.md). | Score | Criteria | |---|---| @@ -86,19 +86,19 @@ Type: hard (objective). Rule sources: [editorial-grammar.md](editorial-grammar.m ### С — Structure (weight 0.20) -Type: soft (expert). Rule sources: [info-style.md](info-style.md) (structure section), [addenda.md](addenda.md) — AD-1 (dash overuse) and AD-6 (manufactured antithesis — **Medium**: the strongest machine-generation tell. A cluster of two or more manufactured pairs within ~150 words lands this dimension in the 5–6 band or lower; a single antecedent-backed pair does not move it), plus secondary signals (low weight): AD-2 (excessive parcellation), AD-4 (unprovoked rebuttal), AD-7 (preemptive virtue qualifier), AD-9 (hollow opener). +Type: soft (expert). Rule sources: [info-style.md](info-style.md) (structure section), [addenda.md](addenda.md) — AD-1 (dash overuse); AD-6 (manufactured antithesis — **Medium**: the strongest machine-generation tell. A cluster of two or more manufactured pairs within ~150 words lands this dimension in the 5–6 band or lower; a single antecedent-backed pair does not move it); AD-14 (chat transcript as the artifact — **High**, and charged to the **document**, not to a fragment: a piece whose skeleton is a conversation cannot reach the upper bands, because no local edit repairs it), plus secondary signals (low weight): AD-2 (excessive parcellation), AD-4 (unprovoked rebuttal), AD-7 (preemptive virtue qualifier), AD-9 (hollow opener), AD-11 (mandatory tricolon), AD-16 (additive pseudo-pair). | Score | Criteria | |---|---| -| 9–10 | Inverted pyramid (main point first). One paragraph = one idea. Logical transitions. Headings where needed. No monotonous dash rhythm. No manufactured antithesis («не X, а Y» / «не просто X, а Y» with no antecedent). No hollow openers | +| 9–10 | Inverted pyramid (main point first). One paragraph = one idea. Logical transitions. Headings where needed. No monotonous dash rhythm. No manufactured antithesis («не X, а Y» / «не просто X, а Y» with no antecedent). No hollow openers. The piece is written rather than transcribed: its shape is an argument, not a sequence of exchanges | | 7–8 | Logical structure, but 1–2 overloaded paragraphs or implicit transitions; at most an isolated antecedent-backed antithesis | -| 5–6 | Structure is guessable but ideas are mixed, uneven paragraphs; a cluster of manufactured antitheses (two or more within ~150 words) recurs — this band or lower | +| 5–6 | Structure is guessable but ideas are mixed, uneven paragraphs; a cluster of manufactured antitheses (two or more within ~150 words) recurs — this band or lower. A document that is a chat transcript rather than a piece of writing (AD-14) belongs here at best, whatever the quality of the individual answers | | 3–4 | No structure: stream of consciousness without paragraph breaks | | 1–2 | Chaotic exposition, impossible to identify the main point | ### Ц — Reader precision (weight 0.20) -Type: soft (expert). Rule sources: [info-style.md](info-style.md) (facts/evidence section), domain-specific file when applicable. +Type: soft (expert). Rule sources: [info-style.md](info-style.md) (facts/evidence section), domain-specific file when applicable. From [addenda.md](addenda.md): AD-12 (hollowed mechanism — **Medium**: two or more clauses whose operative term is a placeholder land this dimension in the 5–6 band or lower), AD-13 (phantom attribution — **Medium**: a citation gesture with no identifiable source, which suppresses the reader's question instead of answering it), AD-15 (search-engine addressee — **High**, charged to the **document**: a piece written for the crawler cannot reach the upper bands, because the reader it names is not the reader it serves). Domain routing for this dimension: @@ -110,10 +110,10 @@ Domain routing for this dimension: | Score | Criteria | |---|---| -| 9–10 | Concrete facts and numbers. Clear reader benefit. Examples and evidence. No empty declarations. Actionable for the reader | +| 9–10 | Concrete facts and numbers. Clear reader benefit. Examples and evidence. No empty declarations. Actionable for the reader. Sources are named and checkable, never gestured at («исследования показывают»); mechanisms are named, never replaced by placeholders («различные факторы») | | 7–8 | Mostly concrete, but 1–2 claims without evidence or abstract phrases | | 5–6 | Mix of concrete and abstract. Some claims are unsubstantiated. Reader benefit unclear | -| 3–4 | Declarations and promises dominate over facts. Text says nothing | +| 3–4 | Declarations and promises dominate over facts. Text says nothing. Here too: a piece addressed to a search engine rather than a reader (AD-15), which answers the query and leaves the reader unable to act | | 1–2 | Fully abstract: «Мы предлагаем комплексные решения для вашего бизнеса» | --- @@ -140,6 +140,30 @@ These caps prevent high scores in other dimensions from masking critical weaknes Apply the most restrictive cap when multiple conditions trigger. +**Severity in `addenda.md` does not add a cap.** Two rules there are graded High — AD-14 +(chat transcript as the artifact) and AD-15 (search-engine addressee) — and both are +charged to the document rather than to a fragment. High says how far the dimension moves +and that no local edit repairs the defect; it does not mean a fourth row belongs in the +table above. The caps stay where they were: two hard dimensions and the global floor. This +is a deliberate choice and not an omission — a cap keyed to a soft, judgement-heavy +dimension would put a hard ceiling on a score behind a call a reader could reasonably +dispute, which is the failure mode the five orthogonal dimensions exist to avoid. + +**But the label has a floor, and it is not the same thing as a cap.** A document charged +with AD-14 or AD-15 is never labelled «Эталонный» or «Хороший», whatever the arithmetic +returns. Report the number as computed and the label as capped at «Средний», with one line +saying which rule capped it. + +The number is not touched, so nothing about the five dimensions is overridden: this +constrains only the word a reader takes away. It exists because the arithmetic can be +right and the summary still false. Measured: a search-engine stub — grammatical, +typographically clean, and of no use to anyone — scored 6.7 on Typography 9.5 and Grammar +9.5, because the two hard dimensions carry 0.35 of the weight and the whole harm sits in +Reader precision, which was already at 3.0 with nowhere left to fall. A flawless chat +transcript is the same shape and worse: with nothing wrong in any sentence it reaches 9.2, +«Эталонный», for a genre that platforms remove on sight. No rule about words can lower +either one, because neither has a defect in its words. + ## Score interpretation | Score | Label (RU) | Label (EN) | Meaning | @@ -152,6 +176,11 @@ Apply the most restrictive cap when multiple conditions trigger. A score of 8+ is a strong result. 10.0 is a theoretical ideal, not a practical target. +The two top labels have a floor above them: a document charged with AD-14 or AD-15 does not +receive them (see «Non-compensatory rules»). Say so in one line — «ярлык ограничен: AD-15» — +so the reader learns which rule capped it rather than wondering why the number and the word +disagree. + ## Output format ``` @@ -167,6 +196,7 @@ A score of 8+ is a strong result. 10.0 is a theoretical ideal, not a practical t **Формула:** T×0.15 + Ч×0.25 + Г×0.20 + С×0.20 + Ц×0.20 = X.X [Non-compensatory cap note, if triggered] +[Label floor note, if triggered: «Ярлык ограничен до «Средний»: AD-14» / «: AD-15»] ### Что оценка не измеряет [Limitations list] diff --git a/tools/atom-map.tsv b/tools/atom-map.tsv index cff35f0..0aef35b 100644 --- a/tools/atom-map.tsv +++ b/tools/atom-map.tsv @@ -5,4 +5,23 @@ # Dispositions: MOVED · RENUMBERED · NORMALISED · MERGED · DOC # There is deliberately no DELETED. See the header of diff-atoms.sh. # -# Empty on purpose: v2.0 has not moved a rule yet. The first rows arrive with stage 5. +# The rationale field is written for a human reviewer, and it is a human reviewer who has +# to read it. Say what changed and why, in the plainest words available. A row that +# describes a change of MEANING as a change of wording is the failure this file exists to +# prevent, and no checker can catch it. +30010ef1b6aec2c227d27f2507dd894a5d8bb276 NORMALISED dbfdac08dd169714285854edd8c95445f467c719 AD-7.5, the register carve-out. THE MEANING IS DELIBERATELY CHANGED, not merely reworded, and this row must not be read as cosmetic — NORMALISED is simply the only disposition the format has for a line that was rewritten in place. Before: the carve-out exempted the conversational and literary registers as such. After: it exempts a speaker INSIDE the text (dialogue, quotation, interviewee, character) and does not exempt the author writing about their own text in a conversational tone. Narrowed on the director's decision of 27.07.2026, after a probe with «скажу честно» showed the family falling through the old carve-out: that exact form was in no trigger list, its close relative «честно говоря» was among AD-7's, and the register carve-out exempted it — because the assistant register is a monologue written to sound like speech. The rule was there; the coverage was not. AD-10.4 now carries the canonical statement of the boundary; AD-7.5 keeps its own operational copy, because a rule read on its own must decide on its own. +1fe1665a1e817cb35ae327608ec1edfe4c86989e NORMALISED ec303f19ce53b8fabaa7c4268042d458099b8587 Ч — Clarity, the rule-source line. Scope extended, not reworded: AD-10 (declared sincerity) added as a PRIMARY signal at Medium, with the band it moves stated inline, and AD-11 added among the secondary ones for the case where a forced triple praises the author's own delivery. Nothing was removed; the five pre-existing addenda references are all still named. +9c18f65c284dda44bf64733fb986f00f4cbe9115 NORMALISED 2f6445aa082faf339cc99a3860d11765d56e2f20 Ч — Clarity, the 9–10 anchor. One clause appended: honesty is demonstrated rather than announced. Without it AD-10 would carry a severity that nothing in the rubric acts on — a rule declared Medium and scored as nothing. +63d0513e967d8bad6431d15061efb2e3feebf802 NORMALISED 04b64078c64a98dec8319cf00581830f58bdc566 С — Structure, the rule-source line. AD-14 added at High with its document-level charge stated, AD-11 and AD-16 added among the secondary signals. AD-1 and AD-6 keep their existing wording and weight. +348b9b03d941c7ef2a8620cd333fa3c9609ef503 NORMALISED d520cf8c3ffc5c05f25d913f9ee7c2ddd2da9bb7 С — Structure, the 9–10 anchor. One clause appended: the piece is written rather than transcribed. The top band has to exclude the AD-14 shape, or a flawless transcript scores as flawless prose. +8cdc28432c64df8395c1d00704030af9a287a88f NORMALISED 8fcf76ee42e33455839ebec5bac164a37960c798 С — Structure, the 5–6 anchor. One sentence appended placing an AD-14 document in this band at best, whatever the quality of the individual answers — which is the whole point of charging the shape. +68ea85849dcb487596b91d76c48d608d7102540f NORMALISED 1d9cfe353b140dafa896a9f15fa2662790be8e70 Ц — Reader precision, the rule-source line. Extended from two sources to include AD-12 and AD-13 (Medium) and AD-15 (High, document-level). Before this the dimension named no addenda rule at all, so three rules whose whole subject is what the reader gets had nowhere to land. +1ace8f082141db032f7fcb40165421c503e77b2d NORMALISED 798986cfeb554f6c823e319ef999e3bd03a2102f Ц — Reader precision, the 9–10 anchor. One clause appended: sources named rather than gestured at, mechanisms named rather than replaced by placeholders — the two properties AD-13 and AD-12 are about. +8867312137bcd1b063030ef5f0d6a6c7532fa1ca NORMALISED 8000f9886588df98a4cb68432cae9ee7c09d3483 Ц — Reader precision, the 3–4 anchor. One sentence appended putting an AD-15 document here: a text that answers the query and leaves the reader unable to act is the band's existing description, now named. +38e4534b7ef70e2058c5b6ac578ef56dcc00c2cc NORMALISED 121f94321e8be23f853c2e91338348371275a89b scoring.md, the header line that states what the rubric evaluates against. The UNIT changed, not just the number: «~1 044 independently formulated rules» was a hand-maintained count that nobody could reproduce and that had drifted, and it is replaced by the atom — the unit this repository's own no-loss gate counts. Reproduce with `tools/extract-atoms.sh skills/ru-text | wc -l` (2089 on this commit). «Over 2,000» is deliberately a floor rather than a figure: it stays true while the corpus grows and does not need re-stamping in eleven files every time a rule is added. Director's call, 28.07.2026. +59dd6b17c942acdc3f59e58ecaf7534a3880bbf0 NORMALISED 67c8e2c61329e0bb13eefb954c3e97ce709162ba scoring.md, the second statement of the same count inside «Why context-aware evaluation». The number is dropped rather than restated: one place in a file is enough to carry a figure, and the second copy is where the first one goes stale unnoticed. The sentence keeps its meaning — the rubric evaluates against the whole corpus — and loses only the duplicated number. +7d4ba9c891e0699d6ed045008db4cd96195fa9d6 NORMALISED 3568854daba830ac5227e04347bb24f5766def3c SKILL.md frontmatter, line 4 of the folded `description`. THE STRING WAS REWRITTEN ON PURPOSE and these three rows are one change, not three. On hosts with no instruction file — the Claude apps, ChatGPT — this description is the ENTIRE trigger, and the old wording contained no Russian at all: a user typing «вычитай» or «проверь текст» got nothing. The new one leads with the six phrases people actually type, inside the first 100 characters so a truncating picker still shows them, and keeps «Also any Russian output» because that clause carries the always-on semantics and is the easiest thing to lose in a rewrite. 210 characters against our own 250-character style budget. Approved in the v2 plan; `tools/check-version.sh` now fails if any of the six phrases goes missing again. +d2d1a428e9edfba03a9c8b8cf0ace6b6f0d111fb NORMALISED 11a5f35b77f4d382792261f70340630e0eab1cfa SKILL.md frontmatter, line 5 of the folded `description`. THE STRING WAS REWRITTEN ON PURPOSE and these three rows are one change, not three. On hosts with no instruction file — the Claude apps, ChatGPT — this description is the ENTIRE trigger, and the old wording contained no Russian at all: a user typing «вычитай» or «проверь текст» got nothing. The new one leads with the six phrases people actually type, inside the first 100 characters so a truncating picker still shows them, and keeps «Also any Russian output» because that clause carries the always-on semantics and is the easiest thing to lose in a rewrite. 210 characters against our own 250-character style budget. Approved in the v2 plan; `tools/check-version.sh` now fails if any of the six phrases goes missing again. +77986ec1fbc3669c9a71b097e5ee7a5a3833a748 NORMALISED 22dd3df3206f5a8158c92dc3af22177b53c1b570 SKILL.md frontmatter, line 6 of the folded `description`. THE STRING WAS REWRITTEN ON PURPOSE and these three rows are one change, not three. On hosts with no instruction file — the Claude apps, ChatGPT — this description is the ENTIRE trigger, and the old wording contained no Russian at all: a user typing «вычитай» or «проверь текст» got nothing. The new one leads with the six phrases people actually type, inside the first 100 characters so a truncating picker still shows them, and keeps «Also any Russian output» because that clause carries the always-on semantics and is the easiest thing to lose in a rewrite. 210 characters against our own 250-character style budget. Approved in the v2 plan; `tools/check-version.sh` now fails if any of the six phrases goes missing again. +5abfb7ce37b0477dedd1d53b05eba9b7128559df NORMALISED ff1576e376d682d314bfdea6714ac2dce11ded90 scoring.md, the rule-source line of T — Typography. The per-file rule counts are gone. They were unverifiable — no reproducible definition produced 171 for editorial-grammar.md — and adding §I, §J and §K made that one wrong for certain. Same decision as the corpus size on 28.07.2026: a figure nobody can recompute is a figure that drifts, and the file references carry everything a reader needs. Typography's 96 was accurate (R1…R96, no gaps, verified) and goes with the rest for consistency, not for being wrong. +89110d3041eaa8f8712173df454cae9d024ddaf8 NORMALISED 88a10b35cb5379bdf7a864d9536e6a385231c4ce scoring.md, the rule-source line of Г — Grammar. The per-file rule counts are gone. They were unverifiable — no reproducible definition produced 171 for editorial-grammar.md — and adding §I, §J and §K made that one wrong for certain. Same decision as the corpus size on 28.07.2026: a figure nobody can recompute is a figure that drifts, and the file references carry everything a reader needs. Typography's 96 was accurate (R1…R96, no gaps, verified) and goes with the rest for consistency, not for being wrong. The line also names the three new sections, so a reader of the rubric knows they exist. diff --git a/tools/build-release.sh b/tools/build-release.sh new file mode 100755 index 0000000..a45191f --- /dev/null +++ b/tools/build-release.sh @@ -0,0 +1,124 @@ +#!/bin/sh +# build-release.sh — the two assets a release ships, built from tracked files only. +# +# tools/build-release.sh # build into dist/ +# tools/build-release.sh --check # build, verify, then delete — proves it still works +# +# Two archives, because hosts disagree about what a "skill" is: +# +# ru-text-skills.zip three folders at the top — ru-text/, ru-check/, ru-score/ — for a +# host that installs a directory of skills. +# ru-text-skill.zip one folder, ru-text/, for a host that takes exactly one skill and +# would treat the other two as strays. +# +# Both carry the same corpus. Neither carries the tools, the golden set, the manifests or +# the docs: a user installing a skill has no use for the machinery that verifies it. +# +# ── the rules this script exists to enforce ────────────────────────────────────────── +# +# **Tracked files only.** `git ls-files`, never the working tree. An asset built from the +# tree ships whatever happens to be lying in it — a scratch file, a half-finished rule, an +# editor backup — and the difference is invisible in the archive. +# +# **A clean checkout, or nothing.** Building from a dirty tree produces an artefact whose +# contents no commit describes, and the version inside it is then a lie about which code it +# is. The paid-MCP refresh learned this the same way and refuses for the same reason. +# +# **Verified by unpacking.** A zip that lists the right names can still unpack wrong. Every +# build extracts into a clean directory and compares the result with the source, file by +# file, before the archive is allowed to exist. + +set -eu +export LC_ALL=C + +cd "$(dirname "$0")/.." + +CHECK=0 +[ "${1:-}" = "--check" ] && CHECK=1 + +fail() { printf 'build-release: FAIL — %s\n' "$1" >&2; exit 1; } + +command -v zip >/dev/null 2>&1 || fail "zip is not installed" + +# ── 1. The checkout must be clean ──────────────────────────────────────────────────── +# --check does not require it: that mode proves the build still works and writes nothing, +# so it can run inside a gate on a tree someone is in the middle of editing. Warning about +# dirtiness on every local run would be noise, and noise is how a gate teaches people to +# stop reading it. +if [ "$CHECK" -eq 0 ] && [ -n "$(git status --porcelain 2>/dev/null)" ]; then + git status --porcelain >&2 + fail "the working tree is dirty; an asset built from it matches no commit" +fi + +# ── 2. The version must agree with itself ──────────────────────────────────────────── +tools/check-version.sh >/dev/null || fail "check-version — the version disagrees with itself" +VERSION=$(python3 -c "import io,json;print(json.load(io.open('.claude-plugin/plugin.json',encoding='utf-8'))['version'])") +printf 'build-release: version %s\n' "$VERSION" + +DIST=dist +rm -rf "$DIST" +mkdir -p "$DIST" + +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT INT TERM + +# Tracked skill files, copied preserving their paths under skills/. +git ls-files -z skills \ + | tr '\0' '\n' \ + | while IFS= read -r f; do + [ -n "$f" ] || continue + mkdir -p "$STAGE/all/$(dirname "${f#skills/}")" + cp "$f" "$STAGE/all/${f#skills/}" + done + +[ -f "$STAGE/all/ru-text/SKILL.md" ] || fail "the staged tree has no ru-text/SKILL.md" + +# ── 3. Asset one: three skills ─────────────────────────────────────────────────────── +( cd "$STAGE/all" && zip -q -r -X "$STAGE/ru-text-skills.zip" ru-text ru-check ru-score ) + +# ── 4. Asset two: one skill ────────────────────────────────────────────────────────── +( cd "$STAGE/all" && zip -q -r -X "$STAGE/ru-text-skill.zip" ru-text ) + +# ── 5. Unpack each and compare, because a listing is not a proof ───────────────────── +verify() { # $1=archive $2=expected top-level dirs (space separated) + a=$1; want=$2 + out="$STAGE/verify-$(basename "$a" .zip)" + mkdir -p "$out" + unzip -q "$a" -d "$out" || fail "$a does not unpack" + got=$(cd "$out" && ls -1 | sort | tr '\n' ' ') + exp=$(printf '%s\n' $want | sort | tr '\n' ' ') + [ "$got" = "$exp" ] || fail "$a unpacks to [$got], expected [$exp]" + for d in $want; do + diff -r "$STAGE/all/$d" "$out/$d" >/dev/null || fail "$a: $d differs from the source after a round trip" + done + n=$(find "$out" -type f | wc -l | tr -d ' ') + printf 'build-release: ok %-22s %s files, round-trip identical\n' "$(basename "$a")" "$n" +} + +verify "$STAGE/ru-text-skills.zip" "ru-text ru-check ru-score" +verify "$STAGE/ru-text-skill.zip" "ru-text" + +# ── 6. The corpus inside the asset must be the corpus ──────────────────────────────── +# A build that quietly drops a reference file is the failure mode with no symptom: the +# skill installs, activates, and answers from whatever it still has. +# Two comparisons, and the second is the one that matters. Tracked-versus-staged catches a +# path bug in the copy loop. But both sides read `git ls-files`, so a file dropped from the +# INDEX moves both numbers together and the equality stays true — which is exactly what a +# selftest case proved. The floor is the answer: the paid-MCP contract fixes references/ at +# ten files (tools/frozen.sha256, files=10), and an asset with fewer is not this product. +MIN_REFS=10 +want_refs=$(git ls-files skills/ru-text/references | wc -l | tr -d ' ') +got_refs=$(find "$STAGE/all/ru-text/references" -name '*.md' | wc -l | tr -d ' ') +[ "$want_refs" = "$got_refs" ] || fail "references/: $got_refs in the asset, $want_refs tracked" +[ "$got_refs" -ge "$MIN_REFS" ] || fail "references/: $got_refs in the asset, the contract fixes it at $MIN_REFS" +printf 'build-release: ok references/ complete (%s files)\n' "$got_refs" + +if [ "$CHECK" -eq 1 ]; then + rm -rf "$DIST" + echo "build-release: PASS (--check: nothing written)" + exit 0 +fi + +mv "$STAGE/ru-text-skills.zip" "$STAGE/ru-text-skill.zip" "$DIST/" +printf 'build-release: wrote %s/ru-text-skills.zip and %s/ru-text-skill.zip\n' "$DIST" "$DIST" +echo "build-release: PASS" diff --git a/tools/check-dogfood.sh b/tools/check-dogfood.sh new file mode 100755 index 0000000..35fc113 --- /dev/null +++ b/tools/check-dogfood.sh @@ -0,0 +1,186 @@ +#!/bin/sh +# check-dogfood.sh — the numbers this product states about itself must be true. +# +# tools/check-dogfood.sh # verify +# tools/check-dogfood.sh --print # what it measured, and every place that claims it +# +# check-version.sh proves the version agrees with itself. This proves the claims agree with +# the corpus: the size of the stop-word catalogue, and the number of its categories. Both +# are measured from `info-style.md` by the same parser check-frozen.sh uses — itself a port +# of the paid MCP's — and then compared with every file that states them. +# +# ── why it is built this way ───────────────────────────────────────────────────────── +# +# The first version scanned for claims with patterns like «(\d+) записей» and immediately +# reported twenty-two false ones: `anti-patterns.md` counts its own sections in the same +# words. Guessing which number in a sentence is a claim about the catalogue is not a +# solvable problem in a shell script, and a checker that cries wolf is one people disable. +# +# So the list of consumers is EXPLICIT, and a second check guarantees the list is complete: +# the current value must not appear anywhere outside it. Move the catalogue to 93 and the +# stale «92» left in some file is still a «92» — the guard finds it, names the file, and +# the list gets a new row. A fixed list alone would have the failure mode that put four +# consumers out of date last time; a fixed list WITH a completeness guard does not. +# +# Deliberately unread: CHANGELOG.md, which records what the numbers WERE and must keep +# saying 97 where 97 was once wrong; and tools/, whose comments describe past parser bugs +# by their numbers. A checker that fails on an accurate history teaches people to skip it. + +set -eu +export LC_ALL=C + +cd "$(dirname "$0")/.." + +INFO=skills/ru-text/references/info-style.md + +fail=0 +ok() { printf ' ok %s\n' "$1"; } +bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; } + +catalog_size() { + awk ' + /^## B\. Каталог стоп-слов/ { inb = 1; next } + !inb { next } + /^## / { exit } + /^### / { next } + /^слово\|замена$/ { next } + /^[^|#]+\|.+$/ { print } + ' "$INFO" | wc -l | tr -d ' ' +} + +category_count() { + awk ' + /^## B\. Каталог стоп-слов/ { inb = 1; next } + !inb { next } + /^## / { exit } + /^### / { n++ } + END { print n + 0 } + ' "$INFO" +} + +# Every place that states the catalogue size, and the phrase it states it in. A file is +# listed once per claim. Adding a claim to the product means adding a row here — and if you +# forget, the completeness guard below says so by name. +CATALOG_CLAIMS='skills/ru-text/SKILL.md|Full stop-word catalog (%s entries) +README.md|каталог из %s стоп-слов +README.en.md|the catalogue of %s stop-words +notion/README.md|- %s stop-words across +notion/README.md|- %s стоп-слова в +skills/ru-text/references/editorial-grammar.md|%s in §B +tools/frozen.sha256|catalog_entries=%s' + +CATEGORY_CLAIMS='notion/README.md|across %s categories +notion/README.md|в %s категориях' + +# Words the product says about its own corpus that must exist IN that corpus. Numbers are +# checked above; this checks names, and it exists because one name got it wrong twice. +# +# «онбординг» was written into the README's description of ux-writing.md, corrected on +# 28.07 when a check found the file has no such section (its thirteen run A to M, and I is +# «Диалоги подтверждения»), and then written back in during a rewrite two days later. A +# defect that returns is a defect nobody is guarding. +# +# Format: claiming file phrase file the phrase claims to describe. +NAME_CLAIMS='README.md онбординг skills/ru-text/references/ux-writing.md +README.en.md onboarding skills/ru-text/references/ux-writing.md' + +verify_names() { + bad='' + while IFS=' ' read -r claimant phrase target; do + [ -n "$claimant" ] || continue + [ -f "$claimant" ] || continue + grep -qiF -- "$phrase" "$claimant" || continue # not claimed: nothing to check + grep -qiF -- "$phrase" "$target" && continue # claimed and present: fine + bad="$bad${bad:+ +}$claimant says «$phrase» about $target, which does not contain it" + done < "$TMP/claims.$$" + n=$(grep -c '^OK' "$TMP/claims.$$" || true) + wrong=$(grep -v '^OK' "$TMP/claims.$$" || true) + if [ -z "$wrong" ]; then + ok "$1: $n place(s) state $2, and the corpus agrees" + else + bad "$1: the corpus says $2, and these do not:" + printf '%s\n' "$wrong" | sed 's/^/ /' + fi + rm -f "$TMP/claims.$$" +} + +# The completeness guard. Any occurrence of the current value outside the listed places is +# either a claim nobody registered or a coincidence — and the checker cannot tell them +# apart, which is the point: it names the line and a person decides once. +guard_complete() { # $1=label $2=value $3=claim list + listed=$(printf '%s\n' "$3" | cut -d'|' -f1 | sort -u) + hits=$(git ls-files -z \ + | tr '\0' '\n' \ + | grep -vE '^(CHANGELOG\.md|tools/|docs/|\.github/)' \ + | while IFS= read -r f; do + # NOT a `case` with alternation: bash 3.2 — which is /bin/sh on macOS — fails to + # parse one inside a command substitution, and `sh -n` does not catch it. The + # error surfaces only when the line runs. Measured 28.07.2026. + printf '%s' "$f" | grep -qE '\.(md|json|ya?ml|sha256)$' || continue + printf '%s\n' "$listed" | grep -qxF "$f" && continue + [ -f "$f" ] || continue + # A letter before the digits means an identifier, not a count: typography.md + # carries rule R92, and a guard that reads it as a stale claim about the + # catalogue is the crying-wolf failure this design exists to avoid. + grep -nE "(^|[^0-9A-Za-zА-Яа-яЁё])$2([^0-9]|$)" "$f" 2>/dev/null | sed "s|^|$f:|" + done) + if [ -z "$hits" ]; then + ok "$1: no unregistered occurrence of $2 outside the listed places" + else + bad "$1: $2 appears where no claim is registered — add a row or rephrase the line:" + printf '%s\n' "$hits" | sed 's/^/ /' + fi +} + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT INT TERM + +CAT=$(catalog_size) +CATEG=$(category_count) + +if [ "${1:-}" = "--print" ]; then + printf 'catalogue entries: %s\ncategories: %s\n\n' "$CAT" "$CATEG" + echo '— places that state the size —' + printf '%s\n' "$CATALOG_CLAIMS" | while IFS='|' read -r f tmpl; do + printf ' %-46s %s\n' "$f" "$(printf '%s' "$tmpl" | sed "s/%s/$CAT/")" + done + exit 0 +fi + +printf 'check-dogfood: the numbers the product states about itself\n' + +verify_claims "stop-word catalogue" "$CAT" "$CATALOG_CLAIMS" +guard_complete "stop-word catalogue" "$CAT" "$CATALOG_CLAIMS" +verify_claims "catalogue categories" "$CATEG" "$CATEGORY_CLAIMS" +verify_names + +[ "$fail" -eq 0 ] && { echo "check-dogfood: PASS"; exit 0; } +echo "check-dogfood: FAIL — $fail check(s)" +exit 1 diff --git a/tools/check-typography.sh b/tools/check-typography.sh new file mode 100755 index 0000000..5586ded --- /dev/null +++ b/tools/check-typography.sh @@ -0,0 +1,165 @@ +#!/bin/sh +# check-typography.sh — this product's own Russian prose must obey this product's own rules. +# +# tools/check-typography.sh # verify +# tools/check-typography.sh --print # what is checked, and where +# +# ── why ────────────────────────────────────────────────────────────────────────────── +# +# The README claims, in its own words, that «каждое тире, кавычка и пробел в этом документе +# соответствуют правилам самого плагина». Nothing enforced that. It was checked by asking a +# model, by hand, when someone remembered — and it fell out of step twice in one afternoon: +# a literal `~` reached the file from a placeholder for a non-breaking space, and a missing +# NBSP before an em dash shipped in prose written to explain non-breaking spaces. +# +# A model reads the whole corpus and judges meaning. This checks the mechanical subset that +# needs no judgement at all, on every run, for free. It does not replace the model; it stops +# the class of defect that should never have needed one. +# +# ── scope, and why it is an explicit list ──────────────────────────────────────────── +# +# ONLY continuous Russian prose written in the project's own voice. The list is literal and +# short, because the alternative was measured and rejected: run the same patterns over the +# corpus files and `notion/ru-text-notion-skill.md` and they report 53 «violations» that are +# all correct content — the wrong-hand column of a «плохо → хорошо» table, and stop-word +# entries like `в настоящее время` that are dictionary keys rather than sentences. In a file +# that teaches typography by showing it broken, breakage is the point. +# +# Deliberately excluded, each for a stated reason: +# skills/ru-text/references/* — rule examples; a violation there is the specimen +# notion/ru-text-notion-skill.md — same, condensed +# tools/golden/* — fixtures; several are broken on purpose +# CHANGELOG.md — English prose quoting Russian fragments; an English em +# dash takes an ordinary space, so the rule does not apply +# README.en.md, INSTALL.en.md — English; guillemets and NBSP are not its conventions +# +# ── what is checked ────────────────────────────────────────────────────────────────── +# +# R16/R44 the space before an em dash is non-breaking, so the dash cannot open a line +# R32/R53 digit groups take a non-breaking space, never a comma, a period or a plain space +# R31 initials bind to the surname they belong to +# R30 a single-letter preposition does not end a line +# R1/R2 guillemets, not straight quotes, around Russian text +# R-ellipsis a single … character, not three periods +# plus: a stray literal `~`, which is not a rule but the exact defect that shipped twice — +# a placeholder for U+00A0 that survived into the file. +# +# Inline code and URLs are stripped before matching: `~/.agents/skills` is a home directory, +# not a mangled space, and a hyphen inside a command is not a dash. + +set -eu +export LC_ALL=C + +cd "$(dirname "$0")/.." + +# Every file whose prose speaks as the project. Adding one is a deliberate act; see the scope +# note above before you do, because the wrong file here makes this checker cry wolf. +FILES='README.md +INSTALL.md' + +fail=0 +ok() { printf ' ok %s\n' "$1"; } +bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; } + +if [ "${1:-}" = "--print" ]; then + printf 'files checked, and why only these — see the header:\n' + printf '%s\n' "$FILES" | sed 's/^/ /' + exit 0 +fi + +printf 'check-typography: the product obeys its own typography\n' + +report=$(python3 - $FILES <<'PY' +import io, re, sys + +NB = '\u00a0' +# BOTH cases. The first version listed lowercase only, and a check that never tests «В» or +# «У» at the start of a sentence is not strict — it is blind exactly where a sentence most +# often begins. It reported INSTALL.md clean while the file held six of them. +PREP = 'вкосуиаяВКОСУИАЯ' +out = [] + +for path in sys.argv[1:]: + try: + lines = io.open(path, encoding='utf-8').read().split('\n') + except OSError as e: + out.append('%s\tMISSING\t%s' % (path, e)) + continue + + fence = False + for n, raw in enumerate(lines, 1): + if raw.lstrip().startswith('```'): + fence = not fence + continue + if fence: + continue + # Inline code and link targets carry paths, flags and commands. `~/.agents/skills` is + # a tilde that belongs; a hyphen in `-g` is not a dash. Blank them, keeping offsets so + # the reported column still points at the real character. + s = re.sub(r'`[^`]*`', lambda m: ' ' * len(m.group(0)), raw) + s = re.sub(r'\]\([^)]*\)', lambda m: ' ' * len(m.group(0)), s) + s = re.sub(r'https?://\S+', lambda m: ' ' * len(m.group(0)), s) + + for m in re.finditer('—', s): + if m.start() > 0 and s[m.start() - 1] == ' ': + out.append('%s\t%d\tR16/R44\tordinary space before an em dash\t%s' + % (path, n, s[max(0, m.start() - 40):m.start() + 20].strip())) + + # What may FOLLOW the space is deliberately broad. The first version required a + # Cyrillic letter or a digit next, so «в [README]», «и **`.agents/skills/`**» and + # «и /ru-score» all escaped — a preposition is no less stranded for being followed + # by a bracket, an asterisk or a slash. Excluded: another space (already a break) + # and end of line (nothing left to bind to). + for m in re.finditer(r'(?-?|\|-?)?[ \t]*\n((?:[ \t]+\S.*\n)+)|^description:[ \t]*(.+)$', + fm.group(1), re.M) +if not m: + sys.exit('no description in ' + sys.argv[1]) +body = m.group(1) or m.group(2) or '' +text = ' '.join(l.strip() for l in body.strip().split('\n')) +# Длина — в СИМВОЛАХ. `wc -m` под LC_ALL=C считает байты, а кириллица в UTF-8 двухбайтовая: +# на бюджете 250 это давало 255 «символов» для строки из 210 и роняло проверку впустую. +print('%d\t%s' % (len(text), text)) +PY +} + +# The phrases a Russian-speaking user actually types. Losing one is invisible everywhere +# except in whether the skill fires, so they are listed here and nowhere else. +TRIGGERS='вычитай +проверь текст +поправь +отредактируй +причеши +ru-text' + +if [ "${1:-}" = "--print" ]; then + echo "— version points —"; version_points + d=$(description) + printf -- '— description (%s chars, budget %s) —\n%s\n' "$(printf '%s' "$d" | cut -f1)" "$BUDGET" "$(printf '%s' "$d" | cut -f2-)" + exit 0 +fi + +printf 'check-version: one version, one budget, one set of triggers\n' + +# ── 1. Every version point agrees ──────────────────────────────────────────────────── +points=$(version_points) +distinct=$(printf '%s\n' "$points" | cut -f2 | sort -u | wc -l | tr -d ' ') +count=$(printf '%s\n' "$points" | grep -c . | tr -d ' ') +if printf '%s\n' "$points" | grep -q 'MISSING-OR-DUPLICATE'; then + bad "a version line is missing or duplicated:" + printf '%s\n' "$points" | grep 'MISSING-OR-DUPLICATE' | sed 's/^/ /' +elif [ "$distinct" -eq 1 ]; then + ok "all $count version points agree on $(printf '%s\n' "$points" | head -1 | cut -f2)" +else + bad "$count version points disagree:" + printf '%s\n' "$points" | sed 's/^/ /' +fi + +# ── 2. The description fits our budget ─────────────────────────────────────────────── +raw=$(description) +len=$(printf '%s' "$raw" | cut -f1) +desc=$(printf '%s' "$raw" | cut -f2-) +if [ "$len" -le "$BUDGET" ]; then + ok "skill description is $len characters (our budget: $BUDGET)" +else + bad "skill description is $len characters, over our budget of $BUDGET" +fi + +# ── 3. Every trigger phrase is present, and the Russian ones come early ────────────── +head_chunk=$(printf '%s' "$desc" | cut -c1-$((FIRST_N * 3))) # cut counts bytes; UTF-8 RU is 2 +missing='' +late='' +printf '%s\n' "$TRIGGERS" | while read -r t; do + [ -n "$t" ] || continue + case "$desc" in + *"$t"*) ;; + *) printf 'MISSING\t%s\n' "$t" ;; + esac +done > /tmp/.check-version-triggers.$$ +missing=$(cat /tmp/.check-version-triggers.$$); rm -f /tmp/.check-version-triggers.$$ +if [ -n "$missing" ]; then + bad "the description has lost a trigger phrase — on the Claude apps and ChatGPT that is the whole trigger:" + printf '%s\n' "$missing" | sed 's/^/ /' +else + ok "all trigger phrases present" +fi + +# Position matters only for the Russian ones: a host that truncates shows the head. +early=$(python3 - "$FIRST_N" <= n] +print('\n'.join(late)) +PY +) +if [ -n "$early" ]; then + bad "Russian trigger phrases sit past character $FIRST_N, where a truncating host will not show them:" + printf '%s\n' "$early" | sed 's/^/ /' +elif [ -n "$missing" ]; then + # Not «ok»: with the phrases absent there is nothing for a position test to be true about, + # and a green line here would read as though the placement had been verified. + printf ' -- position of the Russian phrases not checked: they are missing\n' +else + ok "Russian trigger phrases are inside the first $FIRST_N characters" +fi + +# ── 4. Both READMEs state the true size of SKILL.md ──────────────────────────────────── +# «Техническое качество» quotes the word and line count as evidence that the always-on half +# of the skill is small. Nobody re-measured it: the line said 587 words for three releases +# while the file held 583. A claim offered as proof has to be measured, or it is the exact +# opposite of what it claims to be. +# +# The numbers are compared, not the sentence. Russian inflects the noun with the numeral -- +# 583 slova, 587 slov, 581 slovo -- so a literal-string check would fail on a correct line +# whenever the count crossed a declension boundary, and the fix would be to edit the checker. +# Reading the numbers off the line is form-agnostic and survives translation; the trailing +# budget figures are ignored because only the first two are the claim. +# +# Exactly ONE line per README may state the size, which is the rule section 1 above already +# lives by: a second copy is where the first goes stale. A review proved the cost of not +# applying it here -- with two "SKILL.md:" lines the checker read the first, vouched for it, +# and passed while both files were wrong. +# +# Grouped thousands are glued before the digits are read. re.findall for digit runs never +# matches a separator, so replacing one cannot change the result: an earlier version replaced +# NBSP and comma and was a provable no-op, which read "1 083 words" as the pair (1, 083) and +# failed a correct README with a nonsense diagnostic. +skill_size_ok=$(python3 - "$SKILL" README.md README.en.md <<'PY' +import io, re, sys + +path, readmes = sys.argv[1], sys.argv[2:] +text = io.open(path, encoding='utf-8').read() +# Defined to match `LC_ALL=C wc -w` and `wc -l`, which is what a maintainer checking this by +# hand will run -- and this script exports LC_ALL=C. NOT text.split(): python treats U+00A0 as +# whitespace and byte-wise wc does not, so on a file carrying the non-breaking spaces R16/R44 +# require the two would disagree, and the README would be "corrected" into a false failure. +words = len(re.findall(r'[^ \t\n\r\f\v]+', text)) +lines = text.count('\n') + +bad = [] +for r in readmes: + try: + content = io.open(r, encoding='utf-8').read() + except OSError as e: + bad.append('%s: unreadable (%s)' % (r, e)) + continue + hit = [ln for ln in content.split('\n') if 'SKILL.md:' in ln] + # Nought is allowed and one is allowed; two is not. The size claim is optional — the + # rewritten Russian README drops it, because a manifest's word count answers «did the + # author follow the spec», which is not a question any reader has. What this section + # forbids is stating it WRONGLY, and a file that says nothing states nothing wrongly. + # Two lines is still a defect: that is where the first goes stale unnoticed. + if len(hit) == 0: + continue + if len(hit) > 1: + bad.append('%s: %d lines state the size of SKILL.md, want at most 1' % (r, len(hit))) + continue + glued = re.sub(r'(?<=\d)[ \t  ,](?=\d\d\d(?!\d))', '', hit[0]) + nums = re.findall(r'\d+', glued) + if len(nums) < 2: + bad.append('%s: %r carries no pair of numbers to check' % (r, hit[0].strip())) + continue + got_w, got_l = int(nums[0]), int(nums[1]) + if (got_w, got_l) != (words, lines): + bad.append('%s: says %d words / %d lines, file has %d / %d' + % (r, got_w, got_l, words, lines)) + +# US (unit separator), not "; ": a README line quoted back into a diagnostic can contain a +# semicolon, and splitting on one tore a message in half mid-quote. +print('%d\t%d\t%s' % (words, lines, '\x1f'.join(bad))) +PY +) +sz_words=$(printf '%s' "$skill_size_ok" | cut -f1) +sz_lines=$(printf '%s' "$skill_size_ok" | cut -f2) +sz_bad=$(printf '%s' "$skill_size_ok" | cut -f3-) +# An empty measurement must not read as agreement. The heredoc always reaches its print, so +# this needs a broken interpreter -- but the ok branch below is keyed on "no complaints", and +# "no output at all" is indistinguishable from it without this line. +case "$sz_words$sz_lines" in + ''|*[!0-9]*) sz_bad="the SKILL.md measurement produced nothing -- is python3 usable?" ;; +esac +if [ -z "$sz_bad" ]; then + ok "both READMEs state the size of SKILL.md correctly ($sz_words words, $sz_lines lines)" +else + bad "a README states the size of SKILL.md wrongly:" + printf '%s\n' "$sz_bad" | tr '\037' '\n' | sed 's/^ */ /' +fi + +[ "$fail" -eq 0 ] && { echo "check-version: PASS"; exit 0; } +echo "check-version: FAIL — $fail check(s)" +exit 1 diff --git a/tools/gates.sh b/tools/gates.sh new file mode 100755 index 0000000..3a00749 --- /dev/null +++ b/tools/gates.sh @@ -0,0 +1,111 @@ +#!/bin/sh +# gates.sh — every gate CI runs, in CI's order, from one command. +# +# tools/gates.sh +# +# Exit 0 only when all of them pass: a checker selftest, the corpus contract with the paid +# MCP, and the no-loss gate against the pinned v1.10.1 baseline. +# +# What it does NOT cover, said here so a green line is not read as more than it is: +# `tools/golden/` — the curated texts with their expected findings. Those are run by a +# model through `/ru-text:ru-check`, deliberately and permanently without a harness +# (tools/golden/README.md says why), so nothing here can execute them. A `gates: PASS` +# means the machinery is sound; whether the corpus still finds what it promises is a +# separate question this script does not ask. +# +# Why it exists. The three gates were three commands a person had to remember, listed in a +# handoff note and in .github/workflows/gates.yml, and nowhere the machine would read. +# The pre-push hook looks for a repository's declared suite — .claude/pre-push.sh, then +# tools/gates.sh, then tools/selftest.sh — so naming the file this way is what makes a red +# gate stop a push instead of reddening CI ten minutes later. +# +# Kept in step with .github/workflows/gates.yml by hand, and that is a real cost: two +# copies of a sequence drift. The alternative — CI calling this script — was rejected +# because then a broken script silently disables CI too, and the runner's own `set -eu` +# and step boundaries are the second opinion. Change one, change the other; the selftest +# case «gates.sh runs the same checkers CI does» fails when they diverge. + +set -eu +export LC_ALL=C + +cd "$(dirname "$0")/.." + +BASELINE=tools/baseline/atoms-v1.10.1.tsv +# The baseline is pinned by content, not by policy: without this, deleting a rule from the +# corpus AND its line from the baseline passes every gate green, because the comparison is +# with a file the same commit is free to edit. This moves once, at the v2.0 release. +BASELINE_SHA=98b2e03bc771d7c0b3268f4eb406fbb3641bc8fdb01243ff3053a1790e2b3aa4 + +fail() { printf 'gates: FAIL — %s\n' "$1" >&2; exit 1; } + +# ── 1. The checkers must be runnable ──────────────────────────────────────────────── +# A syntax error in a checker reads as a passing checker to anything that only looks at +# whether the file exists. +# +# `find`, not the `tools/*.sh` glob: the glob stops at the top level, and a checker one +# directory down was invisible to this gate and to the CI-parity case both. +# +# `dash -n` when it exists, because `sh -n` checks a different language on each machine. +# On macOS /bin/sh is bash 3.2, which parses `arr=(a b)` happily; on the ubuntu runner +# /bin/sh is dash, which does not. A checker written with a bashism therefore passed here +# and failed in CI — the exact divergence a local gate exists to catch first. +SHCHECK=sh +command -v dash >/dev/null 2>&1 && SHCHECK=dash +for f in $(find tools -name '*.sh' -type f | sort); do + [ -x "$f" ] || fail "not executable: $f" + "$SHCHECK" -n "$f" || fail "syntax error: $f" +done +echo "gates: ok tools/**.sh executable and POSIX-parseable (via $SHCHECK -n)" + +# ── 2. Selftest — every checker must be able to fail ──────────────────────────────── +# Runs first among the gates. If the checkers cannot fail, their passing below means +# nothing at all. +tools/selftest.sh || fail "selftest" + +# ── 3. The paid-MCP corpus contract ───────────────────────────────────────────────── +tools/check-frozen.sh || fail "check-frozen" + +# ── 4. One version, one description budget, one set of triggers ───────────────────── +tools/check-version.sh || fail "check-version" + +# ── 5. The numbers the product states about itself ────────────────────────────────── +tools/check-dogfood.sh || fail "check-dogfood" + +# ── 5b. The product obeys its own typography ──────────────────────────────────────── +# The mechanical subset only, over the two files that speak in the project's own voice. +# It exists because the README claims its every dash and space follows the plugin's rules, +# and that claim was checked by remembering to ask a model — which failed twice in one day. +tools/check-typography.sh || fail "check-typography" + +# ── 6. No-loss gate against the pinned baseline ───────────────────────────────────── +# GNU coreutils names it sha256sum; macOS ships shasum and no sha256sum. Checking for +# both, rather than assuming either, is why this script runs on the author's machine and +# on the Linux runner. +if command -v sha256sum >/dev/null 2>&1; then + printf '%s %s\n' "$BASELINE_SHA" "$BASELINE" | sha256sum -c - >/dev/null \ + || fail "baseline pin moved: $BASELINE" +elif command -v shasum >/dev/null 2>&1; then + printf '%s %s\n' "$BASELINE_SHA" "$BASELINE" | shasum -a 256 -c - >/dev/null \ + || fail "baseline pin moved: $BASELINE" +else + fail "no sha256 tool (sha256sum, shasum) — the baseline pin cannot be verified" +fi +echo "gates: ok baseline pinned by content" + +NOW=$(mktemp) +# INT and TERM as well as EXIT: dash does not run an EXIT trap when the shell dies on a +# signal, so on the Linux runner a Ctrl-C left the snapshot behind. Measured, not assumed. +trap 'rm -f "$NOW"' EXIT INT TERM +tools/extract-atoms.sh skills/ru-text > "$NOW" || fail "extract-atoms" +# Invoked exactly as CI invokes it. The map argument is the same file diff-atoms.sh falls +# back to, so passing it changed nothing — except to put a difference between the two +# copies of this sequence that the parity case cannot see. +tools/diff-atoms.sh "$BASELINE" "$NOW" || fail "no-loss gate" + +# ── 7. The release assets can still be built ──────────────────────────────────────── +# Writes nothing. Catches the failure with no symptom: a renamed or dropped reference +# file that leaves the skill installing, activating, and answering from what it still has. +tools/build-release.sh --check >/dev/null || fail "build-release" +echo "gates: ok both release assets build and round-trip" + +echo "gates: PASS" diff --git a/tools/golden/01-corporate-landing/expected.md b/tools/golden/01-corporate-landing/expected.md new file mode 100644 index 0000000..d133c50 --- /dev/null +++ b/tools/golden/01-corporate-landing/expected.md @@ -0,0 +1,19 @@ +Домен: info-style + anti-patterns. Корпоративный лендинг — плотность стоп-слов и +канцелярита. + +Обязаны найтись: + +- `осуществляет деятельность` — канцелярит, §B каталога («осуществлять» → делать) +- `в сфере предоставления` — номинализация +- `являясь одним из лидеров рынка` — оценка без доказательств +- `в настоящее время` — §B («сейчас») +- `действительно качественные` — усилитель + оценка без доказательств +- `индивидуальный подход` — штамп, §B (замена: конкретный процесс работы с клиентом) +- `данный подход` — канцелярит («этот») +- `максимально эффективных` — усилитель без факта +- `в кратчайшие сроки` — штамп, срок не назван +- `производим работы` — канцелярит («делаем») +- `обеспечиваем сопровождение` — номинализация + +Не должно найтись: ничего из AD-1…AD-9 — текст плох по инфостилю, но не машинный по +структуре. diff --git a/tools/golden/01-corporate-landing/text.md b/tools/golden/01-corporate-landing/text.md new file mode 100644 index 0000000..72f86e5 --- /dev/null +++ b/tools/golden/01-corporate-landing/text.md @@ -0,0 +1,8 @@ +Наша компания на протяжении многих лет осуществляет деятельность в сфере +предоставления консалтинговых услуг, являясь одним из лидеров рынка. + +В настоящее время мы предлагаем действительно качественные решения по выгодным ценам. +Индивидуальный подход к каждому клиенту — это то, что нас отличает. + +Данный подход позволяет достичь максимально эффективных результатов в кратчайшие сроки. +Мы производим работы по внедрению и обеспечиваем сопровождение на всех этапах. diff --git a/tools/golden/02-bureaucratic-memo/expected.md b/tools/golden/02-bureaucratic-memo/expected.md new file mode 100644 index 0000000..63c037b --- /dev/null +++ b/tools/golden/02-bureaucratic-memo/expected.md @@ -0,0 +1,16 @@ +Домен: anti-patterns §Канцелярит + info-style. Служебная записка. + +Обязаны найтись: + +- `в целях` — §B («чтобы, для») +- `повышения эффективности взаимодействия` — цепочка номинализаций +- `было принято решение о проведении мероприятий` — пассив + номинализация +- `осуществляется контроль` — канцелярит и пассив +- `надлежащим` — §B («нужный, правильный») +- `в соответствии с` — §B («по, согласно») +- `подлежит рассмотрению` — пассив +- `в установленные сроки` — срок не назван +- `по итогам проведённой работы` — канцелярит +- `соответствующего решения` — пустой определитель + +Не должно найтись: AD-1…AD-9. diff --git a/tools/golden/02-bureaucratic-memo/text.md b/tools/golden/02-bureaucratic-memo/text.md new file mode 100644 index 0000000..7d7018f --- /dev/null +++ b/tools/golden/02-bureaucratic-memo/text.md @@ -0,0 +1,9 @@ +В целях повышения эффективности взаимодействия между подразделениями было принято +решение о проведении мероприятий по оптимизации документооборота. + +Ответственными лицами осуществляется контроль за надлежащим исполнением поручений. +В соответствии с утверждённым регламентом информация подлежит рассмотрению в +установленные сроки. + +По итогам проведённой работы будет подготовлен отчёт, который направляется руководству +для принятия соответствующего решения. diff --git a/tools/golden/03-typography/expected.md b/tools/golden/03-typography/expected.md new file mode 100644 index 0000000..0d0c76a --- /dev/null +++ b/tools/golden/03-typography/expected.md @@ -0,0 +1,17 @@ +Домен: typography. Текст специально нарушает разряды правил R1–R89. + +Обязаны найтись: + +- `"Ромашка"` — прямые кавычки вместо ёлочек +- `офисов - в Москве` — дефис вместо тире +- `Спб` — сокращение вместо «СПб» +- `1500000 руб.` — нет разрядов и знака рубля +- `30 %` — пробел перед знаком процента (по R37 пишется слитно) +- `2024...` — три точки вместо многоточия +- `с 9:00 до 18:00` — двоеточие вместо точки в обозначении времени +- `+7(495)123-45-67` — телефон без пробелов +- `No. 5` — латинское No вместо `№` (R85), и после `№` неразрывный пробел (R86) +- `1920x1080` — латинская x вместо знака умножения (R82–R83) +- `20 °C ± 2°` — несогласованная запись единиц + +Не должно найтись: AD-1…AD-9. diff --git a/tools/golden/03-typography/text.md b/tools/golden/03-typography/text.md new file mode 100644 index 0000000..2e249ab --- /dev/null +++ b/tools/golden/03-typography/text.md @@ -0,0 +1,5 @@ +Компания "Ромашка" открыла 5 новых офисов - в Москве, Спб и Казани. +Оборот за 2025 г. составил 1500000 руб., что на 30 % больше, чем в 2024... + +Заказы принимаются с 9:00 до 18:00 по тел. +7(495)123-45-67, отдел No. 5. +Размер экрана 1920x1080, температура хранения 20 °C ± 2°. diff --git a/tools/golden/04-ui-copy/expected.md b/tools/golden/04-ui-copy/expected.md new file mode 100644 index 0000000..af52853 --- /dev/null +++ b/tools/golden/04-ui-copy/expected.md @@ -0,0 +1,13 @@ +Домен: ux-writing. Интерфейсные тексты. + +Обязаны найтись: + +- `Ошибка! Что-то пошло не так.` — не объясняет, что случилось и что делать +- `не может быть обработан` — пассив в интерфейсе +- `в данный момент` — канцелярит («сейчас») +- `Пожалуйста` — избыточная вежливость в системном сообщении +- `попробуйте позже` — нет срока и нет действия +- `Вы уверены, что хотите удалить?` — не назван объект удаления +- `Данные успешно сохранены!` — восклицательный знак в подтверждении, «успешно» избыточно + +Не должно найтись: AD-1…AD-9. diff --git a/tools/golden/04-ui-copy/text.md b/tools/golden/04-ui-copy/text.md new file mode 100644 index 0000000..b6032c6 --- /dev/null +++ b/tools/golden/04-ui-copy/text.md @@ -0,0 +1,7 @@ +Ошибка! Что-то пошло не так. + +Ваш запрос не может быть обработан в данный момент. Пожалуйста, попробуйте позже. + +Вы уверены, что хотите удалить? Это действие невозможно отменить. + +Данные успешно сохранены! diff --git a/tools/golden/05-business-letter/expected.md b/tools/golden/05-business-letter/expected.md new file mode 100644 index 0000000..77128c6 --- /dev/null +++ b/tools/golden/05-business-letter/expected.md @@ -0,0 +1,13 @@ +Домен: business-writing + info-style. + +Обязаны найтись: + +- `Довожу до Вашего сведения` — канцелярит +- `в связи с необходимостью уточнения ряда моментов` — цепочка номинализаций, §B + («в связи с тем что» → «потому что») +- `нам необходимо организовать встречу` — не назван предмет и не предложено время +- `Прошу Вас рассмотреть возможность выделения времени` — просьба через три обёртки +- `Со своей стороны` — канцелярит +- `С уважением и наилучшими пожеланиями` — двойная формула прощания + +Не должно найтись: AD-1…AD-9. diff --git a/tools/golden/05-business-letter/text.md b/tools/golden/05-business-letter/text.md new file mode 100644 index 0000000..c08113a --- /dev/null +++ b/tools/golden/05-business-letter/text.md @@ -0,0 +1,10 @@ +Уважаемый Иван! + +Довожу до Вашего сведения, что в связи с необходимостью уточнения ряда моментов по +проекту, нам необходимо организовать встречу. + +Прошу Вас рассмотреть возможность выделения времени на следующей неделе. Со своей +стороны готовы подстроиться под Ваш график. + +С уважением и наилучшими пожеланиями, +Пётр diff --git a/tools/golden/06-punctuation/expected.md b/tools/golden/06-punctuation/expected.md new file mode 100644 index 0000000..d93de5e --- /dev/null +++ b/tools/golden/06-punctuation/expected.md @@ -0,0 +1,12 @@ +Домен: editorial-punctuation. + +Обязаны найтись: + +- `сказал что придёт` — нет запятой перед изъяснительным союзом +- `документы которые` — нет запятой перед определительным придаточным +- `Несмотря на то что сроки сжатые мы успеем` — не выделен оборот +- `если конечно не будет` — не выделено вводное «конечно» +- `Кроме того необходимо` — не выделено вводное «кроме того» +- `уточнить, детали` — лишняя запятая между сказуемым и дополнением + +Не должно найтись: AD-1…AD-9. diff --git a/tools/golden/06-punctuation/text.md b/tools/golden/06-punctuation/text.md new file mode 100644 index 0000000..80a410a --- /dev/null +++ b/tools/golden/06-punctuation/text.md @@ -0,0 +1,6 @@ +Он сказал что придёт завтра и принесёт документы которые мы обсуждали. + +Несмотря на то что сроки сжатые мы успеем, если конечно не будет форс-мажора. + +Работа выполнена, однако требует доработки в части, касающейся оформления. +Кроме того необходимо уточнить, детали. diff --git a/tools/golden/07-numerals/expected.md b/tools/golden/07-numerals/expected.md new file mode 100644 index 0000000..18596f3 --- /dev/null +++ b/tools/golden/07-numerals/expected.md @@ -0,0 +1,17 @@ +Домен: editorial-grammar. Числительные и согласование. + +Обязаны найтись: + +- `21 человек` — при количественном подсчёте нужен родительный: «21 человек» верно, + а вот `3 женщины и 18 мужчин` рядом — несогласованный ряд, требует проверки +- `Обе решения` — родовая ошибка: «оба решения» +- `в течение полутора суток` — «полутора суток» верно, но «в течение» с сутками требует + проверки формы +- `в 1,5 раза` — при дробном числительном существительное в родительном единственного: + «в 1,5 раза» верно; проверить согласование в соседнем обороте +- `С 22 сотрудниками` — при составном числительном на «два» нужен другой оборот +- `сроком на 1,5 года` — согласование дробного с существительным + +Замечание к случаю: часть строк здесь — пограничные, и это намеренно. Набор проверяет +не только «находит ли», но и «не выдумывает ли»: если проверка пометит верные формы как +ошибки, это регрессия того же класса, что и пропуск. diff --git a/tools/golden/07-numerals/text.md b/tools/golden/07-numerals/text.md new file mode 100644 index 0000000..9c36a38 --- /dev/null +++ b/tools/golden/07-numerals/text.md @@ -0,0 +1,6 @@ +В опросе приняли участие 21 человек, из них 3 женщины и 18 мужчин. + +Обе решения были приняты в течение полутора суток. К 2026 году планируется увеличить +показатель в 1,5 раза по сравнению с 2024 годом. + +С 22 сотрудниками заключены договоры сроком на 1,5 года. diff --git a/tools/golden/08-neuroslop-antithesis-openers/expected.md b/tools/golden/08-neuroslop-antithesis-openers/expected.md new file mode 100644 index 0000000..21a0fb0 --- /dev/null +++ b/tools/golden/08-neuroslop-antithesis-openers/expected.md @@ -0,0 +1,14 @@ +Домен: addenda. Целевые правила — **AD-6** (ложная антитеза) и **AD-9** (пустой зачин). + +Обязаны найтись: + +- `Давайте разберёмся` — AD-9, зачин без содержания, не пошаговый разбор +- `Важно понимать, что` — AD-9, конструкция не несёт информации +- `дело не в скорости, а в предсказуемости` — AD-6, антитеза без антецедента +- `не просто о производительности, а о том` — AD-6.7, «не просто X, а Y» +- `Погрузимся в детали` — AD-9 +- `Вопрос не в том… а в том` — AD-6.7, входит в расширенный набор триггеров +- `суть не в технологии, а в подходе` — AD-6 + +Кластер AD-6 обязан понизить измерение **С — Структура**: по severity это Medium и +единственный сильнейший признак машинной генерации в наборе. diff --git a/tools/golden/08-neuroslop-antithesis-openers/text.md b/tools/golden/08-neuroslop-antithesis-openers/text.md new file mode 100644 index 0000000..7e16898 --- /dev/null +++ b/tools/golden/08-neuroslop-antithesis-openers/text.md @@ -0,0 +1,9 @@ +Давайте разберёмся, как это работает. + +Важно понимать, что дело не в скорости, а в предсказуемости. Речь не просто о +производительности, а о том, как система ведёт себя под нагрузкой. + +Погрузимся в детали. Вопрос не в том, сколько запросов обрабатывается, а в том, +что происходит при отказе. + +Итак, подводя итог: суть не в технологии, а в подходе. diff --git a/tools/golden/09-neuroslop-virtue-assistant/expected.md b/tools/golden/09-neuroslop-virtue-assistant/expected.md new file mode 100644 index 0000000..b2badd6 --- /dev/null +++ b/tools/golden/09-neuroslop-virtue-assistant/expected.md @@ -0,0 +1,20 @@ +Домен: addenda. Целевые правила — **AD-7** (непрошенная оговорка) и **AD-8** +(сервисные реплики ассистента). + +Обязаны найтись: + +- `Отличный вопрос!` — AD-8 +- `Сейчас всё объясню` — AD-8 +- `коротко, без воды и по делу` — AD-7.1, самопохвала о собственной подаче +- `Скажу честно` — **зонд директора**: сейчас корпус ловит это как AD-7, но AD-7.5 + выводит разговорный регистр из-под флага, и служебный регистр ассистента проваливается + в этот карв-аут. Ожидаемое поведение ДО правки — находка может отсутствовать; это + зафиксированный пробел, а не ошибка набора +- `Если честно` — то же самое +- `действительно полезный` — AD-7, объявленная добродетель +- `Надеюсь, это помогло!` — AD-8 +- `обращайтесь` — AD-8, сервисное закрытие + +Случай сознательно фиксирует **известный пробел**: после внедрения нейрослоп-правил +«скажу честно» и «если честно» обязаны находиться, и этот файл — то место, где разница +будет видна. diff --git a/tools/golden/09-neuroslop-virtue-assistant/text.md b/tools/golden/09-neuroslop-virtue-assistant/text.md new file mode 100644 index 0000000..3206684 --- /dev/null +++ b/tools/golden/09-neuroslop-virtue-assistant/text.md @@ -0,0 +1,6 @@ +Отличный вопрос! Сейчас всё объясню — коротко, без воды и по делу. + +Скажу честно: тут есть нюанс. Если честно, большинство статей об этом пишут +поверхностно, но я постараюсь дать действительно полезный разбор. + +Надеюсь, это помогло! Если остались вопросы — обращайтесь. diff --git a/tools/golden/10-neuroslop-parcellation-mismatch/expected.md b/tools/golden/10-neuroslop-parcellation-mismatch/expected.md new file mode 100644 index 0000000..e077e5b --- /dev/null +++ b/tools/golden/10-neuroslop-parcellation-mismatch/expected.md @@ -0,0 +1,13 @@ +Домен: addenda. Целевые правила — **AD-2** (избыточная парцелляция) и **AD-5** +(семантическое несоответствие субъекта и предиката). + +Обязаны найтись: + +- `Быстро. Качественно. Без компромиссов.` — AD-2, три обрубка подряд +- `Всегда.` — AD-2, парцеллят в позиции закрытия +- `Решение принимает ответственность` — AD-5, решение не может принимать ответственность +- `Система понимает потребности` — AD-5, антропоморфизм +- `стремится их закрыть` — AD-5 +- `Продукт знает, чего вы хотите` — AD-5 +- `Опыт подсказывает` — AD-5, пограничный случай: устойчивая метафора; если помечен, + проверить, не сработал ли карв-аут diff --git a/tools/golden/10-neuroslop-parcellation-mismatch/text.md b/tools/golden/10-neuroslop-parcellation-mismatch/text.md new file mode 100644 index 0000000..9ba0c95 --- /dev/null +++ b/tools/golden/10-neuroslop-parcellation-mismatch/text.md @@ -0,0 +1,6 @@ +Мы сделали это. Быстро. Качественно. Без компромиссов. + +Решение принимает ответственность за результат. Система понимает потребности +пользователя и стремится их закрыть. + +Продукт знает, чего вы хотите. Опыт подсказывает верное направление. Всегда. diff --git a/tools/golden/11-neuroslop-dashes-explain-rebut/expected.md b/tools/golden/11-neuroslop-dashes-explain-rebut/expected.md new file mode 100644 index 0000000..3c3bad0 --- /dev/null +++ b/tools/golden/11-neuroslop-dashes-explain-rebut/expected.md @@ -0,0 +1,17 @@ +Домен: addenda. Целевые правила — **AD-1** (избыточные тире), **AD-3** +(разжёвывание очевидного), **AD-4** (возражение без предпосылок). Три правила, которых +в наборе не было до v2.0: их карв-ауты никакой гейт не защищал. + +Обязаны найтись: + +- три тире в одном абзаце подряд — AD-1.1 (предел 1–2 на абзац) и AD-1.3 + (последовательные предложения с тире = монотонный узор) +- `Скорость — не главное` / `Главное — стабильность` — AD-1.2, тире вместо более + точного знака +- `То есть десять тысяч обращений` — AD-3.1 и AD-3.2, повтор числа словами +- `Другими словами, очень много` — AD-3.3, маркер-предупреждение, за которым нет + новой информации +- `Но на самом деле` — AD-4, антецедента нет +- `Однако на практике` — AD-4 +- `Только вот` — AD-4 +- `а это уже серьёзно` — AD-4, из списка триггеров diff --git a/tools/golden/11-neuroslop-dashes-explain-rebut/text.md b/tools/golden/11-neuroslop-dashes-explain-rebut/text.md new file mode 100644 index 0000000..04411c9 --- /dev/null +++ b/tools/golden/11-neuroslop-dashes-explain-rebut/text.md @@ -0,0 +1,7 @@ +Скорость — не главное. Главное — стабильность. Всё остальное — детали. + +Мы обрабатываем 10 000 запросов в секунду. То есть десять тысяч обращений каждую +секунду. Другими словами, очень много. + +Но на самом деле важно не это. Однако на практике всё оказывается сложнее. Только вот +никто об этом не говорит — а это уже серьёзно. diff --git a/tools/golden/12-clean-control/expected.md b/tools/golden/12-clean-control/expected.md new file mode 100644 index 0000000..5ecfd5d --- /dev/null +++ b/tools/golden/12-clean-control/expected.md @@ -0,0 +1,24 @@ +**Контрольный текст. Находок обязано быть ноль — и до правок, и после.** + +Текст написан по правилам корпуса: конкретные числа вместо оценок, короткие предложения +без парцелляции, одно тире на абзац, никаких зачинов и сервисных реплик, действительный +залог. Типографика полная: неразрывные пробелы после однобуквенных предлогов (R30), между +числом и единицей (R36) и перед длинным тире (R16, R44) — их **семь**, и считать их надо +командой, а не глазом: `python3 -c "import io;print(io.open('text.md',encoding='utf-8').read().count(chr(0xA0)))"`. + +Если проверка что-то здесь находит — это ложное срабатывание, и оно дороже пропуска: +пользователь перестаёт верить инструменту. + +**На что этот текст молча опирается** (записано после того, как правка 2026 года добавила +семь правил и вместе с ними семь новых способов сработать на честном тексте): + +- `на трёх площадках: Москва, Франкфурт, Сингапур` — карв-аут **AD-11.4**: тройка + продиктована предметом, у каждого члена своё измерение. Потеряется исключение — контроль + начнёт находить «обязательную триаду» в перечне из трёх городов +- `Медиана — 12 мс`, `Если ответ не приходит за 2 с` — числа названы, а не заменены + заполнителями: карв-ауты **AD-12.3** и **AD-13** не нужны, потому что заполнителей нет + вовсе. Это и есть образец того, чего требует AD-12 + +**Историческая заметка.** До 27.07.2026 этот файл требовал нуля находок, а сам текст был +набран обычными пробелами — и первый же прогон дал семь находок по типографике. Контроль, +который никто не прогонял, ничего не контролирует. diff --git a/tools/golden/12-clean-control/text.md b/tools/golden/12-clean-control/text.md new file mode 100644 index 0000000..ba042e5 --- /dev/null +++ b/tools/golden/12-clean-control/text.md @@ -0,0 +1,7 @@ +Сервис отвечает за 40 мс на 95-м процентиле. Медиана — 12 мс. + +Мы измеряли на трёх площадках: Москва, Франкфурт, Сингапур. В Сингапуре задержка выше +на 60 мс из-за физического расстояния до дата-центра. + +Если ответ не приходит за 2 с, клиент повторяет запрос один раз, затем возвращает +ошибку. Логи повторов лежат в `retry.log`. diff --git a/tools/golden/13-carveouts-control/expected.md b/tools/golden/13-carveouts-control/expected.md new file mode 100644 index 0000000..6a6ed51 --- /dev/null +++ b/tools/golden/13-carveouts-control/expected.md @@ -0,0 +1,15 @@ +**Контрольный текст на карв-ауты. Находок обязано быть ноль — и до правок, и после.** + +Каждая конструкция здесь **явно выведена из-под флага** соответствующим подправилом: + +- `— Отличный вопрос, — сказал Иван` — AD-8.4, прямая речь и голос персонажа +- `Честно говоря` в реплике персонажа — AD-7.5, разговорный и литературный регистр +- `Давайте разберём по шагам` с настоящим пошаговым разбором — AD-9.5 +- `Важно понимать разницу между TCP и UDP` — AD-9.6, информативное «важно»: за ним + идёт содержание, а не пустота +- `Итак, мы прошли все три шага` — AD-9.4, настоящий суммирующий связник, замыкающий + цепочку рассуждения + +**Это самый ценный текст набора.** Он ловит не пропуск, а переусердствование: если правка +корпуса потеряет исключение, набор начнёт находить ошибки в честном тексте. Такую +регрессию не видит ни один автотест, и именно она разрушает доверие к инструменту. diff --git a/tools/golden/13-carveouts-control/text.md b/tools/golden/13-carveouts-control/text.md new file mode 100644 index 0000000..38e752a --- /dev/null +++ b/tools/golden/13-carveouts-control/text.md @@ -0,0 +1,8 @@ +— Отличный вопрос, — сказал Иван. — Честно говоря, я сам долго путался. + +Давайте разберём по шагам. Сначала открываем конфиг, затем меняем порт, затем +перезапускаем сервис. На третьем шаге легко ошибиться, поэтому смотрим внимательно. + +Важно понимать разницу между TCP и UDP — от неё зависит выбор транспорта. + +Итак, мы прошли все три шага и получили рабочую конфигурацию. diff --git a/tools/golden/14-neuroslop-sincerity-tricolon/expected.md b/tools/golden/14-neuroslop-sincerity-tricolon/expected.md new file mode 100644 index 0000000..5d0fdd3 --- /dev/null +++ b/tools/golden/14-neuroslop-sincerity-tricolon/expected.md @@ -0,0 +1,24 @@ +Домен: addenda. Целевые правила — **AD-10** (объявленная искренность) и **AD-11** +(обязательная триада). Оба вводятся нейрослоп-правкой 2026; до неё случай измеряет, +насколько корпус ловит эти конструкции чужими правилами. + +Обязаны найтись ПОСЛЕ правки: + +- `Честный разбор` — AD-10, искренность объявлена свойством самого текста, в заголовке +- `Давайте будем честны` — AD-10, рамка честности, наброшенная на то, что идёт следом +- `Рассказываю как есть` — AD-10 +- `без прикрас` — AD-10 либо AD-7: один фрагмент, один зачёт (AD-10.6) +- `прозрачным, предсказуемым и управляемым` — AD-11, тройка не продиктована предметом +- `быстро, надёжно и красиво` — тройка о собственном методе: зачёт **один**, по AD-7 + (самопохвала подаче), не по AD-11; правило AD-11.5 разводит эти два дома +- `инновационным, трансформирующим и по-настоящему прорывным` — AD-11 плюс три оценки + без доказательств из каталога info-style §B: зачёт один (AD-11.6) +- `к планированию, к оценке и к контролю` — AD-11, третья тройка подряд; плотность и + есть главный сигнал правила + +Не должно найтись: AD-6 — антитезы в тексте нет, и «не X, а Y» не должно +дорисовываться в конструкции, которых там нет. + +Замечание к случаю: три тройки подряд стоят здесь намеренно. Одна тройка — слабый +сигнал, и правило это прямо говорит; случай проверяет, что срабатывает именно +плотность, а не первое попавшееся перечисление. diff --git a/tools/golden/14-neuroslop-sincerity-tricolon/text.md b/tools/golden/14-neuroslop-sincerity-tricolon/text.md new file mode 100644 index 0000000..7dadb46 --- /dev/null +++ b/tools/golden/14-neuroslop-sincerity-tricolon/text.md @@ -0,0 +1,10 @@ +Честный разбор: почему сроки в разработке всё время срываются + +Давайте будем честны — планировать умеет не каждая команда. Рассказываю как есть, +без прикрас. + +Хороший процесс должен быть прозрачным, предсказуемым и управляемым. Мы строим +такие процессы быстро, надёжно и красиво. + +Новый инструмент получился инновационным, трансформирующим и по-настоящему +прорывным. Он меняет подход к планированию, к оценке и к контролю. diff --git a/tools/golden/15-neuroslop-hollow-attribution/expected.md b/tools/golden/15-neuroslop-hollow-attribution/expected.md new file mode 100644 index 0000000..0be4472 --- /dev/null +++ b/tools/golden/15-neuroslop-hollow-attribution/expected.md @@ -0,0 +1,27 @@ +Домен: addenda. Целевые правила — **AD-12** (выхолощенность) и **AD-13** (неопределённая +атрибуция). Оба вводятся нейрослоп-правкой 2026. + +Обязаны найтись ПОСЛЕ правки: + +- `Исследования показывают` — AD-13, ссылка на источник, которого нет +- `Эксперты отмечают` — AD-13 +- `Специалисты рекомендуют` — AD-13 +- `Учёные доказали` — AD-13, самая сильная форма: доказательство приписано без работы +- `играет ключевую роль` — AD-13, ранг назначен, но ранжирует некому и не по чему +- `различных факторов` — AD-12, механизм заменён заполнителем +- `целый ряд особенностей` — AD-12 +- `специфики коммуникации` — AD-12, «специфика» вместо названного отличия +- `Многие аспекты` — AD-12 +- `те или иные подходы` — AD-12 +- `определённые условия` — уже в каталоге info-style §B («определённый»): зачёт один, + по §B, не по AD-12 (правило AD-12.5) +- `разный эффект` — AD-12, эффект назван, величина и направление нет + +Не должно найтись: AD-6, AD-7, AD-8, AD-9 — ни антитезы, ни самопохвалы, ни сервисных +реплик, ни пустых зачинов здесь нет. Текст плох ровно двумя способами, и правки не +должны находить в нём третий. + +Замечание к случаю: этот текст — самая близкая к честному письму подделка в наборе. +Каждая фраза грамматически безупречна и звучит как научно-популярная статья. Именно +поэтому он ценен: если проверка не отличит его от текста с настоящими ссылками, оба +правила бесполезны. diff --git a/tools/golden/15-neuroslop-hollow-attribution/text.md b/tools/golden/15-neuroslop-hollow-attribution/text.md new file mode 100644 index 0000000..343410f --- /dev/null +++ b/tools/golden/15-neuroslop-hollow-attribution/text.md @@ -0,0 +1,9 @@ +Исследования показывают, что удалённая работа влияет на продуктивность команд. +Эксперты отмечают: результат зависит от различных факторов. + +Специалисты рекомендуют учитывать целый ряд особенностей — от организации рабочего +места до специфики коммуникации в распределённых командах. Многие аспекты этого +процесса до сих пор изучены недостаточно. + +Мотивация играет ключевую роль. Учёные доказали, что определённые условия труда +повышают вовлечённость, а те или иные подходы к обратной связи дают разный эффект. diff --git a/tools/golden/16-neuroslop-chat-transcript/expected.md b/tools/golden/16-neuroslop-chat-transcript/expected.md new file mode 100644 index 0000000..023e64a --- /dev/null +++ b/tools/golden/16-neuroslop-chat-transcript/expected.md @@ -0,0 +1,26 @@ +Домен: addenda. Целевое правило — **AD-14** (диалог с нейросетью как готовый текст). +Вводится нейрослоп-правкой 2026, тяжесть High, и это **одно из двух правил набора, +которые предъявляются документу целиком, а не фрагменту** (второе — AD-15). + +Обязаны найтись ПОСЛЕ правки: + +- AD-14 — **на весь документ**: скелет текста есть переписка, а не рассуждение. Реплики + чередуются «Я / Модель», реплики читателя воспроизведены дословно, вывода, ради + которого стоило публиковать, нет +- `Отличный вопрос!` — AD-8, отдельная реплика +- `Давайте разберёмся по порядку` — AD-9, пустой зачин +- `Надеюсь, это помогло!` — AD-8 +- `Рад был помочь!` — AD-8 +- `Если остались вопросы — обращайтесь.` — AD-8, сервисное закрытие без реального канала + +Замечание о двойном зачёте: находка AD-14 и находки AD-8 **не являются** двойным +зачётом одного фрагмента. AD-14 предъявляется устройству документа, AD-8 — четырём +отдельным репликам внутри него. Правило AD-14.5 говорит это прямо: убери все реплики +ассистента — документ останется расшифровкой переписки, то есть дефект AD-14 переживёт +исправление всех дефектов AD-8. + +Не должно найтись: AD-6. + +Замечание к случаю: содержание ответов здесь **верное** — ETag, `If-None-Match`, 304 +описаны правильно. Так и задумано. Правило запрещает не ошибку, а форму: расшифровка +переписки опубликована вместо текста, и фактическая правота ответов её не спасает. diff --git a/tools/golden/16-neuroslop-chat-transcript/text.md b/tools/golden/16-neuroslop-chat-transcript/text.md new file mode 100644 index 0000000..af12aa8 --- /dev/null +++ b/tools/golden/16-neuroslop-chat-transcript/text.md @@ -0,0 +1,16 @@ +Разбираемся с HTTP-кешированием: мой диалог с нейросетью + +**Я:** Объясни, как работает HTTP-кеширование. + +**Модель:** Отличный вопрос! HTTP-кеширование — это механизм, который позволяет +браузеру не запрашивать один и тот же ресурс дважды. Давайте разберёмся по порядку. + +**Я:** А что такое ETag? + +**Модель:** ETag — идентификатор версии ресурса. Сервер присылает его в заголовке, +браузер возвращает в `If-None-Match`, и если версия совпала, приходит 304. Надеюсь, +это помогло! + +**Я:** Спасибо, теперь понятно. + +**Модель:** Рад был помочь! Если остались вопросы — обращайтесь. diff --git a/tools/golden/17-neuroslop-search-addressee/expected.md b/tools/golden/17-neuroslop-search-addressee/expected.md new file mode 100644 index 0000000..38db85d --- /dev/null +++ b/tools/golden/17-neuroslop-search-addressee/expected.md @@ -0,0 +1,29 @@ +Домен: addenda + info-style. Целевое правило — **AD-15** (адресат — поисковик, а не +читатель). Вводится нейрослоп-правкой 2026, тяжесть High, измерение **Ц — Точность для +читателя**. + +Обязаны найтись ПОСЛЕ правки: + +- AD-15 — **на весь документ**: подразумеваемый адресат текста — поисковая машина. + Подписи разделов повторяют формулировки запросов; каждый раздел отвечает не на + следующий вопрос, а на очередной вариант того же +- `купить кофемашину в Москве` — точное вхождение повторено четыре раза, в том числе + там, где хватило бы местоимения +- `от типа кофемашины, от производителя кофемашины` — предмет назван полной именной + группой там, где второй раз естественно сказать «от производителя» +- `Как выбрать кофемашину для дома: выводы` — раздел выводов пересказывает подписи + предыдущих разделов и не добавляет ни числа, ни рекомендации +- `недорого` — оценка без доказательств, каталог info-style §B: цена не названа +- отсутствие полезного действия целиком: после текста читатель не может выбрать + кофемашину — ни одной модели, ни одной цены, ни одного критерия сравнения + +Не должно найтись: + +- `рожковые, капсульные и автоматические` — **НЕ** AD-11. Тройка продиктована + предметом: это исчерпывающий перечень типов, а не риторическая тройка. Если проверка + пометит его, карв-аут AD-11.4 потерян +- AD-6, AD-7, AD-8 + +Замечание к случаю: текст грамотен и типографически чист. Единственный его дефект — +адресат. Случай проверяет, что правило ловит именно это, а не цепляется за плохую +грамматику, которой здесь нет. diff --git a/tools/golden/17-neuroslop-search-addressee/text.md b/tools/golden/17-neuroslop-search-addressee/text.md new file mode 100644 index 0000000..3fb6a6d --- /dev/null +++ b/tools/golden/17-neuroslop-search-addressee/text.md @@ -0,0 +1,19 @@ +Купить кофемашину в Москве: как выбрать кофемашину для дома + +Купить кофемашину в Москве можно в интернет-магазинах и в розничных точках. Если вы +решили купить кофемашину в Москве, обратите внимание на тип кофемашины. + +## Какие бывают кофемашины + +Кофемашины бывают рожковые, капсульные и автоматические. Рожковые кофемашины +подойдут тем, кто хочет купить кофемашину для дома недорого. + +## Сколько стоит кофемашина в Москве + +Цена кофемашины зависит от типа кофемашины, от производителя кофемашины и от того, +где вы решили купить кофемашину в Москве. + +## Как выбрать кофемашину для дома: выводы + +Выбрать кофемашину для дома несложно, если знать, какие бывают кофемашины и сколько +стоит кофемашина в Москве. diff --git a/tools/golden/18-neuroslop-additive-pair/expected.md b/tools/golden/18-neuroslop-additive-pair/expected.md new file mode 100644 index 0000000..f538b3e --- /dev/null +++ b/tools/golden/18-neuroslop-additive-pair/expected.md @@ -0,0 +1,23 @@ +Домен: addenda. Целевое правило — **AD-16** («не только X, но и Y» — аддитивная +псевдопара). Вводится нейрослоп-правкой 2026, тяжесть Low. + +Обязаны найтись ПОСЛЕ правки: + +- `не только удобный, но и комфортный` — AD-16, полюса синонимичны +- `не только экономить время, но и тратить его меньше` — AD-16, второй полюс есть + пересказ первого другими словами +- `как качественные решения, так и решения высокого уровня` — AD-16 в форме «как X, так + и Y»; плюс две оценки без доказательств (info-style §B) — зачёт один, по AD-16.6 +- `не только интерфейс, но и внешний вид` — AD-16, второй полюс поглощён первым +- `и быстрее, и оперативнее` — AD-16 в форме «и X, и Y», полюса синонимичны + +Не должно найтись: + +- AD-6 ни в одном месте. Соседство близкое и путаница вероятна: там противопоставление + без антецедента, здесь сложение без содержания. Если пары помечены как AD-6, граница + между правилами не работает +- AD-11: пар здесь пять, а троек ни одной + +Замечание к случаю: пять пар на пятьдесят слов — плотность выше любого реального +текста. Так и задумано: правило Low, одиночная пара его не поднимает, и случай обязан +показать, что срабатывает набор, а не единичное вхождение. diff --git a/tools/golden/18-neuroslop-additive-pair/text.md b/tools/golden/18-neuroslop-additive-pair/text.md new file mode 100644 index 0000000..a28a929 --- /dev/null +++ b/tools/golden/18-neuroslop-additive-pair/text.md @@ -0,0 +1,7 @@ +Сервис не только удобный, но и комфортный в использовании. + +Платформа помогает не только экономить время, но и тратить его меньше. Мы предлагаем +как качественные решения, так и решения высокого уровня. + +Обновление затрагивает не только интерфейс, но и внешний вид приложения. Оно делает +работу и быстрее, и оперативнее. diff --git a/tools/golden/19-carveouts-v2/expected.md b/tools/golden/19-carveouts-v2/expected.md new file mode 100644 index 0000000..4cc40ad --- /dev/null +++ b/tools/golden/19-carveouts-v2/expected.md @@ -0,0 +1,39 @@ +**Контрольный текст на карв-ауты нейрослоп-правил 2026. Находок обязано быть ноль — и до +правки, и после.** + +Второй контрольный текст набора. Случай 13 защищает исключения правил AD-1…AD-9; этот — +исключения семи правил, добавленных в 2026 году. Разделены намеренно: когда контроль +начнёт срабатывать, по номеру случая сразу видно, чьё исключение потеряно. + +Каждая конструкция здесь **явно выведена из-под флага** соответствующим подправилом: + +- `— Скажу честно, я не знаю, — ответил инженер` — AD-10.4: искренность принадлежит + говорящему внутри текста, а не автору о собственном тексте. Это же различение — + сужённый AD-7.5 +- `По данным Росстата за 2025 год` — AD-13.3: атрибуция названа, проверяема и датирована +- `три вещи: расстояние до дата-центра, загрузка канала и версия протокола` — AD-11.4 + (тройка продиктована предметом, полюса различны) и AD-12.3 (механизм назван поимённо, + а не заменён «различными факторами») +- `не только в Москве, но и в Новосибирске: 12 мс и 47 мс` — AD-16.3: оба полюса несут + свои числа, удаление любого теряет факт +- `Ниже — расшифровка эксперимента… Привожу переписку целиком: без неё вывод о разбросе + никто не перепроверит` — AD-14.4: переписка приведена как улика к + собственному выводу автора, а не вместо текста +- `Поле updated_at меняется при каждой записи, поле created_at — нет` — AD-15.4: имя + повторено потому, что местоимение здесь двусмысленно, а не ради поисковой машины + +**Почему этот текст дороже остальных.** Он ловит не пропуск, а переусердствование. Семь +новых правил — это семь новых способов сработать на честном тексте, и цена ложного +срабатывания выше цены пропуска: пропущенную находку пользователь не заметит, а +ошибочную увидит сразу и перестанет верить всему остальному. Такую регрессию не покажет +ни один автотест. + +Заодно текст проверяет, что старые правила не сработали на новом материале: прямая речь +не помечена как AD-8, «Ниже — расшифровка» не помечено как AD-9, тире в четырёх местах +не набрало AD-1. + +**Поправка 28.07.2026.** Определяющий прогон дал здесь одну находку вместо нуля, и она была +верной: «Переписка приведена целиком» — пассив при известном исполнителе, названном +предложением раньше. Дефект был в контрольном тексте, а не в карв-аутах. Тот же класс, что +неразрывные пробелы в случаях 12 и 13 и стоп-слова в случае 22: текст, проверяющий одни +правила, обязан быть чист по всем остальным. diff --git a/tools/golden/19-carveouts-v2/text.md b/tools/golden/19-carveouts-v2/text.md new file mode 100644 index 0000000..b34c263 --- /dev/null +++ b/tools/golden/19-carveouts-v2/text.md @@ -0,0 +1,13 @@ +— Скажу честно, я не знаю, — ответил инженер. — Данных за квартал пока нет. + +По данным Росстата за 2025 год, средняя зарплата в отрасли выросла на 12%. Задержку +определяют три вещи: расстояние до дата-центра, загрузка канала и версия протокола. + +Мы измеряли не только в Москве, но и в Новосибирске: 12 мс и 47 мс. + +Ниже — расшифровка эксперимента: я задал модели один и тот же вопрос трижды +и свёл ответы в таблицу. Привожу переписку целиком: без неё вывод о разбросе +никто не перепроверит. + +В ответе два поля: `created_at` и `updated_at`. Поле `updated_at` меняется при каждой +записи, поле `created_at` — нет. diff --git a/tools/golden/20-grammar-government/expected.md b/tools/golden/20-grammar-government/expected.md new file mode 100644 index 0000000..f0607d4 --- /dev/null +++ b/tools/golden/20-grammar-government/expected.md @@ -0,0 +1,23 @@ +Домен: editorial-grammar §I. Целевые правила — **§I.1** (управление закрытым списком) и +**§I.2** (разное управление при однородных). Раздел введён в v2.0; до него правил об +управлении в корпусе не было вовсе — только о согласовании (§D). + +Обязаны найтись: + +- `Согласно приказа` — §I.1, дательный: «согласно приказу» +- `Благодаря поддержки` — §I.1, дательный: «благодаря поддержке» +- `Заведующий отдела` — §I.1, творительный: «заведующий отделом» +- `оплатить за проезд` — §I.1, «оплатить» без предлога: «оплатить проезд» +- `Вопреки ожиданий` — §I.1, дательный: «вопреки ожиданиям» +- `преимущество над конкурентами` — §I.1, «преимущество перед» +- `По приезду в город` — §I.1, предложный: «по приезде» +- `организовать и руководить работой` — §I.2: два глагола на одном дополнении с разным + управлением. Правка: «организовать работу и руководить ею» + +Не должно найтись: AD-6…AD-16. Текст плох ровно управлением, и правки нейрослоп-правил не +должны находить в нём машинных признаков. + +**Замечание о границе.** Раздел §I намеренно закрыт списком. Если проверка пометит здесь +конструкцию, которой в списке нет, — это не находчивость, а нарушение §I.1.1: открытая +форма проверки падежей есть детектор неносительского русского, а его строить запрещено +решением директора. diff --git a/tools/golden/20-grammar-government/text.md b/tools/golden/20-grammar-government/text.md new file mode 100644 index 0000000..5bbc482 --- /dev/null +++ b/tools/golden/20-grammar-government/text.md @@ -0,0 +1,8 @@ +Согласно приказа от 12 марта работы приостановлены. Благодаря поддержки партнёров +проект запустился в срок. + +Заведующий отдела просит оплатить за проезд до конца недели. Вопреки ожиданий бюджет +не вырос, а преимущество над конкурентами сохранилось. + +Нам нужно организовать и руководить работой подрядчика. По приезду в город свяжитесь +с координатором. diff --git a/tools/golden/21-grammar-gerunds-homophones/expected.md b/tools/golden/21-grammar-gerunds-homophones/expected.md new file mode 100644 index 0000000..ca7348e --- /dev/null +++ b/tools/golden/21-grammar-gerunds-homophones/expected.md @@ -0,0 +1,22 @@ +Домен: editorial-grammar §J и §K. Целевые правила — **§J.1** (деепричастие с чужим +субъектом) и **§K** (контекстные омофоны). Оба раздела введены в v2.0. + +Обязаны найтись: + +- `Подъезжая к станции, у меня слетела шляпа` — §J.1: деепричастие выполняет «я», подлежащее + главной части — «шляпа». Правка: «Когда я подъезжал к станции…» +- `Прочитав отчёт, ему стало ясно` — §J.1: у безличной части нет подлежащего, а деепричастие + требует деятеля. Карв-аут §J.1.3 **не применим**: там безличное с инфинитивом («следует + смотреть»), здесь безличное без инфинитива +- `Открыв файл, была обнаружена ошибка` — §J.1: пассив прячет деятеля +- `В течении года` — §K: отрезок времени → «в течение года» +- `Что бы успеть к релизу` — §K: «бы» неотделимо → «чтобы» +- `Не смотря на это` — §K: значение «хотя» → «несмотря на» +- `имели ввиду` — §K: устойчивое сочетание пишется тремя словами, «иметь в виду» + +Не должно найтись: AD-6…AD-16. + +**Замечание о механичности.** §J.1 держится на тесте, не требующем суждения: назвать +исполнителя деепричастия и подлежащее главной части и сравнить. Именно поэтому правило +допущено в измерение **Г**, объявленное объективным. Если проверка обосновывает находку +стилем, а не тождеством субъектов, — правило применено не так, как написано. diff --git a/tools/golden/21-grammar-gerunds-homophones/text.md b/tools/golden/21-grammar-gerunds-homophones/text.md new file mode 100644 index 0000000..1e5b78c --- /dev/null +++ b/tools/golden/21-grammar-gerunds-homophones/text.md @@ -0,0 +1,8 @@ +Подъезжая к станции, у меня слетела шляпа. Прочитав отчёт, ему стало ясно, что сроки +сорваны. + +В течении года мы выпустили три версии. Что бы успеть к релизу, команда работала +по выходным. + +Открыв файл, была обнаружена ошибка кодировки. Не смотря на это, релиз состоялся +в срок, и мы имели ввиду именно такой исход. diff --git a/tools/golden/22-carveouts-v3/expected.md b/tools/golden/22-carveouts-v3/expected.md new file mode 100644 index 0000000..3af3c2c --- /dev/null +++ b/tools/golden/22-carveouts-v3/expected.md @@ -0,0 +1,56 @@ +**Контрольный текст на карв-ауты грамматического пакета v2.0. Находок обязано быть ноль.** + +Третий контрольный текст набора. Случай 13 держит исключения AD-1…AD-9, случай 19 — +исключения нейрослоп-правил 2026, этот — исключения разделов §I, §J и §K. Разделены +намеренно: когда контроль начнёт срабатывать, по номеру случая сразу видно, чьё исключение +потеряно. + +Каждая конструкция здесь **явно выведена из-под флага**: + +- `Открыв счёт, клиенту предстоит подтвердить личность`, `Готовя отчёт, аналитик успел + сверить три источника` — §J.1.3: деятель деепричастия совпадает с деятелем инфинитива +- `Открыв файл, проверьте кодировку` — §J.1.5: повелительное наклонение, деятель у обоих + один и тот же — «вы» +- `Судя по отчёту`, `исходя из данных`, `Несмотря на возражения` — §J.1.4: формы, ставшие + предлогами; тест на субъект к ним не применяется +- `заплатил за проезд` — §I.1: платя ЗА что-то, говорят «заплатить за». Строка стоит в + списке именно затем, чтобы верную форму не «исправили» +- `Рад встрече`, `Удивляюсь результату` — §I.1: дательный после «рад» и после «удивляться» + верен; обе строки внесены в список как правильные +- `В течении реки` — §K: течение реки, а не отрезок времени +- `Что бы ни случилось` — §K: «бы» принадлежит «что», пишется раздельно +- `Он также пришёл` и `мы так же обсудили` — §K: обе формы верны и стоят рядом намеренно +- `Иметь в виду` — §K: устойчивое сочетание в правильном написании +- `Читать и перечитывать книгу` — §I.2.3: оба глагола требуют одного падежа, разнобоя нет + +Дописано после гейт-панели 28.07.2026, которая показала: первая редакция этого текста +состояла из **дословных иллюстраций самих правил**, то есть проверяла узнавание, а не +обобщение. Ни одно из четырёх ложных срабатываний, найденных панелью, набором не подавалось. +Теперь подаются: + +- `Он работал молча` — §J.1.6: деепричастие, ставшее наречием образа действия. Оборота нет, + запятой не требует, сравнивать деятелей не с чем +- `Просим клиентов, оплачивая счёт, указывать номер договора` — §J.1.3: деятель деепричастия + совпадает с деятелем инфинитива («клиенты»), хотя главная часть определённо-личная. Это + тот случай, ради которого критерий переписан с «подлежащего» на «производителя действия» +- `Открыв файл, давайте проверим кодировку` — §J.1.5: побудительное с «давайте», подлежащего + нет, деятель назван окончанием +- `К работе приступают начиная с понедельника` — §J.1.4: «начиная» опускается без потери + смысла, значит здесь это предлог, а не деепричастие +- `В течении болезни наметился перелом` — §K: «течение» существительным в предложном — это не + только река. Первая редакция правила утверждала обратное, и панель это поймала +- `Эскадра шла в виду берега` — §K: раздельное «в виду» существует вне «иметь в виду» и + значит «в поле зрения». Правка на «ввиду» дала бы «по причине берега» + +**Почему этот текст дороже остальных двух.** Раздел §I закрыт списком, §J держится на +механическом тесте, §K — на десяти парах. Все три легко превратить в придирку: проверка, +которая «исправит» «заплатить за проезд» или «рад встрече», не найдёт ошибку, а выдумает +её — в измерении, объявленном объективным, где один спорный вердикт стоит дороже десяти +пропусков. + +**Вторая поправка, 28.07.2026.** Первый прогон расширенного текста дал две находки вместо +нуля — и обе оказались верными: примеры карв-аутов были написаны словами «следует», «нужно» +и «можно», а это стоп-слова каталога `info-style.md` §B. Контроль на карв-ауты §I/§J/§K сам +нарушал инфостиль. Тот же класс дефекта, что неразрывные пробелы в случаях 12 и 13: текст, +проверяющий одно правило, обязан быть чист по всем остальным, иначе ноль недостижим и +критерий не работает. diff --git a/tools/golden/22-carveouts-v3/text.md b/tools/golden/22-carveouts-v3/text.md new file mode 100644 index 0000000..0474a9e --- /dev/null +++ b/tools/golden/22-carveouts-v3/text.md @@ -0,0 +1,18 @@ +Открыв счёт, клиенту предстоит подтвердить личность. Готовя отчёт, аналитик успел +сверить три источника. + +Открыв файл, проверьте кодировку. Судя по отчёту, сроки реальны; исходя из данных, +релиз сдвинут на неделю. Несмотря на возражения, решение принято. + +Пассажир заплатил за проезд картой. Рад встрече. Удивляюсь результату: он выше прогноза +на 12%. + +В течении реки два порога. Что бы ни случилось, релиз состоится. Он также пришёл, +и мы так же обсудили сроки. Иметь в виду стоит только измеренное. + +Читать и перечитывать книгу — разные занятия. + +Он работал молча. Просим клиентов, оплачивая счёт, указывать номер договора. +Открыв файл, давайте проверим кодировку. К работе приступают начиная с понедельника. + +В течении болезни наметился перелом. Эскадра шла в виду берега. diff --git a/tools/golden/README.md b/tools/golden/README.md new file mode 100644 index 0000000..41fc20b --- /dev/null +++ b/tools/golden/README.md @@ -0,0 +1,53 @@ +# Golden-set — измеритель качества покрытия + +Курируемые тексты с заранее выписанными ожидаемыми находками. Не автотест: прогон делает +модель через `/ru-text:ru-check` и `/ru-text:ru-score`, транскрипты кладутся в PR. + +Прогон золотого набора — всегда полная глубина: это явный вызов, а не самостоятельный, и triage-режим здесь не существует. Иначе набор мерил бы более узкую проверку, чем та, на которую написаны ожидания. +Автоматического харнесса здесь нет и не будет — имитировать детерминированный прогон +LLM-проверки значило бы измерять имитацию. + +## Зачем + +До golden-set любая правка корпуса делалась вслепую: тесты зелёные, поведение сломано — +классический провал. Набор отвечает на один вопрос, который иначе не проверить: **находит +ли проверка то, что обещает, и не начала ли она находить лишнее.** + +## Критерии приёмки + +- Находки ПОСЛЕ правки ⊇ находок ДО на каждом тексте. Одна пропавшая — провал. +- На контрольных текстах (`clean`, `carveouts`) находок ноль и до, и после. Текст + `carveouts` набран из конструкций, которые правила **явно выводят из-под флага**: если + правка потеряет исключение, он начнёт срабатывать на честном тексте. +- Оценка `/ru-score` не уходит больше чем на 0,5 и не меняет словесную категорию. + +## Раскладка + +Каждый случай — каталог: `text.md` — что подаётся на вход, `expected.md` — что обязано +найтись, с указанием правила и цитаты фрагмента. + +## Прогон корпуса v2.0, 28.07.2026 + +Все 22 текста, вслепую к `expected.md`. Транскрипты — `~/.claude/plans/ru-text-v2/golden-final/`. + +| случай | находки | итог | | случай | находки | итог | +|---|---|---|---|---|---|---| +| 01 лендинг | 23 | 5,3 | | 12 **контроль** | **0** | 9,5 | +| 02 записка | 24 | 5,1 | | 13 **контроль** | **0** | 9,5 | +| 03 типографика | 14 | 6,8 | | 14 искренность | 9 | 5,8 | +| 04 интерфейс | 12 | 7,0 | | 15 выхолощенность | 10 | 6,4 | +| 05 письмо | 8 | 5,3 | | 16 расшифровка | 6 | 6,7 | +| 06 пунктуация | 12 | 5,7 | | 17 поисковик | 6 | 6,7 | +| 07 числительные | 15 | 6,0 | | 18 псевдопары | 9 | 5,0 | +| 08 антитезы | 11 | 5,3 | | 19 **контроль** | **0** | 9,3 | +| 09 добродетель | 15 | 5,5 | | 20 управление | 11 | 6,5 | +| 10 парцелляция | 10 | 5,0 | | 21 обороты | 8 | 5,0 | +| 11 тире | 18 | 5,9 | | 22 **контроль** | **0** | 8,5 | + +**Четыре контроля из четырёх — ноль.** Это главный результат набора: шестнадцать +нейрослоп-правил и три грамматических раздела не сработали ни на одном честном фрагменте. + +Три из четырёх контролей до этого краснели, и **каждый раз был неправ сам текст**: в 12 и 13 +не было ни одного неразрывного пробела, в 22 примеры карв-аутов были написаны стоп-словами +каталога, в 19 стоял пассив при известном исполнителе. Контроль, проверяющий одни правила, +обязан быть чист по всем остальным, иначе ноль недостижим и критерий ничего не значит. diff --git a/tools/install-paths.tsv b/tools/install-paths.tsv new file mode 100644 index 0000000..ef21b2e --- /dev/null +++ b/tools/install-paths.tsv @@ -0,0 +1,49 @@ +# Where each platform loads a skill from, and who says so. +# +# This file is the single source for three things that used to drift apart: the table in +# INSTALL.md, the assertions in probe-install.sh, and whatever a person says out loud about +# where ru-text ends up. A path stated in prose and nowhere else is a path nobody re-checks; +# every row here carries the vendor URL it came from and the date it was last read. +# +# Why it exists at all: on 28.07.2026 an independent probe handed a fresh agent nothing but +# «install this skill globally» and watched it install Codex's copy into ~/.codex/skills on +# the strength of a December-2025 blog post, while OpenAI's current documentation says user +# skills live in $HOME/.agents/skills. The repository is what an agent reads first, so the +# repository has to be the place the true path is written down. +# +# The headline the rows show: ~/.agents/skills is read by Codex, Cursor, Windsurf AND +# GitHub Copilot. It is not a convention one vendor invented — it is where four of them +# converged, and it is what the skills CLI writes to. Everything else is an exception. +# +# Columns: platform scope path source read-on +# scope: user | project | plugin — «plugin» means the platform has its own installer and +# a filesystem path is not the interface (Claude Code, Gemini CLI, OpenClaw, Notion). +# path: `-` when scope is plugin. Home-relative paths keep their leading ~. +# +# Regenerating is a human act: re-read the source URL, then edit the row and the date. +# probe-install.sh asserts the CONTENT of these paths; it cannot tell you a vendor moved one. + +platform scope path source read-on +Codex CLI user ~/.agents/skills https://learn.chatgpt.com/docs/build-skills.md 2026-07-28 +Codex CLI project .agents/skills https://learn.chatgpt.com/docs/build-skills.md 2026-07-28 +Cursor user ~/.agents/skills https://cursor.com/docs/skills.md 2026-07-28 +Cursor user ~/.cursor/skills https://cursor.com/docs/skills.md 2026-07-28 +Cursor project .agents/skills https://cursor.com/docs/skills.md 2026-07-28 +Cursor project .cursor/skills https://cursor.com/docs/skills.md 2026-07-28 +GitHub Copilot user ~/.agents/skills https://docs.github.com/en/copilot/concepts/agents/about-agent-skills 2026-07-28 +GitHub Copilot user ~/.copilot/skills https://docs.github.com/en/copilot/concepts/agents/about-agent-skills 2026-07-28 +GitHub Copilot project .agents/skills https://docs.github.com/en/copilot/concepts/agents/about-agent-skills 2026-07-28 +GitHub Copilot project .github/skills https://docs.github.com/en/copilot/concepts/agents/about-agent-skills 2026-07-28 +Windsurf user ~/.agents/skills https://docs.devin.ai/desktop/cascade/skills 2026-07-28 +Windsurf user ~/.codeium/windsurf/skills https://docs.devin.ai/desktop/cascade/skills 2026-07-28 +Windsurf project .agents/skills https://docs.devin.ai/desktop/cascade/skills 2026-07-28 +Windsurf project .windsurf/skills https://docs.devin.ai/desktop/cascade/skills 2026-07-28 +Google Antigravity user ~/.gemini/config/skills https://antigravity.google/docs/skills 2026-07-28 +Google Antigravity project .agents/skills https://antigravity.google/docs/skills 2026-07-28 +JetBrains Junie project .junie/skills https://github.com/vercel-labs/skills 2026-07-28 +Continue.dev project .continue/skills https://github.com/vercel-labs/skills 2026-07-28 +Cline project .cline/skills https://github.com/vercel-labs/skills 2026-07-28 +Claude Code plugin - https://code.claude.com/docs/en/discover-plugins 2026-07-28 +Gemini CLI plugin - https://github.com/talkstream/ru-text 2026-07-28 +OpenClaw plugin - https://clawhub.ai/talkstream/ru-text 2026-07-28 +Notion plugin - https://www.notion.com/help/notion-ai-skills 2026-07-28 diff --git a/tools/probe-install.sh b/tools/probe-install.sh new file mode 100755 index 0000000..76c912b --- /dev/null +++ b/tools/probe-install.sh @@ -0,0 +1,279 @@ +#!/bin/sh +# probe-install.sh — does the one-line prompt actually put the skill where the vendor says? +# +# tools/probe-install.sh --print # the documented paths, by platform +# tools/probe-install.sh setup [platform] # build a sandbox, print the prompt +# tools/probe-install.sh check # judge what the agent did +# +# ── why this exists, and why it replaces what came before ──────────────────────────── +# +# Until today the install instructions were gated by reading them against each vendor's +# documentation. That gate was run once, on 28.07.2026, over 81 claims; 31 of them were +# wrong. It is expensive, it is manual, and — the part that matters — it gates the PROSE +# rather than the OUTCOME. A README can describe a directory perfectly and the skill can +# still end up somewhere else, because the thing that installs it is an agent, not a reader. +# +# So the gate moved. The product's promise is now one sentence — «install this skill +# globally» — and the only honest test of that promise is to hand the sentence to a fresh +# agent and look at the disk afterwards. That is what this script judges. +# +# The failure it was built from: an independent probe watched an agent install Codex's copy +# into ~/.codex/skills, sourced from a December-2025 blog post, while OpenAI's current +# documentation puts user skills in $HOME/.agents/skills. Nothing in the repository said +# otherwise, so nothing corrected it. A check that only asserted «a SKILL.md exists +# somewhere» would have called that a pass. This one fails it, by name. +# +# ── what this script does NOT do ───────────────────────────────────────────────────── +# +# It does not run the agent. A POSIX shell cannot start Cursor's agent or Cascade or Codex, +# and a script that pretended to would be a gate testing itself. `setup` prepares the +# sandbox and prints the exact prompt; a human or an orchestrator runs the agent against it; +# `check` reads the result. The seam is deliberate and is where the honesty lives: if nobody +# runs the middle step, `check` finds an empty sandbox and says so rather than passing. + +set -eu +export LC_ALL=C + +cd "$(dirname "$0")/.." +ROOT=$(pwd) +PATHS=tools/install-paths.tsv +SRC=skills/ru-text + +fail=0 +ok() { printf ' ok %s\n' "$1"; } +bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; } + +usage() { + sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//' + exit 2 +} + +# Documented paths for a platform, one per line, with the leading ~ left intact. Comment and +# header lines are dropped by the field test, not by a line count: a comment added to the top +# of the TSV must not shift anything. +paths_for() { # $1=platform + awk -F'\t' -v p="$1" ' + /^#/ { next } + $1 == "platform" { next } + $1 == p && $3 != "-" { print $2 "\t" $3 } + ' "$PATHS" +} + +platforms() { + awk -F'\t' '!/^#/ && $1 != "platform" && NF > 1 { print $1 }' "$PATHS" | sort -u +} + +# ── --print ────────────────────────────────────────────────────────────────────────── +if [ "${1:-}" = "--print" ]; then + printf 'documented install paths, from %s\n\n' "$PATHS" + platforms | while IFS= read -r p; do + printf '%s\n' "$p" + rows=$(paths_for "$p") + if [ -z "$rows" ]; then + printf ' (own installer — no filesystem path is the interface)\n' + else + printf '%s\n' "$rows" | sed 's/^/ /' + fi + done + exit 0 +fi + +cmd=${1:-}; [ -n "$cmd" ] || usage +dir=${2:-}; [ -n "$dir" ] || usage + +case "$cmd" in +setup) + platform=${3:-any} + # A sandbox is a HOME and a project, both empty. Empty is the point: every directory the + # agent creates is a decision it made, and `check` can therefore attribute it. + mkdir -p "$dir/home" "$dir/project" + cat > "$dir/PROMPT.txt" < "$dir/RUN.md" <&2; exit 2; } + +printf 'probe-install: %s\n' "$platform" + +rows=$(paths_for "$platform") +if [ -z "$rows" ]; then + echo "probe-install: $platform installs through its own tool; this probe judges filesystem paths only" >&2 + exit 2 +fi + +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT INT TERM + +# Every SKILL.md the agent left anywhere in the sandbox. +# +# -L, so symlinks are followed. The first version did not, on the stated reasoning that «a +# symlinked skill is a different thing from an installed one». A live probe refuted that in +# one run: the agent symlinked ~/.agents/skills/ru-text at a clone under ~/src and cited +# OpenAI's own page for it — «Codex supports symlinked skill folders and follows the symlink +# target when scanning these locations» (learn.chatgpt.com/docs/build-skills.md). It is not +# merely allowed, it is the better install: git pull on the clone updates it. A gate that +# fails the vendor's documented shape is wrong about the product, not strict about it. +# +# The clone itself is then excluded below. Following symlinks without excluding it would +# report the source tree as four rogue installs, which is what the same live run did. +find -L "$dir" -type f -name SKILL.md 2>/dev/null | sort > "$TMP/all" + +# A SKILL.md inside the SOURCE CLONE is a checkout, not an install. Agents clone first and +# link or copy second, so the clone is a normal intermediate and flagging it is noise of the +# crying-wolf kind this project keeps refusing to ship. +# +# ⚠ «Inside a git working tree» is NOT the test, though it was the first one written here. A +# security review reproduced the bypass in one command: drop an empty `.git` beside a rogue +# install — `mkdir ~/.codex/skills/../.git` — and the probe excused it and printed PASS. A +# gate that is disarmed by creating a directory is worse than no gate, because it reports +# success. +# +# So a clone must LOOK LIKE THIS REPOSITORY, and the marker has to be something an install +# directory cannot have. `skills/ru-text/SKILL.md` was tried and is NOT such a marker: the +# second attempt at this fix still passed the bypass, because `~/.codex/skills/ru-text` has +# exactly that shape — any platform whose skills directory is literally named `skills` looks +# like a repository root by that test, with no decoy needed. The marker used instead is +# `.claude-plugin/plugin.json`: the plugin manifest, which lives only at the root of this +# repository and has no reason to exist inside an installed skill. +# +# Two conditions, both required: a `.git` and the manifest beside it. Everything under such a +# root is source — including `tools/testdata/corpus/SKILL.md`, the fixture this repository +# ships, which an earlier `skills/`-only restriction reported as a rogue install. +# +# Cloning INTO a stale path is not excused by this, and does not need a special case: the +# repository has no SKILL.md at its root — check-frozen.sh enforces that — so a clone placed +# at `~/.codex/skills/ru-text` produces no skill the platform can load, and check 1 fails for +# the plain reason that nothing landed at a documented path. +# +# Walking stops at the sandbox root, so a `.git` above the sandbox cannot excuse anything. +: > "$TMP/found" +while IFS= read -r f; do + [ -n "$f" ] || continue + d=$(dirname "$f"); clone=0 + while [ "$d" != "$dir" ] && [ "$d" != "/" ]; do + if [ -e "$d/.git" ] && [ -f "$d/.claude-plugin/plugin.json" ]; then clone=1; break; fi + d=$(dirname "$d") + done + [ "$clone" -eq 0 ] && printf '%s\n' "$f" >> "$TMP/found" +done < "$TMP/all" +n=$(grep -c . "$TMP/found" || true) + +if [ "$n" -eq 0 ]; then + bad "the sandbox holds no SKILL.md — either the agent installed nothing, or nobody ran it" + echo "probe-install: FAIL — $fail check(s)" + exit 1 +fi + +# Expand the documented paths into absolute sandbox locations. `~` maps to the sandbox HOME, +# a bare relative path to the sandbox project. +: > "$TMP/expected" +printf '%s\n' "$rows" | while IFS=' ' read -r scope path; do + case "$path" in + '~'/*) printf '%s/home/%s\n' "$dir" "${path#'~'/}" ;; + *) printf '%s/project/%s\n' "$dir" "$path" ;; + esac +done >> "$TMP/expected" + +# 1. It landed somewhere the vendor documents. +hit='' +while IFS= read -r want; do + [ -n "$want" ] || continue + if [ -f "$want/ru-text/SKILL.md" ]; then hit="$want/ru-text"; break; fi +done < "$TMP/expected" + +if [ -n "$hit" ]; then + ok "installed at a documented path: ${hit#"$dir"/}" +else + bad "installed nowhere $platform reads. Found instead:" + sed "s|^$dir/| |" "$TMP/found" | sed 's/^/ /' + echo " documented for $platform:" + sed "s|^$dir/| |" "$TMP/expected" | sed 's/^/ /' +fi + +# 2. Nothing landed at a path NOT documented for this platform. This is the check that +# catches the real failure: an agent that took a path from a stale third-party post and +# produced a directory the platform never scans. A probe that only looked for a hit +# would have called that a pass, because the agent usually writes several copies. +stray='' +# The set of skill directories reachable THROUGH a documented path, as physical locations. +# Comparing directory names was not enough: an agent that links ~/.agents/skills/ru-text at a +# copy under ~/src has one install, reachable two ways, and a name comparison calls the target +# a second rogue copy. Resolving both sides collapses them into the one install they are. +: > "$TMP/legit" +while IFS= read -r want; do + [ -n "$want" ] || continue + [ -d "$want" ] || continue + for entry in "$want"/*; do + [ -e "$entry/SKILL.md" ] || continue + (cd "$entry" 2>/dev/null && pwd -P) >> "$TMP/legit" || true + done +done < "$TMP/expected" + +while IFS= read -r got; do + [ -n "$got" ] || continue + sd=$(dirname "$got") + sdreal=$(cd "$sd" 2>/dev/null && pwd -P || printf '%s' "$sd") + if grep -qxF "$sdreal" "$TMP/legit" 2>/dev/null; then continue; fi + stray="$stray${stray:+ +}$got" +done < "$TMP/found" + +if [ -z "$stray" ]; then + ok "no copy landed outside the documented paths" +else + bad "a copy landed where $platform does not look — this is the stale-docs failure:" + printf '%s\n' "$stray" | sed "s|^$dir/| |" | sed 's/^/ /' +fi + +# 3. What arrived is this corpus, not a stale or partial one. Bytes, not names: an agent +# that fetched a tag, a fork or a truncated copy passes every path test above. +if [ -n "$hit" ]; then + if cmp -s "$hit/SKILL.md" "$SRC/SKILL.md"; then + ok "SKILL.md is byte-identical to this checkout" + else + # Name what the agent actually fetched. Without it the line reads as a mystery, and the + # commonest cause is not a fork at all — it is probing before the work is on the default + # branch, so the agent clones an older release and the diff is correct and expected. + at='' + src_repo=$(find "$dir" -type d -name .git -maxdepth 6 2>/dev/null | head -1) + if [ -n "$src_repo" ]; then + at=$(git --git-dir="$src_repo" rev-parse --short HEAD 2>/dev/null || true) + [ -n "$at" ] && at=" — it fetched $at" + fi + bad "SKILL.md differs from this checkout$at (a stale ref, a fork, or a partial copy; if you are probing before the branch is merged, this is expected)" + fi + want_refs=$(find "$SRC/references" -name '*.md' -type f | wc -l | tr -d ' ') + got_refs=$(find "$hit/references" -name '*.md' -type f 2>/dev/null | wc -l | tr -d ' ') + if [ "$got_refs" = "$want_refs" ]; then + ok "all $want_refs reference files came with it" + else + bad "reference files: $got_refs arrived, $want_refs expected — the corpus is truncated" + fi +fi + +[ "$fail" -eq 0 ] && { echo "probe-install: PASS"; exit 0; } +echo "probe-install: FAIL — $fail check(s)" +exit 1 diff --git a/tools/selftest.sh b/tools/selftest.sh index d3457d3..ec7bbdf 100755 --- a/tools/selftest.sh +++ b/tools/selftest.sh @@ -31,10 +31,24 @@ bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; } # fresh_copy — a self-contained repository slice: corpus, tools and baseline, all writable fresh_copy() { d=$(mktemp -d "$TMPROOT/case.XXXXXX") - mkdir -p "$d/skills/ru-text" - cp -R "$ROOT/skills/ru-text/references" "$d/skills/ru-text/references" - cp "$ROOT/skills/ru-text/SKILL.md" "$d/skills/ru-text/SKILL.md" + # The WHOLE skills tree, not just references/ and SKILL.md. The baseline counts atoms in + # skills/ru-text/agents/*.yaml too, so a copy missing them starts eight atoms short and + # any no-loss gate run against it fails for a reason that has nothing to do with the case. + cp -R "$ROOT/skills" "$d/skills" cp -R "$ROOT/tools" "$d/tools" + # Everything a checker reads has to be in the copy, or gates.sh fails inside a case for a + # reason the case is not about. This list grows when a checker learns to read a new file: + # check-version.sh added the manifests and the two READMEs. Missing files are copied + # silently rather than fatally — a case may deliberately be run against a partial tree. + cp -R "$ROOT/.claude-plugin" "$ROOT/.codex-plugin" "$ROOT/.cursor-plugin" "$d/" 2>/dev/null || true + mkdir -p "$d/.claude" + for f in gemini-extension.json openclaw.plugin.json README.md README.en.md INSTALL.md INSTALL.en.md .claude/CLAUDE.md; do + [ -f "$ROOT/$f" ] && cp "$ROOT/$f" "$d/$f" 2>/dev/null + done + cp -R "$ROOT/notion" "$d/notion" 2>/dev/null || true + # A repository, not just a directory: check-dogfood walks `git ls-files`, so a copy that + # is not one fails inside every case for a reason no case is about. + ( cd "$d" && git init -q . && git add -A ) >/dev/null 2>&1 || true printf '%s' "$d" } @@ -167,8 +181,14 @@ esac printf 'selftest: extract-atoms.sh + diff-atoms.sh\n' # expect_diff [map] +# The default map is EMPTY, not the repository's own. These cases compare snapshots of the +# current corpus against each other, and every row of the real map names an atom the +# current corpus no longer has — so in such a comparison every real row is legitimately +# STALE, and «identical snapshots pass» started failing the moment the map stopped being +# empty. What is under test here is diff-atoms on a synthetic pair; the real map is +# exercised for real by gates.sh, and by the cases below that pass their own fixture. expect_diff() { - name=$1; want=$2; needle=$3; o=$4; n=$5; m=${6:-$ROOT/tools/atom-map.tsv} + name=$1; want=$2; needle=$3; o=$4; n=$5; m=${6:-$EMPTY_MAP} out=$("$ROOT/tools/diff-atoms.sh" "$o" "$n" "$m" 2>&1) && st=0 || st=$? if [ "$st" -ne "$want" ]; then bad "$name — exit $st, wanted $want" @@ -184,6 +204,8 @@ expect_diff() { } W=$(mktemp -d "$TMPROOT/atoms.XXXXXX") +EMPTY_MAP="$W/empty-map.tsv" +printf '# no rows: see the note above expect_diff\n' > "$EMPTY_MAP" "$ROOT/tools/extract-atoms.sh" "$ROOT/skills/ru-text" > "$W/a.tsv" "$ROOT/tools/extract-atoms.sh" "$ROOT/skills/ru-text" > "$W/b.tsv" @@ -427,16 +449,651 @@ fi # that would have said otherwise. if ! command -v python3 >/dev/null 2>&1; then bad "python3 is absent, so the NFC assumption behind extract-atoms is unverified here" -elif python3 -c " -import unicodedata,glob,io,sys -bad=[(f,i+1) for f in glob.glob('$ROOT/skills/ru-text/references/*.md')+['$ROOT/skills/ru-text/SKILL.md'] - for i,l in enumerate(io.open(f,encoding='utf-8').read().split(chr(10))) - if unicodedata.normalize('NFC',l)!=l] -sys.exit(1 if bad else 0)"; then +# $ROOT arrives through argv, never interpolated into the source. Spliced in, a repository +# checked out into a directory whose name contains an apostrophe closes the string literal +# and runs whatever follows it — and the milder half is worse to debug: the interpreter +# raises, the else branch fires, and the selftest announces that THE CORPUS is no longer +# NFC. A true-sounding sentence about the wrong file. The folding check above already +# passes ROOT this way; this one did not. +elif python3 - "$ROOT" <<'PYEOF'; then +import unicodedata, glob, io, sys +R = sys.argv[1] +bad = [(f, i + 1) + for f in glob.glob(R + '/skills/ru-text/references/*.md') + [R + '/skills/ru-text/SKILL.md'] + for i, l in enumerate(io.open(f, encoding='utf-8').read().split(chr(10))) + if unicodedata.normalize('NFC', l) != l] +sys.exit(1 if bad else 0) +PYEOF ok "the corpus is still NFC (no normalisation step needed)" else bad "the corpus is no longer NFC — extract-atoms must normalise, or comparisons will drift" fi +printf 'selftest: check-version.sh\n' + +# expect_version +expect_version() { + name=$1; needle=$2; d=$3 + out=$("$d/tools/check-version.sh" 2>&1) && status=0 || status=$? + if [ "$status" -eq 0 ]; then + bad "$name — check-version PASSED on corrupted input" + elif printf '%s' "$out" | grep -qF "$needle"; then + ok "$name" + else + bad "$name — failed, but not for the stated reason (wanted: $needle)" + printf '%s\n' "$out" | sed 's/^/ /' + fi +} + +d=$(fresh_copy) +if "$d/tools/check-version.sh" >/dev/null 2>&1; then + ok "an untouched copy passes check-version" +else + bad "an untouched copy FAILS check-version — the cases below mean nothing" + "$d/tools/check-version.sh" 2>&1 | sed 's/^/ /' +fi + +# One manifest moved and the rest did not: the exact shape of the v1.10.1 slip, where both +# READMEs advertised the previous release while the manifests were current. +d=$(fresh_copy) +# [^"]*, not [0-9.]*: on a pre-release tree the narrow class matches nothing, the sed +# no-ops, and this case passes or fails on whatever else is wrong with the copy. +sed 's/"version": "[^"]*"/"version": "9.9.9"/' "$d/gemini-extension.json" > "$d/v" && mv "$d/v" "$d/gemini-extension.json" +expect_version "one manifest out of step is caught" "version points disagree" "$d" + +# A pre-release is a version like any other. The v2.0.0-rc.1 bump caught the prose pattern +# truncating the suffix — 2.0.0-rc.1 read as 2.0.0 — so a correct tree failed as an +# eight-way disagreement. This copy pins the fix after the release strips the suffix from +# the tree itself. +d=$(fresh_copy) +for m in .claude-plugin/plugin.json .claude-plugin/marketplace.json .codex-plugin/plugin.json \ + .cursor-plugin/plugin.json gemini-extension.json openclaw.plugin.json; do + sed 's/"version": "[^"]*"/"version": "9.9.9-rc.1"/' "$d/$m" > "$d/v" && mv "$d/v" "$d/$m" +done +sed 's/\*\*Version:\*\* [^ ]*/**Version:** 9.9.9-rc.1/' "$d/.claude/CLAUDE.md" > "$d/v" && mv "$d/v" "$d/.claude/CLAUDE.md" +if "$d/tools/check-version.sh" >/dev/null 2>&1; then + ok "a pre-release version agreeing at every point passes" +else + bad "a pre-release version agreeing at every point FAILS — the prose pattern is truncating the suffix again" + "$d/tools/check-version.sh" 2>&1 | grep -A9 'FAIL' | sed 's/^/ /' +fi + +# The prose copy is the half that actually went stale last time. Both READMEs dropped their +# prose version in favour of a badge that renders live from the releases API, so the one +# prose point left is the convention file — which is the one that went stale SECOND, after +# the READMEs were fixed. +d=$(fresh_copy) +sed 's/\*\*Version:\*\*/**Versionn:**/' "$d/.claude/CLAUDE.md" > "$d/v" && mv "$d/v" "$d/.claude/CLAUDE.md" +expect_version "a version line missing from the convention file is caught" "missing or duplicated" "$d" + +# A trigger phrase dropped from the description. This is the failure the file exists for: +# on a host with no instruction file the description IS the trigger, so losing a phrase +# stops the skill firing for Russian-speaking users while every other gate stays green. +d=$(fresh_copy) +sed 's/вычитай, //' "$d/skills/ru-text/SKILL.md" > "$d/v" && mv "$d/v" "$d/skills/ru-text/SKILL.md" +expect_version "a lost trigger phrase is caught" "lost a trigger phrase" "$d" + +# Present but pushed out of the head, where a truncating picker stops showing them. +d=$(fresh_copy) +python3 - "$d/skills/ru-text/SKILL.md" <<'PY' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +s = s.replace(' Russian text quality. Triggers: ', + ' Russian text quality for typography, info-style, editorial, UX writing and\n' + ' business correspondence, plus AI-text cleanup. Triggers: ') +io.open(p, 'w', encoding='utf-8').write(s) +PY +expect_version "Russian phrases pushed past the head are caught" "past character" "$d" + +# Over our own style budget. +d=$(fresh_copy) +python3 - "$d/skills/ru-text/SKILL.md" <<'PY' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +s = s.replace(' причеши, ru-text.', ' причеши, ru-text. ' + ('padding words to overflow the budget ' * 3)) +io.open(p, 'w', encoding='utf-8').write(s) +PY +expect_version "a description over the budget is caught" "over our budget" "$d" + +# Neither README states the size of SKILL.md any more: the rewrite dropped it, because a +# manifest's word count answers «did the author follow the spec», which is not a question a +# reader has. §4 of check-version stays and is self-activating — if a size claim ever returns, +# it is checked — so these cases BUILD the claim in the sandbox before corrupting it. Testing +# a checker against a claim the product no longer makes would be testing nothing at all. +add_size_claim() { # $1=dir $2=words $3=lines + python3 - "$1/README.md" "$2" "$3" <<'PY' +import io, sys +p, w, l = sys.argv[1], sys.argv[2], sys.argv[3] +s = io.open(p, encoding='utf-8').read() +anchor = '## Обновление' +assert s.count(anchor) == 1, 'anchor moved; fix the fixture' +s = s.replace(anchor, '## Технические детали\n\n- SKILL.md: %s слов, %s строк\n\n' % (w, l) + anchor, 1) +io.open(p, 'w', encoding='utf-8').write(s) +PY +} + +skill_words() { python3 -c "import io,re,sys;print(len(re.findall(r'[^ \t\n\r\f\v]+', io.open(sys.argv[1], encoding='utf-8').read())))" "$1/skills/ru-text/SKILL.md"; } +skill_lines() { grep -c '' "$1/skills/ru-text/SKILL.md"; } + +# Word count wrong, line count right. +d=$(fresh_copy) +add_size_claim "$d" 999 "$(skill_lines "$d")" +expect_version "a stale SKILL.md word count is caught" "states the size of SKILL.md wrongly" "$d" + +# Line count wrong, word count right — pinned separately, because a comparison that looked +# only at words passed this and left the selftest green. +d=$(fresh_copy) +add_size_claim "$d" "$(skill_words "$d")" 42 +expect_version "a stale SKILL.md line count is caught" "states the size of SKILL.md wrongly" "$d" + +# Two lines stating the size. §1 of this file has lived by «exactly once» since v1.10.1 and +# §4 did not inherit it; it now allows none or one and refuses two. +d=$(fresh_copy) +add_size_claim "$d" 999 42 +add_size_claim "$d" 888 41 +expect_version "a second line stating the size is caught, not silently ignored" "want at most 1" "$d" + +# The file grows and a claim that was correct when written stops being true. This is the +# failure that will actually happen: SKILL.md is edited far more often than the sentence. +d=$(fresh_copy) +add_size_claim "$d" "$(skill_words "$d")" "$(skill_lines "$d")" +printf '\n- A line added to SKILL.md long after the README stopped being re-read.\n' >> "$d/skills/ru-text/SKILL.md" +expect_version "SKILL.md growing past its stated size is caught" "states the size of SKILL.md wrongly" "$d" + + +printf 'selftest: check-dogfood.sh\n' + +expect_dogfood() { # $1=case-name $2=needle $3=dir + name=$1; needle=$2; d=$3 + out=$(cd "$d" && ./tools/check-dogfood.sh 2>&1) && status=0 || status=$? + if [ "$status" -eq 0 ]; then + bad "$name — check-dogfood PASSED on corrupted input" + elif printf '%s' "$out" | grep -qF "$needle"; then + ok "$name" + else + bad "$name — failed, but not for the stated reason (wanted: $needle)" + printf '%s\n' "$out" | sed 's/^/ /' + fi +} + +# The word guard, and it exists because the word came back. «онбординг» was written into the +# README's description of ux-writing.md, corrected when a check found the file has no such +# section, and written back in during a rewrite two days later. Numbers had a guard; names did +# not. +d=$(fresh_copy) +python3 - "$d/README.md" <<'PYEOF' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +old = 'уведомления, диалоги подтверждения' +assert s.count(old) == 1, 'anchor moved; fix the fixture' +io.open(p, 'w', encoding='utf-8').write(s.replace(old, 'уведомления, онбординг')) +PYEOF +expect_dogfood "a file credited with a section it does not have is caught" "does not contain it" "$d" + +d=$(fresh_copy) +if (cd "$d" && ./tools/check-dogfood.sh >/dev/null 2>&1); then + ok "an untouched copy passes check-dogfood" +else + bad "an untouched copy FAILS check-dogfood — the cases below mean nothing" + (cd "$d" && ./tools/check-dogfood.sh 2>&1) | sed 's/^/ /' +fi + +# A consumer left behind when the catalogue moves. This is the defect the checker exists +# for: four files went stale unnoticed the last time the number changed. +d=$(fresh_copy) +sed 's/Full stop-word catalog (92 entries)/Full stop-word catalog (91 entries)/' \ + "$d/skills/ru-text/SKILL.md" > "$d/v" && mv "$d/v" "$d/skills/ru-text/SKILL.md" +expect_dogfood "a consumer left behind is caught" "and these do not" "$d" + +# A claim nobody registered. The explicit list is only safe because this guard exists. +d=$(fresh_copy) +# A file NOT on the list: appending to README.md would prove nothing, because the guard +# skips listed files by design. +printf 'The catalogue holds 92 entries.\n' > "$d/CONTRIBUTING.md" +( cd "$d" && git add -A ) >/dev/null 2>&1 +expect_dogfood "an unregistered claim is caught" "no claim is registered" "$d" + +# The corpus itself moving, with every claim left at the old number. +d=$(fresh_copy) +python3 - "$d/skills/ru-text/references/info-style.md" <<'PY' +import io, re, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +s = re.sub(r'(?m)^(## B\. Каталог стоп-слов.*\n)', r'\1\nвыдуманное|заменённое\n', s, count=1) +io.open(p, 'w', encoding='utf-8').write(s) +PY +expect_dogfood "the corpus growing past its claims is caught" "and these do not" "$d" + +printf 'selftest: build-release.sh\n' + +expect_build() { # $1=case-name $2=needle $3=dir [$4=mode: check|build] + # NOT `${4:---check}`: that substitutes the default for an EMPTY fourth argument too, so + # the «dirty tree» case silently ran in --check mode and passed. A named mode cannot do + # that, and the mistake is invisible in the output when it happens. + name=$1; needle=$2; d=$3 + if [ "${4:-check}" = "build" ]; then flag=""; else flag="--check"; fi + out=$(cd "$d" && ./tools/build-release.sh $flag 2>&1) && status=0 || status=$? + if [ "$status" -eq 0 ]; then + bad "$name — build-release PASSED on corrupted input" + elif printf '%s' "$out" | grep -qF "$needle"; then + ok "$name" + else + bad "$name — failed, but not for the stated reason (wanted: $needle)" + printf '%s\n' "$out" | tail -3 | sed 's/^/ /' + fi +} + +d=$(fresh_copy) +if (cd "$d" && ./tools/build-release.sh --check >/dev/null 2>&1); then + ok "an untouched copy builds both assets and round-trips" +else + bad "an untouched copy FAILS build-release — the cases below mean nothing" + (cd "$d" && ./tools/build-release.sh --check 2>&1) | tail -4 | sed 's/^/ /' +fi + +# A reference file dropped from the index. This is the failure with no symptom: the skill +# still installs, still activates, and answers from whatever corpus it has left. +d=$(fresh_copy) +( cd "$d" && git rm -q --cached skills/ru-text/references/ux-writing.md ) >/dev/null 2>&1 +expect_build "a reference file missing from the asset is caught" "in the asset" "$d" + +# The build must not run at all without the skill it is named after. +d=$(fresh_copy) +( cd "$d" && git rm -q --cached skills/ru-text/SKILL.md ) >/dev/null 2>&1 +expect_build "a staged tree with no ru-text/SKILL.md is caught" "no ru-text/SKILL.md" "$d" + +# Without --check the tree must be clean: an artefact built from a dirty tree matches no +# commit, and the version inside it is then a claim about code that does not exist. +d=$(fresh_copy) +printf 'scratch\n' > "$d/skills/ru-text/references/scratch.md" +expect_build "a dirty tree refuses a real build" "working tree is dirty" "$d" build + +printf 'selftest: gates.sh\n' + +# gates.sh calls this file, so a case that ran it unmodified would re-enter the selftest +# forever. The copy's selftest is replaced by a stub whose exit code the case chooses: +# what is under test here is the ORCHESTRATION — that a red checker stops the run, that a +# green tree does not — never the checker being stubbed. +stub_selftest() { printf '#!/bin/sh\nexit %s\n' "$2" > "$1/tools/selftest.sh"; chmod +x "$1/tools/selftest.sh"; } + +d=$(fresh_copy); stub_selftest "$d" 0 +if "$d/tools/gates.sh" >/dev/null 2>&1; then + ok "gates.sh passes on an untouched copy" +else + bad "gates.sh FAILS on an untouched copy — every case below means nothing" + "$d/tools/gates.sh" 2>&1 | sed 's/^/ /' +fi + +d=$(fresh_copy); stub_selftest "$d" 1 +if "$d/tools/gates.sh" >/dev/null 2>&1; then + bad "gates.sh passed although a checker exited non-zero" +else + ok "a red checker stops gates.sh" +fi + +# The no-loss gate is the LAST step, so a script that quietly stopped earlier would still +# pass the case above. Losing a corpus line must reach it. +# +# The line is taken from typography.md, which has not changed since v1.10.1, and NOT from +# addenda.md. The gate compares against the pinned baseline, so it can only miss an atom +# the baseline HAS: trimming a line added after v1.10.1 is invisible to it by design, and +# this case silently stopped testing anything the day addenda.md grew a new last line. +# +# A named rule, not `sed '$d'`. The last LINE of a reference file is not necessarily the +# last ATOM of it — typography.md ends on blank lines, so deleting the last line deleted +# nothing the gate could miss, and the case passed the corpus as intact while asserting the +# opposite. R1 is the first typography rule and has been in the corpus since long before +# the pinned baseline. +d=$(fresh_copy); stub_selftest "$d" 0 +sed '/^R1\. Основные кавычки/d' "$d/skills/ru-text/references/typography.md" > "$d/trimmed" \ + && mv "$d/trimmed" "$d/skills/ru-text/references/typography.md" +grep -q '^R1\. Основные кавычки' "$d/skills/ru-text/references/typography.md" \ + && bad "the fixture did not remove R1 — the case below proves nothing" +if "$d/tools/gates.sh" >/dev/null 2>&1; then + bad "gates.sh passed on a corpus that lost a line — it never reached the no-loss gate" +else + ok "a corpus that lost a line stops gates.sh" +fi + +# gates.sh and the CI workflow are two copies of one sequence, kept in step by hand. Two +# copies drift; this is the case that says so out loud. Comments are stripped first, so +# naming a checker in prose does not count as running it. +CI=$ROOT/.github/workflows/gates.yml +# The character class covers what a checker may actually be named. It was `[a-z0-9-]+`, +# which cannot see `tools/check_new.sh`, `tools/checkNew.sh`, or anything in a subdirectory +# — three ways to add a checker to one file and not the other with this case still green. +checkers() { sed 's/#.*//' "$1" | grep -oE 'tools/[A-Za-z0-9._/-]+\.sh' | sort -u; } +if [ ! -f "$CI" ]; then + bad "the CI workflow is missing — gates.sh has nothing to be in step with" +elif [ "$(checkers "$ROOT/tools/gates.sh")" = "$(checkers "$CI")" ]; then + ok "gates.sh invokes exactly the checkers CI invokes" +else + bad "gates.sh and CI disagree about which checkers run" + printf ' gates.sh: %s\n' "$(checkers "$ROOT/tools/gates.sh" | tr '\n' ' ')" + printf ' CI: %s\n' "$(checkers "$CI" | tr '\n' ' ')" +fi + +# Naming a checker is not running it. A checker whose failure is swallowed — `|| true`, +# or the line replaced by an `echo` that still mentions it — leaves the case above green, +# because that case compares NAMES. This one compares the shape of the invocation: every +# line in gates.sh that runs a checker must hand its failure to `fail`, and there must be +# as many such lines as there are checkers, so that deleting one is not a way to pass. +# `if ! tools/x.sh; then :; fi` runs a checker too, and the anchor used to miss it — a +# swallowed failure in that shape was invisible to both arms of this case. +guarded_invocations() { sed 's/#.*//' "$1" | grep -E '^[[:space:]]*(if[[:space:]]+!?[[:space:]]*)?tools/[A-Za-z0-9._/-]+\.sh' || true; } +inv=$(guarded_invocations "$ROOT/tools/gates.sh") +inv_n=$(printf '%s\n' "$inv" | grep -c . || true) +unguarded=$(printf '%s\n' "$inv" | grep -c -v '|| fail' || true) +[ -z "$inv" ] && inv_n=0 && unguarded=0 +if [ "$inv_n" -ge 4 ] && [ "$unguarded" -eq 0 ]; then + ok "every checker gates.sh runs hands its failure to fail (${inv_n} invocations)" +else + bad "gates.sh: ${inv_n} checker invocations, ${unguarded} of them unguarded (want >=4 and 0)" + printf '%s\n' "$inv" | sed 's/^/ /' +fi + +# The executable-and-parseable gate is the one step of gates.sh no name comparison can see: +# it is written as the glob `tools/*.sh`, which matches no checker path. It was removable +# with every case still green. Tested by behaviour rather than by grep — a checker that +# cannot be parsed, in a subdirectory, must stop the run. +d=$(fresh_copy); stub_selftest "$d" 0 +mkdir -p "$d/tools/sub" +printf '#!/bin/sh\nif then fi\n' > "$d/tools/sub/broken.sh"; chmod +x "$d/tools/sub/broken.sh" +out=$("$d/tools/gates.sh" 2>&1) && st=0 || st=$? +if [ "$st" -eq 0 ]; then + bad "an unparseable checker did not stop gates.sh — the syntax gate is gone or shallow" +elif printf '%s' "$out" | grep -qF 'syntax error: tools/sub/broken.sh'; then + ok "an unparseable checker in a subdirectory stops gates.sh, and says which one" +else + bad "gates.sh failed, but not on the syntax gate — the case proves nothing" + printf '%s\n' "$out" | tail -3 | sed 's/^/ /' +fi + +# The syntax gate must speak the runner's shell. /bin/sh is bash 3.2 on macOS and dash on +# the ubuntu runner, so a bashism passed locally and failed in CI — the divergence a local +# gate exists to catch first. Skipped, loudly, where dash is absent: a case that cannot run +# must not report as a case that passed. +if command -v dash >/dev/null 2>&1; then + d=$(fresh_copy); stub_selftest "$d" 0 + printf '#!/bin/sh\narr=(a b)\necho "$arr"\n' > "$d/tools/bashism.sh"; chmod +x "$d/tools/bashism.sh" + if "$d/tools/gates.sh" >/dev/null 2>&1; then + bad "a bashism in a checker passed the syntax gate — sh -n instead of dash -n?" + else + ok "a bashism in a checker is caught locally, as it would be on the runner" + fi +else + note "dash absent — the bashism case did not run (the runner's /bin/sh is dash)" +fi + +# Static, and said to be static: $ROOT must reach python through argv, never spliced into +# the source. A repository checked out into a directory whose name contains an apostrophe +# closes the string literal and runs what follows; the milder half is a false «the corpus +# is no longer NFC» about a file that is fine. +spliced=$(grep -cE "glob\.glob\('\\\$ROOT|open\('\\\$ROOT|'\\\$ROOT/" "$ROOT/tools/selftest.sh" || true) +viaargv=$(grep -cE "^(elif )?python3 - \"\\\$ROOT\"|^ *folded=\\\$\(python3 - \"\\\$ROOT\"" "$ROOT/tools/selftest.sh" || true) +if [ "$spliced" -eq 0 ] && [ "$viaargv" -ge 2 ]; then + ok "both python blocks take \$ROOT through argv, none splices it into the source" +else + bad "\$ROOT spliced into python source (${spliced} sites) or argv form lost (${viaargv} of 2)" +fi + +# The parity case compares two sets built by ONE regex, so narrowing that regex blinds both +# sides at once and the case cannot notice. Pin its breadth directly, on the names that +# broke it: an underscore, a camel hump, a subdirectory. +probe=$(printf 'tools/check_new.sh\ntools/checkNew.sh\ntools/sub/deep.sh\n' \ + | grep -cE 'tools/[A-Za-z0-9._/-]+\.sh' || true) +[ "$probe" -eq 3 ] && ok "the checker-name pattern covers _, camelCase and subdirectories" \ + || bad "the checker-name pattern matched $probe of 3 — divergence would go unseen" + +# CI can stop RUNNING a checker while still naming it, and the name comparison would stay +# green. Every checker named in the workflow must sit on a line that runs it. +if [ -f "$CI" ]; then + badline=$(sed 's/#.*//' "$CI" | grep -E 'tools/[A-Za-z0-9._/-]+\.sh' \ + | grep -vE '^[[:space:]]*(run:[[:space:]]*)?(\$\()?(sha256sum|for|\[|tools/)' | grep -c . || true) + [ "$badline" -eq 0 ] && ok "every checker the CI workflow names is on a line that runs it" \ + || bad "CI names $badline checker(s) outside a running line" +fi + +# The baseline pin is written out twice for the same reason, and a stale copy is worse +# than none: the local gate would keep vouching for a baseline the repository no longer has. +# +# Comments are stripped from BOTH sides. Stripping only the local one meant a CI pin block +# commented out — the check gone, the text still there — compared equal and stayed green. +if [ -f "$CI" ]; then + hashes() { sed 's/#.*//' "$1" | grep -oE '[0-9a-f]{64}' | sort -u; } + pinpath() { sed 's/#.*//' "$1" | grep -oE 'tools/baseline/[A-Za-z0-9.-]+' | sort -u; } + pin_local=$(hashes "$ROOT/tools/gates.sh"); pin_ci=$(hashes "$CI") + path_local=$(pinpath "$ROOT/tools/gates.sh"); path_ci=$(pinpath "$CI") + if [ -n "$pin_local" ] && [ -n "$path_local" ] && + [ "$pin_local" = "$pin_ci" ] && [ "$path_local" = "$path_ci" ]; then + ok "the baseline pin in gates.sh matches CI, file and hash" + else + bad "baseline pin differs: gates.sh $path_local $pin_local / CI $path_ci $pin_ci" + fi +fi + +printf 'selftest: check-typography.sh\n' + +# The checker shipped green over eight real defects, because both halves of its R30 pattern +# were narrower than the rule: the letter set held lowercase only, and the follower class +# demanded a Cyrillic letter or a digit next. +# +# The two cases below pin one half EACH, and their shapes are chosen for that and nothing else. +# The first pair written here failed this requirement — «В WSL-сессиях» and «В [README]» both +# combine a capital preposition with a non-Cyrillic follower, so either mutation killed both +# and neither hand was actually guarded. Verified by mutation: restoring the lowercase-only set +# must break the FIRST case only, and restoring the narrow follower class the SECOND only. +# capital + Cyrillic follower -> isolates the letter set +# lowercase + non-letter -> isolates the follower class +expect_typo() { # $1=case-name $2=needle $3=dir + name=$1; needle=$2; d=$3 + out=$(cd "$d" && ./tools/check-typography.sh 2>&1) && st=0 || st=$? + if [ "$st" -eq 0 ]; then + bad "$name — check-typography PASSED on corrupted input" + elif printf '%s' "$out" | grep -qF "$needle"; then + ok "$name" + else + bad "$name — failed, but not for the stated reason (wanted: $needle)" + printf '%s\n' "$out" | sed 's/^/ /' + fi +} + +d=$(fresh_copy) +if (cd "$d" && ./tools/check-typography.sh >/dev/null 2>&1); then + ok "an untouched copy passes check-typography" +else + bad "an untouched copy FAILS check-typography — the cases below mean nothing" + (cd "$d" && ./tools/check-typography.sh 2>&1) | sed 's/^/ /' +fi + +# A CAPITAL preposition, which the first pattern never tested — and which is where a Russian +# sentence most often starts one. +d=$(fresh_copy) +python3 - "$d/INSTALL.md" <<'PYEOF' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +io.open(p, 'w', encoding='utf-8').write(s.replace('В\u00a0приложении', 'В приложении', 1)) +PYEOF +expect_typo "a capital single-letter preposition is caught" "single-letter «В»" "$d" + +# A preposition followed by something that is not a Cyrillic letter — a markdown link. The +# first pattern required a letter or digit next and let every one of these through. +d=$(fresh_copy) +python3 - "$d/INSTALL.md" <<'PYEOF' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +io.open(p, 'w', encoding='utf-8').write(s.replace('и\u00a0**`.agents/skills/`**', 'и **`.agents/skills/`**', 1)) +PYEOF +expect_typo "a preposition before a non-letter is caught" "single-letter «и»" "$d" + +# The two defects that actually shipped, kept as regression cases. +d=$(fresh_copy) +python3 - "$d/README.md" <<'PYEOF' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +s = s.replace('машинного текста', 'машинного~текста', 1) +io.open(p, 'w', encoding='utf-8').write(s) +PYEOF +expect_typo "a literal tilde standing in for a non-breaking space is caught" "literal ~" "$d" + +d=$(fresh_copy) +python3 - "$d/README.md" <<'PYEOF' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +io.open(p, 'w', encoding='utf-8').write(s.replace('\u00a0\u2014', ' \u2014', 1)) +PYEOF +expect_typo "an ordinary space before an em dash is caught" "before an em dash" "$d" + +# Digit grouping. This case exists because it ESCAPED: the checker was green over «более +# 2 000 атомов» set with an ordinary space, since nothing looked at digit groups at all, and +# a judge found it by reading the file rather than by running the gate. +d=$(fresh_copy) +python3 - "$d/README.md" <<'PY' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +io.open(p, 'w', encoding='utf-8').write(s.replace('2\u00a0000', '2 000', 1)) +PY +expect_typo "an ordinary space between digit groups is caught" "between digit groups" "$d" + +# Initials. Found by a judge reading the sources list after three green gates had run over +# it — the fourth rule this checker did not name. The pattern of the misses is worth stating: +# every one was a rule the corpus has and the checker did not, never a rule it got wrong. +d=$(fresh_copy) +python3 - "$d/README.md" <<'PYEOF' +import io, sys +p = sys.argv[1] +s = io.open(p, encoding='utf-8').read() +io.open(p, 'w', encoding='utf-8').write(s.replace('А.\u00a0Э.\u00a0Мильчин', 'А. Э. Мильчин', 1)) +PYEOF +expect_typo "an ordinary space after an initial is caught" "after an initial" "$d" + +printf 'selftest: probe-install.sh\n' + +# The probe judges what an agent did to a sandbox. These cases judge the probe, by building +# the three outcomes by hand — because the one thing a gate must never do is pass on an +# install that did not happen, and that is exactly what an «is there a SKILL.md anywhere» +# check would do. +probe_case() { # $1=case-name $2=want(PASS|FAIL) $3=needle $4=sandbox + name=$1; want=$2; needle=$3; sb=$4 + out=$("$ROOT/tools/probe-install.sh" check "$sb" 'Codex CLI' 2>&1) && st=PASS || st=FAIL + if [ "$st" != "$want" ]; then + bad "$name — probe said $st, wanted $want" + printf '%s\n' "$out" | sed 's/^/ /' + elif [ -z "$needle" ] || printf '%s' "$out" | grep -qF "$needle"; then + ok "$name" + else + bad "$name — $st, but not for the stated reason (wanted: $needle)" + printf '%s\n' "$out" | sed 's/^/ /' + fi +} + +PB=$(mktemp -d) + +# Nobody ran the agent. An empty sandbox must never read as a clean install. +"$ROOT/tools/probe-install.sh" setup "$PB/empty" 'Codex CLI' >/dev/null 2>&1 +probe_case "an empty sandbox is not a pass" FAIL "holds no SKILL.md" "$PB/empty" + +# The documented user path. This is the only shape that may pass. +"$ROOT/tools/probe-install.sh" setup "$PB/good" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/good/home/.agents/skills" +cp -R "$ROOT/skills/ru-text" "$PB/good/home/.agents/skills/ru-text" +probe_case "an install at the documented path passes" PASS "documented path" "$PB/good" + +# The failure this file was written from: a real agent installed Codex's copy into +# ~/.codex/skills on the strength of a December-2025 blog post. The path exists, the skill +# is intact, and Codex never looks there. +"$ROOT/tools/probe-install.sh" setup "$PB/stray" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/stray/home/.codex/skills" +cp -R "$ROOT/skills/ru-text" "$PB/stray/home/.codex/skills/ru-text" +probe_case "an install at an undocumented path is caught" FAIL "does not look" "$PB/stray" + +# Right path, wrong corpus. An agent that fetched an old tag or a fork passes every path +# test above; only the bytes catch it. +"$ROOT/tools/probe-install.sh" setup "$PB/stale" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/stale/home/.agents/skills" +cp -R "$ROOT/skills/ru-text" "$PB/stale/home/.agents/skills/ru-text" +printf '\nA line no released ru-text ever carried.\n' >> "$PB/stale/home/.agents/skills/ru-text/SKILL.md" +probe_case "a stale or forked copy at the right path is caught" FAIL "differs from this checkout" "$PB/stale" + +# Right path, right SKILL.md, half the corpus. The reference files are the product; a probe +# that stopped at SKILL.md would bless a skill with no rules in it. +"$ROOT/tools/probe-install.sh" setup "$PB/partial" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/partial/home/.agents/skills" +cp -R "$ROOT/skills/ru-text" "$PB/partial/home/.agents/skills/ru-text" +rm -f "$PB/partial/home/.agents/skills/ru-text/references/typography.md" +probe_case "a truncated corpus at the right path is caught" FAIL "truncated" "$PB/partial" + +# A symlinked install. The first version of the probe called this «a different thing from an +# installed one» and would have failed it; a live run refuted that in one shot, with OpenAI's +# own page: «Codex supports symlinked skill folders and follows the symlink target». The agent +# had linked deliberately, so that a pull on the clone updates the install. +"$ROOT/tools/probe-install.sh" setup "$PB/link" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/link/home/src" "$PB/link/home/.agents/skills" +cp -R "$ROOT/skills/ru-text" "$PB/link/home/src/ru-text" +ln -s "$PB/link/home/src/ru-text" "$PB/link/home/.agents/skills/ru-text" +probe_case "a symlinked install at the documented path passes" PASS "documented path" "$PB/link" + +# The clone an agent makes before linking or copying. It is an intermediate, not a rogue +# install, and the same live run had the probe report the source tree as four of them. +"$ROOT/tools/probe-install.sh" setup "$PB/clone" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/clone/home/src/ru-text/.claude-plugin" "$PB/clone/home/.agents/skills" +cp -R "$ROOT/skills" "$PB/clone/home/src/ru-text/skills" +# The manifest is what makes a tree this repository rather than any directory with a .git — +# see the decoy case below, which is why the marker had to become this specific. +cp "$ROOT/.claude-plugin/plugin.json" "$PB/clone/home/src/ru-text/.claude-plugin/plugin.json" +( cd "$PB/clone/home/src/ru-text" && git init -q . ) >/dev/null 2>&1 +cp -R "$ROOT/skills/ru-text" "$PB/clone/home/.agents/skills/ru-text" +probe_case "the source clone is not reported as a rogue install" PASS "no copy landed outside" "$PB/clone" + +# The bypass a security review found and reproduced in one command: an empty `.git` beside a +# rogue install disarmed the clone exclusion, and the probe printed PASS over an install at +# the stale-blog path. A gate that a `mkdir` switches off is worse than no gate. +# +# The decoy is built exactly as the review built it, at ~/.codex/skills — which also pins the +# SECOND attempt at the fix. That one required the tree to hold `skills/ru-text/SKILL.md`, and +# `~/.codex/skills/ru-text` has that shape with no decoy at all, so any platform whose skills +# directory is named `skills` would have excused itself. Only the plugin manifest separates a +# checkout from an install. +"$ROOT/tools/probe-install.sh" setup "$PB/decoy" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/decoy/home/.agents/skills" "$PB/decoy/home/.codex/skills/.git" +cp -R "$ROOT/skills/ru-text" "$PB/decoy/home/.agents/skills/ru-text" +cp -R "$ROOT/skills/ru-text" "$PB/decoy/home/.codex/skills/ru-text" +mkdir -p "$PB/decoy/home/.codex/.git" +probe_case "a decoy .git does not excuse an install at an undocumented path" FAIL "does not look" "$PB/decoy" + +# The repository's own test fixture lives at tools/testdata/corpus/SKILL.md, outside skills/. +# The first clone rule excused only files under the root's `skills/`, so a genuine clone was +# reported as a rogue install on the strength of a fixture. Everything under a verified root +# is source. +"$ROOT/tools/probe-install.sh" setup "$PB/fixture" 'Codex CLI' >/dev/null 2>&1 +mkdir -p "$PB/fixture/home/src/ru-text/.claude-plugin" "$PB/fixture/home/.agents/skills" +cp -R "$ROOT/skills" "$PB/fixture/home/src/ru-text/skills" +cp -R "$ROOT/tools" "$PB/fixture/home/src/ru-text/tools" +cp "$ROOT/.claude-plugin/plugin.json" "$PB/fixture/home/src/ru-text/.claude-plugin/plugin.json" +( cd "$PB/fixture/home/src/ru-text" && git init -q . ) >/dev/null 2>&1 +cp -R "$ROOT/skills/ru-text" "$PB/fixture/home/.agents/skills/ru-text" +probe_case "a fixture outside skills/ does not make a clone look rogue" PASS "no copy landed outside" "$PB/fixture" + +# Every platform named in the table is either given paths or declared to own its installer. +# A row typo silently produces a platform this probe can never judge. +missing=$(awk -F'\t' '!/^#/ && $1 != "platform" && NF > 1 { print $1 "\t" $3 }' "$ROOT/tools/install-paths.tsv" \ + | awk -F'\t' '{ if ($2 == "-") own[$1]=1; else has[$1]=1 } + END { for (p in own) if (p in has) print p }') +if [ -z "$missing" ]; then + ok "no platform is both path-served and self-installing" +else + bad "a platform claims both a path and its own installer: $missing" +fi + +rm -rf "$PB" + printf 'selftest: %d passed, %d failed\n' "$pass" "$fail" [ "$fail" -eq 0 ]