# simple_english

Lint Markdown prose and code comments with the SimpleEnglish Plain-mode rules. Pattern
rules run on LanguageTool (`rules/simple-english.xml`). Counting rules run in Ruby.

## Docs

- `README.md` is the user guide (lint prose). `docs/DAEMON.md` holds
  the daemon HTTP API
- `docs/RULES.md` lists every rule in prose. Regenerate it with
  `bin/render-rules` after a rule change.
- `docs/DEVELOPMENT.md` is the dev guide (layout, tests, rules).
  `docs/RELEASING.md` holds the release flow (prepare, merge, publish)

## Branches

- Never commit on the default branch. Prefix branch names with your
  own handle to show ownership. Release prep uses `release/vX.Y.Z`.

## GitHub

- Follow the repo's templates. Issues go through the forms in
  `.github/ISSUE_TEMPLATE/`. Pull requests use
  `.github/pull_request_template.md`. Fill their sections as written.
  Do not bypass a template by passing `--body` to `gh`.
- `CONTRIBUTING.md` holds the process: false findings get an issue
  first, not a direct fix. Pull requests are drafts.
- A pull request that touches `.github/workflows/` follows the action
  pinning rules in `.github/AGENTS.md`.
- Changelog: run `changie new -k <Kind> -b "BODY"` and commit the fragment
  under `.changes/unreleased/`
  only when the tool's behavior changes. Documentation-only changes get no entry.
  Pass the kind and body as flags: bare `changie new` prompts
  interactively, which hangs a non-interactive session.
  Unsure of the kind? Ask the user, do not guess it.
  Pull requests add fragments, not changelog lines, so they never conflict.
  The kinds are Breaking, Added, Changed, and Fixed. Only Breaking maps to
  a major bump in `changie next auto`. Breaking means the change breaks
  the CLI, the config, or an output format.
  Write the body as one line, per the rules in `.changes/AGENTS.md`.
  Preview the entry with `--dry-run` on the same `changie new` command
  before the pull request. `rake lint` lints the fragment bodies.

## Layout

- `lib/simple_english.rb` is the composition root: `lint`.
  `lint/linter.rb` is the path pipeline the CLI and MCP share.
  `test/corpus_check.rb` runs the corpus pairs
- `lib/simple_english/version.rb` holds the gem version, and a version bump
  is the only change that belongs in it. `cli.rb` is the Thor CLI
- `lib/simple_english/model_context_protocol.rb` is the MCP stdio front
  door (`SimpleEnglish::ModelContextProtocol`). It is a client of the
  daemon, not a part of it
- The tree groups files by domain: `setup/`, `lint/`, `client/`,
  `daemon/`
- `setup/` resolves the environment once, at the process edge. It
  holds `languagetool.rb` (pinned engine facts), `install.rb`
  (bundled executable), `config.rb`, and `fingerprint.rb`
- `lint/` is the pipeline domain: `lint_plan.rb`, `markdown.rb`,
  `extractor.rb`, `annotated_text.rb`, `counts.rb`, `suppressions.rb`
- `client/` is the client tier. `language_tool.rb` speaks the
  LanguageTool wire protocol, used by the daemon's engine.
  `daemon.rb` probes, trusts, and boots the daemon
- `daemon/` holds the server tier: `engine.rb` (lint pipeline),
  `http.rb` (wire framing), `server.rb` (lifecycle). Server failures
  raise typed errors (`PortInUse`, `InnerDied`, `InnerTimeout`)
- One object definition per file: value objects live in their own files
  in `lint/` (`finding.rb`, `paragraph.rb`, `span.rb`, `segment.rb`,
  `result.rb`, `plain_text.rb`)
- Internals are `private_class_method`

## Constraints

