For AI agents: this page as Markdown · llms.txt · JSON Schema

Adding rules to a pack

A pack rule is either written for Rerail and proven by its examples, or ported from another tool (a linter, a security scanner, a complexity metric) and proven against that tool's tests.

flowchart TD
  W["Write the rule"] --> PR["Open the pull request"]
  PR -- "own rule" --> C["Contract passes,<br/>reviewer agrees"] --> S["Shipped"]
  PR -- "ported rule" --> D["Maintainer publishes<br/>its test data"]
  D --> J["Conformance comment<br/>judges the candidate"]
  J -- "fails a check" --> F["Fix, add evidence,<br/>or declare a divergence"] --> J
  J -- "passes every check" --> P["Reviewer promotes it"] --> S
  S -- "a shipped port fails a check" --> B["Fix it, or move it back"] --> J

Below, <id> is the pack (packs/<id>/), <tool> the upstream tool, <rule> the Rerail rule id (ruff-f401-unused-import), and "upstream rule" the tool's name for it (F401).

Candidates and whether a port starts on

A rule the reviewer is not sure of stays a candidate; candidates/README.md says what each candidate waits on. Whether a shipped port starts on is not a judgement call: upstream-status decides it from the tool's declared facts. This page is for the contributor and for the coding agent working with them.

Who does what

Contributors write everything in git; maintainers control the pack test data, publishes and merges; reviewers decide promotions.

Pack test data is a tool's tests plus the findings the pinned tool reported on them; it is not in git. A fork's pull request runs no CI (How contribution works): check ported rules against upstream's tests and say which cases you checked and how.

Who does each step

Reading the data needs access a maintainer grants; writing it is a publish a maintainer dispatches. A branch here has the data and CI. A fork's pull request is judged once a maintainer ports it onto a branch here and publishes its data; until then, check ported rules against the tool's tests in upstream's repository.

Step Who
Write a rule of the pack's own, with its examples Contributor; needs no data
Write ported rules, their port entries, upstream facts and README sections; name their upstream tests Contributor
Write a new tool-pack's generator, manifest and licence records; a new pack's skeleton Contributor
Derive one rule's data and stage it Maintainer
Derive and stage a new tool-pack's whole set; a maintainer derives it again before publishing Contributor
Dispatch and approve the publish Maintainer
Read the Conformance comment; fix the rules, declare checked divergences, add clean samples Contributor
Promote a candidate that passes every check Contributor proposes, a reviewer decides
Merge; a new pack's first publish to npm Maintainer

Before you start

Check licences, pull the test data, and look for conflicting pull requests.

  1. Licence. The tool and its tests must be under a licence on scripts/check/licence-allowlist.json (Third-party material).

  2. Test data, if you can (Pack test data):

    sh
    npm run fixtures:pull    # -- --pack <id> for one pack; needs read access to the bucket, which a maintainer grants
  3. Open pull requests on the same pack. Land the smaller first, then merge main into the other.

Edge cases

A rule of the pack's own

A rule with no upstream needs no test data; its examples are the proof.

  1. Write packs/<id>/rules/<rule>.json (writing-rules.md, text criteria, check 8).
  2. List it in pack.json with a caught and a passing example in the rule's scope (check 3).
  3. Add its section under ## Rule details in packs/<id>/README.md (check 5).
  4. Add a changeset, its file name opening with its change type (.changeset/README.md).
  5. Run npx vitest run test/engine/catalog/packs.test.ts -t "pack <id>".

The pull request says what the examples show and what they leave out.

Edge cases

One rule into an existing tool-pack

Port one more rule of an already-ported tool as a candidate; a maintainer then publishes its data.

Copy a document of the same tool from packs/<id>/rules/. No changeset: a candidate changes nothing published.

  1. Check it can be ported against packs/<id>/upstream/<tool>/gates.yaml, the licence record and packs/<id>/candidates/README.md.
  2. Write packs/<id>/candidates/rules/<rule>.json, with the tool's suppression key on each violation (check 4).
  3. Add the port entry to packs/<id>/conformance.json (schema).
  4. Name its upstream tests where the manifest's watch.tests says; for Ruff and Bandit, add a suppression case.
  5. Add the manifest entry if missing (Declared upstream facts).
  6. Add its section under ## Rule details in packs/<id>/candidates/README.md.

A maintainer then derives the data in a sandbox, stages and publishes it (For maintainers). The Conformance comment on the published commit is the first verdict.

Step details

A new tool-pack

A tool-pack is one upstream tool in packs/<id>/upstream/<tool>/, with its own data, manifest and licence record.

See language-packs.md, "A ported pack". Models: packs/python/upstream/ruff/ (Python generator) and packs/javascript/upstream/ (ESLint-family generators).

What to write

  1. Licence record, packs/<id>/licenses/<project>/: upstream licence and UPSTREAM.md.
  2. Manifest, upstream/<tool>/gates.yaml (schema).
  3. Generator, upstream/<tool>/generate.*, with a recipe in scripts/pack-data/recipes/ (What the generators write).
  4. conformance.json with the port entries; divergences.json starts empty.
  5. Real-code samples, optional (Samples).
  6. The rules, each as above, all candidates.
What each file holds

The first data

