Skip to content

Contributing

Braden Seaborn edited this page Aug 26, 2026 · 1 revision

Contributing

This page is orientation. It describes how the repository is laid out, how a change travels from idea to merged pull request, and which kind of contribution needs which kind of evidence. Read it to decide whether you can contribute, before you clone anything.

For exact commands, file formats and the review checklist, see CONTRIBUTING.md in the repository. This page does not repeat it.

Why the barrier is low

Two properties of this project make it easier to work on than its subject matter suggests.

There is nothing to install. The linter is standard library only, on purpose and permanently. Clone the repository and python -X utf8 ste_lint.py --help runs the tool. No virtualenv, no dependency resolution, no build step. See Installation for the installed-package route, which users want and contributors do not need.

The bulk of the rules are data, not code. What the linter catches lives in JSON tables of words and phrases, read off disk at startup. Adding a word the tool should flag means editing a JSON file and adding one example sentence to a test fixture. You never have to open the engine.

And the contribution this project values highest needs no code at all: a report of a rule firing on text that is correct.

How the repository is laid out

src/ste100/        The package: engine, checks, rule data, presets
tests/             Eight test harnesses and their corpus fixtures
devtools/          Maintenance scripts, not shipped in the wheel
docs/              Reference documentation
examples/          Drop-in configuration for CI, pre-commit, VS Code

src/ste100/

The whole tool. engine.py holds Engine, which composes the check modules as mixins and runs them over each unit of text. The checks are grouped by what they examine, not by rule number:

  • checks_lexical.py -- T1, T3 and T6. Word and phrase lookups against the loaded tables.
  • checks_atomicity.py -- T5. Sentence structure: multiple shall, and/or, punctuation density, EARS template conformance.
  • checks_reference.py -- T4. Pronouns and comparatives with no stated antecedent or baseline.
  • csv_integrity.py -- the STE-CSV-* checks, which examine relationships between rows instead of prose.

Supporting modules each do one job: paths.py loads config and rule data, discovery.py walks the tree and detects which profile a file belongs to, masking.py hides code spans so the checks never fire inside them, units.py defines the Finding record, report.py formats output, fixer.py implements --fix, explain.py implements --explain, cli.py parses arguments, and helve.py is the JSON-RPC server.