- Runtime dependencies stay minimal. The gemspec adds
  `tree_sitter_language_pack` for comment extraction and Markdown
  structure, and `thor` for the CLI. Any further runtime dependency
  needs a stated reason here first. (Thor was a
  user-directed refactor, 2026-02-27.) Platform gems bundle a native
  LanguageTool server. Runtime no longer depends on a JVM (2026-10-01).
  `rexml` remains dev-only for `bin/render-rules` and
  `native/languagetool/verify.rb` (2026-10-02).
- Dev and test tools are the exception. Pick the best tool for the
  job even when it adds a dev-only dependency. Record it here with a
  one-line reason. Keep such tools out of the gemspec runtime list.
  Current tools, each with its reason: `minitest` for tests. The
  bundled gem alone is not enough, because `bundle exec` cannot
  require a bundled gem that the lockfile omits. `minitest-mock`
  restores `Object#stub` after minitest 6 dropped it. `rake` runs them.
  Both are dev-only, in the gemspec. `rexml` parses
  the rule XML in `bin/render-rules` and `verify.rb` extracts rule
  examples from it for native parity checks. It stopped shipping as a Ruby
  default gem in 4.x (2026-10-02). `changie` (a brew
  binary, not
  a gem) batches change fragments into `CHANGELOG.md` at release
  time (2026-09-29). `json_schemer` validates the Cursor plugin
  against the vendored Agent Plugins schema, in the contract tests
  (2026-10-08). Pull requests add fragments, not changelog
  lines, so they never conflict.
- `Markdown.strip` must keep the line count identical to the source. Findings cite
  original line numbers, so stripping changes must preserve them.
- Ruby 3.3 minimum. CI enforces it. The
  gemspec declares no floor.

## Changes

Default to no comment. A comment earns its place only when it says
what the code cannot. It explains why: a non-obvious tradeoff, a
workaround for an upstream bug, a perf choice that looks wrong. It
warns of a real hazard: ordering, concurrency, a caller invariant.
It cites an external source.

Never narration of the nearby code,
restatement of a good name, or notes about the change itself.
Prefer a clearer name or a test over a comment.

## Adding a rule

1. Pattern rule: add to `rules/simple-english.xml` with incorrect and correct
   examples
2. Counting rule: `lib/simple_english/lint/counts.rb`
3. Add a corpus pair `test/corpus/NN-before.md` / `NN-after.md`. The before file
   must trigger the rule. The after file must stay clean.
4. Close a reported false positive or false negative with a corpus pair, in the
   same change. The report found a shape the rule author did not imagine, and
   the pair keeps it covered for good.

## Suppressions

A line containing `se: ignore` suppresses findings reported on that line;
`se: ignore=RULE1,RULE2` scopes it to rules. Pattern findings cite the line
of the match (not the paragraph or comment start), so put the directive on the
line the finding reports. Counting findings cite the paragraph's first line.

## Config

`.simple-english.yml` in the CWD. Keys: `ignore:` (path globs. `**` crosses directories and `*` does not) and `disabled-rules:` (rule IDs dropped from every file).

## Tests

- `rake lint`: self-lint the repo's own prose. CI's `native` job runs it,
  then `ruby test/examples_check.rb` for the rule examples. The
  `unit` job runs `rake test` on the supported-platform Ruby matrix.
  CI adds `standardrb`, `actionlint`, and `hadolint` on top. The
  `native` job installs the built platform gem, then runs
  `bin/e2e-story` without an executable override. It lints the corpus
  and a code comment outside the repo. The corpus has that one CI
  home. standardrb stays out of the bundle.
  Reason: rubocop pins `json ~> 2.3`,
  and Ruby 4.0's default json gem is 3.x. Bundling it breaks
  `bundle exec` on 4.0
- `rake test`: unit tests only, no LanguageTool needed
- `rake lint`: self-lint the repo's own prose, then the changie
  fragment bodies
- `ruby test/examples_check.rb` and `ruby test/corpus_check.rb`: need
  the native LanguageTool server. Set `SE_LANGUAGETOOL_EXECUTABLE` for a
  source-tree build

See `docs/DEVELOPMENT.md` for the full test strategy.
