Skip to content

Truth Rules

github-actions[bot] edited this page Oct 30, 2026 · 3 revisions

Truth Rules

The truth check of nself-ci keeps false facts out of a repository. It reads rule files, scans the files of the repository and reports every line that breaks a rule. It never touches the network. The engine is free/ci/internal/gates/truth; the org rules are data in .github/truth/.

Rule files

File Holds Kinds
.github/truth/org.yaml dead hosts, retired terms, banned literals dead-host, retired-term, banned-literal
.github/truth/claims.yaml hand-typed counts, current-version literals, retired vocabulary numeric-claim, version-claim, plus two vocabulary rules

The claim kinds live in their own file so that a pinned nself-ci release that predates them keeps parsing org.yaml. A repo lists claims.yaml only after its pinned release carries the claim kinds.

A repo enforces the rules with .nself/ci.yaml:

version: 1
checks:
  truth:
    rules: [.github/truth.yaml]

and a .github/truth.yaml that includes the vendored org rules and adds its own exemptions:

version: 1
include: [.github/truth/org.yaml, .github/truth/claims.yaml]
overrides:
  claims.current-version:
    facts: .github/facts.json
    exempt_paths:
      - {glob: "docs/history/**", reason: dated notes keep the version that was current then}

Rule format

All rule files are version: 1. A rule has id, kind, message (what to write instead) and optional paths (globs; default every file). Unknown keys, duplicate ids, bad patterns, exemptions without a reason and rule files outside the repository are E644 and name the file and key.

Key Kinds Meaning
pattern numeric-claim, version-claim RE2, matched case-insensitively against one line or two adjacent lines. Required, with paths.
facts version-claim Repo-relative JSON or YAML file with a top-level version. A literal equal to it passes (a leading v is ignored).
exempt_paths all [{glob, reason}]: files the rule skips.
exempt_matches all but dead-host [{regex, reason}]: a hit inside a match of the regex is exempt.
exempt_regions retired-term, banned-literal true makes the rule honour the markers below (the claim kinds always do).
term ending in (s) retired-term Matches with and without the plural s.

A reason has at least 8 characters.

Exempting a hit

A claim rule (and a vocabulary rule that sets exempt_regions: true) skips a hit that sits in:

  1. A generated region: <!-- BEGIN GENERATED:<id> --> to <!-- END GENERATED:<id> --> (in .mdx: {/* BEGIN GENERATED:<id> */}). Both markers and a declared id must be present; an unclosed or repeated BEGIN exempts nothing.
  2. A JSX expression {...} in an .astro or .mdx file. Braces must balance within a paragraph, inline code and code fences do not count, and an expression that is only a quoted string or a template literal without ${} is typed text, so it is not exempt. Astro frontmatter is scanned conservatively, including strings and comments; <script> and <style> blocks remain skipped.
  3. The line after a comment that is the whole line: <!-- truth: exempt <reason> --> (in .mdx: {/* truth: exempt <reason> */}). Text that merely mentions the marker, in prose, inline code or a fence, exempts nothing. A comment with no reason (fewer than four letters or digits) is a finding (truth.exempt-without-reason) and exempts nothing.
  4. A reasoned exempt_paths entry for a generated file. A GENERATED BY header alone grants no exemption.

Claim rules never read Go source, go.mod, go.sum or go.work, and skip changelogs, release notes (release-notes/, releases/v*), blog posts (content/blog, blog/*.md), ADRs, history folders, testdata, vendor and node_modules by default. They apply to prose paths only: Markdown, MDX, Astro (including frontmatter), TSX, JSX, HTML, README*, text files, i18n JSON values and packaging descriptions. TSX, JSX and frontmatter are scanned conservatively as text, including comments. Path globs ignore case, including exemption paths.

Generated region ids must be declared in a loaded rule file with a reason of at least eight characters. The shipped rules declare only catalog; repositories can declare ids that their generators own:

generated_regions:
  facts: generated by the repository facts generator and checked in CI

Unknown region ids exempt nothing. A header saying GENERATED BY also exempts nothing; use declared region markers or a reasoned path exemption instead.

Binary, UTF-16 and files over 4 MiB still run through dead-host matching up to a bounded 16 MiB read. Unscannable prose in a claim rule's scope produces truth.unscanned-file; a file above the host limit produces the same finding when any rule applies. NUL bytes anywhere in a file trigger this handling. Only explicitly excluded paths bypass it. Finding paths and matches that contain a dead host are redacted. Zero-width spaces, joiners, word joiners and BOMs cannot hide matches. Claims can span two adjacent lines, and the shipped count rule also catches labelled counts such as Plugins: 62.

Known limits: claims split over more than two lines, full-width digits, all 62 free and paid plugins and 62 <b>plugins</b> are outside the count pattern. Astro <script> and <style> blocks and dynamic expressions in Astro/MDX remain exempt.

Overrides

A rule file may carry an overrides block keyed by rule id. It adds exempt_paths (and, for a version-claim, facts) to a rule declared in that file or in a file it includes, directly or not. An override that names an unknown rule id, or a rule the file does not include, is E644. facts is resolved against the scanned root, and a facts file reached through a symlink out of the tree is refused.

The shipped claim rules

Id Kind Catches Write instead
claims.counts numeric-claim a count of plugins, commands, verbs, bundles or integrations render it from the generated facts, or use words
claims.counts-ar numeric-claim the same count in Arabic i18n values (Western or Arabic-Indic digits) render it from the generated facts
claims.current-version version-claim a version typed next to "nself" or "current/latest release" render it from the facts file
vocab.pro-plugin retired-term the retired phrase for licensed plugins "licensed plugin(s)"
vocab.bundles-repo-slug banned-literal the old name of the Bundles repository nself-org/bundles

vocab.bundles-repo-slug exempts Go module paths (exempt_matches): renaming a module path is a breaking change and GitHub redirects the old name.

Examples (each line below is exempt from the rules it shows, so this page stays clean):

  • fails: We ship 129 free plugins.
  • passes: a line preceded by <!-- truth: exempt a quoted line from the 2019 launch post -->

Output

Human mode prints one line per finding, then the rule's message:

truth: <rule-id> <file>:<line>: <match>
    fix: <message>

A dead-host finding prints a hash prefix, never the host. --json prints one document with data.result (pass, fail, error), per-check results and the findings.

Running the check

From free/ci of this repository (the plugin binary does not link the check until the V15 engine does):

go run -mod=vendor ./internal/gates/truth/run.go check --root DIR [--json] [--kind K|rules] RULEFILE...

Exit 0 pass, 1 findings, 2 rule-file or scan error. --kind rules validates the files without scanning.

Vendoring the rules

bash scripts/truth/vendor-rules.sh TARGET_DIR          # writes TARGET_DIR/.github/truth/{org,claims}.yaml
bash scripts/truth/vendor-rules.sh --check TARGET_DIR  # exit 1 when a copy is missing or differs

Each copy starts with # VENDORED FROM nself-org/plugins@<sha> — DO NOT HAND EDIT, then this checkout's bytes unchanged. --check ignores the header line. The truth-sweep drill runs --check on every public clone that carries .github/truth/, so a drifted copy fails the nightly sweep.

nself-plugins

Start

Commands

Architecture

Plugin Ecosystem

Popular Plugins

View All: Home (or see the full alphabetical list below, synced with registry.json)

All Free Plugins

CI

Guides

Operations


Related

Clone this wiki locally