sh
npm run fixtures:derive -- --pack <id> --tool <tool>          # in a sandbox, on the pinned runtime
npm run fixtures:stage -- --pack <id> --tool <tool> --all    # the whole set: a first publish is a `replace`
git add packs/<id>/upstream/<tool>/publish

Derive runs upstream's code: use a sandbox with no credentials (Derive in a sandbox). Then run the gate on one rule and on the pack:

sh
npm run conformance:languages -- --pack <id> --rule <rule> --fixtures   # every false positive and miss of one port
npm run conformance:languages -- --check --pack <id>
Edge cases

A port of many rules

A port that takes weeks outlives main, which keeps shipping rules to the same pack; merge main in with care.

Merge checklist

A new pack

A pack is a directory packs/<id>/ that passes the validation contract and publishes as its own npm package.

Skeleton and full example: language-packs.md, "A pack for a supported language".

  1. Skeleton (Layout).
  2. Register: npm install --package-lock-only --ignore-scripts, then npm run generate:packs.
  3. Changeset: patch (versioning).
  4. Rules: own rules, or a tool-pack of candidates.
  5. Repository lists: grep for a sibling pack's id, including rule-packs.md.

Run npx vitest run test/engine/catalog/packs.test.ts -t "pack <id>". After merge, a maintainer publishes the first version by hand, once (publishing-a-new-pack.md).

Step details

Reading the Conformance comment

Each rule's verdict is the first blocking check it fails; fix one check at a time.

CI keeps one Conformance comment per same-repository pull request (The pull request comment). Locally, with data pulled:

sh
npm run conformance:languages -- --check --pack <id>                   # the same judgement
npm run conformance:languages -- --pack <id> --rule <rule>             # every false positive and miss
npm run conformance:languages -- --pack <id> --rule <rule> --samples   # the samples too

Fix the pack's scoring errors first: no rule passes on a pack that does not score.

What to do per failing check

The run's conformance-summary artifact (gh run download <run id> -n conformance-summary) has full detail and the promotable list.

Check When it fails
loads, in-scope Widen scope to every file type the tool lints, or declare the port's own types if it covers fewer by design
compact Over 15 KiB means the document lists cases: match their shared construct
no-unexplained-false-positives, no-unexplained-misses Fix the rule. Declare a divergence only after checking the tool's answer on that line, with evidence (Divergences)
catches-something Add a true positive: a tool test, or an authored case the tool confirms. A candidate's example pair counts only once shipped
holds-under-rename Match the construct, not names. If renaming removes every finding, it fails unless the manifest gives a rename_exempt reason (the findings are the renamed names)
samples-catch-labelled, samples-silent-on-clean Fix the rule, or declare a samples divergence after checking the label (How samples are judged)
stays-silent If every upstream test for the rule has findings, add a clean real-code sample labelled for the rule (Samples). Other rules' units and Rerail's probes don't count
divergences-current The fix that makes the rule agree removes its divergence in the same pull request
upstream-status (never blocks) Add or fix the manifest entry. On or off lists shipped rules whose listing disagrees

Promoting a candidate

Promotion moves a passing candidate into rules/, with evidence for the reviewer.

Promote from the judged commit's promotable list, after confirming main has not moved (Candidates and promotion):

  1. Move the document to rules/; point the port entry at it.
  2. List it in pack.json: off if the comment's Starts column says off, with its example pair if any.
  3. Move its README section; add a changeset, added-… (its summary may be empty: the Release pull request lists new rules).
  4. Grep for the old path: git grep -F candidates/rules/<rule>.json.
  5. Re-pin the native anchors test, last: cargo test --manifest-path Cargo.toml -p rerail-engine anchors::tests.
Promotion checklist

Moving a shipped rule back

A shipped rule that fails a blocking check the same pull request does not fix goes back to candidates/.

Steps

Before you open the pull request

Run these checks, then say in the pull request what ran and what it covers.

sh
npx vitest run test/engine/catalog/packs.test.ts -t "pack <id>"   # checks 1 to 8, each rule's status
npm run conformance:languages -- --check --pack <id>
npm run check              # metric #3 is red by design while publish/ is tracked; the chain stops there, so also:
npm run check:windows-paths && npm run check:internal-refs && npm run check:pins
npm run check:native       # a shorter rules/ path in an include_str! needs cargo fmt
cargo test --manifest-path Cargo.toml -p rerail-engine anchors::tests

An own rule needs only the first and third commands. A port needs the pulled data.

Data needs and what the pull request states

An own rule in a pack without a tool-pack needs no data. A new tool-pack also needs its derived data in publish/. Grep for stale counts, table rows included, and moved rule details still saying "candidate". The pull request states:

For maintainers

Steps the publishing docs leave out: upstream.md, "Publishing pack data" and REVIEW.md cover the workflow, replace, a finish failing after upload, and approval.

Details and edge cases

Where each file is defined

The reference document for each part of a pack.

Document Defines
pack-format.md Layout, file schemas, pack data, provenance, the validation contract (checks 1 to 8)
conformance/README.md Conformance check, its comment, divergences, candidates, promotion
CONTRIBUTING.md, "Pack test data" Getting the data; adding or changing a pack
language-packs.md, "Start a pack" New pack skeleton with example; what a ported pack adds
upstream.md Upstream snapshots, generators, samples, publish workflow
writing-rules.md, code-rule DSL reference Writing a rule document
publishing-a-new-pack.md A new pack's first npm publish (maintainers)