Two subdirectories hold data instead of code:

  • src/ste100/data/*.json -- the bulk word and phrase tables. T1 substitutions, T2 vague terms, T3 hedges, T6 filler and AI tells, plus budgets and part-of-speech heuristics. These files are the source of truth and are edited by hand.
  • src/ste100/presets/*.json -- the shipped presets. default.json is tuned for general technical documentation; veistra.json is the stricter original profile set. A preset defines the profiles, their path globs, severity tiers and thresholds. Configuration covers what each key does.

Fixed rule IDs outside the bulk tables -- T4, T5, the structural S7-* checks, CSV integrity and budgets -- are declared as constants in rule_ids.py, each beside the --explain text that describes it.

How the Linter Works walks through the pipeline in detail. This page only names the pieces.

tests/

Eight harnesses, each a plain Python script with no test framework behind it. They cover the corpus, T1 suggestion collisions, --fix, --baseline, configuration handling, the default preset's out-of-box quietness, the JSON-RPC server, and adversarial CSV input. Each prints a distinct success line, and CI checks for that line as well as the exit code, because one harness once exited 0 after finding zero files to check.

Alongside them sit the corpus fixtures, which are how this project encodes what it believes:

  • corpus_clean/ -- text that has to produce no findings at all. This is where a fixed false positive goes, permanently.
  • corpus_dirty/ -- text that has to produce specific findings, marked with inline `expect:RULE-ID` annotations.
  • corpus_default/ -- natural documentation the default preset has to stay quiet on.
  • corpus_suggestions/ -- text covering the replacements T1 rules hand out.

devtools/

Maintenance scripts outside the installed package. collision_audit.py is the one contributors touch: it checks that no T1 rule tells you to replace a word with another word that a different rule bans. The rest are audit reports carried over from the project this tool was extracted from.

One warning before you go looking: build_lint_data.py was the generator that produced the JSON tables, and it cannot be run here. It reads a source word list absent from this repository, and the tables have been hand-edited since. Edit the JSON directly.

docs/ and examples/

docs/ holds the reference material the wiki summarises: the full rule list, configuration keys, integration recipes, and the HELVE server protocol. examples/ holds files you can copy into your own project -- GitHub Actions and GitLab CI jobs, a pre-commit configuration, VS Code tasks with a problem matcher. Integrations covers those from the user's side.

Three kinds of contribution

Each needs different evidence, and knowing which one you have saves time.

A false-positive report

The linter flagged text that was fine. This is the most useful report this project receives, and it needs no code.

Four fields make one usable: the exact sentence, the rule ID that fired, the profile the file was linted under, and the correct behaviour. The issue template asks for each. All four go into the regression test, which is what stops the same false positive from coming back.

Why this weighs heavier here than in other linters: a rule that catches genuine problems while also flagging correct usage is worse than no rule. It teaches people to skim past findings, and then the true ones go past too.

A new or changed rule

Three requirements, and they are firm:

  1. A citation. Every rule here names its source -- ASD-STE100 itself, INCOSE, the NASA Systems Engineering Handbook, MIL-STD-961E, the EARS templates, or Femmer et al. on requirements smells. See Simplified Technical English for what the underlying standard does and does not say.
  2. A corpus fixture. An example in corpus_dirty/ that has to fire, and, where the word is plausibly ambiguous, a counterexample in corpus_clean/ that has to stay silent. The counterexample is what gets a rule accepted.
  3. No collision. If a T1 rule's replacement word is banned elsewhere, following the tool's advice creates a new finding. devtools/collision_audit.py finds those, and the suggestion harness fails when it is not clean.

Adding a word to an existing table is routine. New check logic is a design conversation. Open an issue before writing it.

A code change

Extend whichever harness covers the surface you touched, and add no runtime dependency. The stdlib-only rule is not negotiable, and it is the review comment most likely to appear on a first pull request.

From idea to merged

  1. Open an issue first for anything beyond a word-list addition or an obvious fix. For new check logic this is required, and it costs far less than discovering a design objection after you have written the code.
  2. Branch and change. Edit the JSON table or the module, add the fixture, and run the linter over your own example to watch the finding appear or disappear.
  3. Run all eight harnesses. They take seconds, and they are the same checks CI runs.
  4. Open the pull request and fill in the template. One logical change per pull request.
  5. CI runs three jobs: the harnesses across three operating systems and three Python versions, a packaging job that builds and installs the wheel, and a non-blocking job that lints this repository's own Markdown. That last one is advisory by design -- the shipped tables were calibrated for strict specification writing, and community-health prose legitimately uses words the stricter profiles reject.
  6. Review is by the maintainer. This is a one-maintainer project: expect days, not hours. A comment after a quiet week is fine.

Worth knowing before you start

Rule IDs are permanent. Every ID has the shape STE-<test>-<CATEGORY>-<seq4>, for example STE-T1-SUB-0104, and they are stable from v0.1.0 onward. People pin them in --baseline files and in configuration overrides, so renumbering an existing entry breaks both without any warning. Add new numbers; never reuse or renumber. A gap in the sequence is fine.

Python 3.9 is the floor, and the trap is subtler than syntax. Parsing the sources at that feature version catches new grammar, but not a standard library method that gained a new keyword argument later. That shipped once: Path.write_text(newline=...) is 3.10 and later, passed the syntax check, and broke --fix on 3.9 on every platform until the matrix caught it.

Match the code that is there. No type annotations anywhere, .format() instead of f-strings for messages, and comments that explain why the code is shaped the way it is -- often by naming the bug that made it necessary.

The agent skill ships with the repository. If your change alters flags or behaviour, check whether .claude/skills/ste100-lint/SKILL.md needs the same update. See Agent Skill.

Community standards

The project follows the Contributor Covenant. See CODE_OF_CONDUCT.md.

Report security issues privately through GitHub Security Advisories, never as a public issue; see SECURITY.md.

Contributions are licensed under Apache-2.0, matching the project. There is no separate agreement to sign.

Clone this wiki locally