Scoping
A rule never judges the whole repository. It narrows from the walked index to a specific set of files through three gates applied in a fixed order: when: decides whether the rule runs at all, paths: selects files by glob, and scope_filter: refines that selection per file. Only what survives all three is evaluated.
The three gates
Section titled “The three gates”when: is a run-wide switch, not a per-file filter. It is a bounded boolean expression over four namespaces (facts., vars., env., and iter. inside iteration contexts), evaluated once per run against facts computed once. If it is false, the whole rule is dropped before a single file is read, so a rule gated on an absent ecosystem costs nothing. A missing fact reads as null (falsy).
paths: is the primary selector: a glob, a list of globs, or an {include, exclude} pair matched against the walked index. It answers “which files is this rule about.”
scope_filter: refines that per file, for the cases a glob cannot express. Its predicates AND-compose: when more than one is set, a file must satisfy all of them. At least one predicate must be present. Cross-file rules (pair, for_each_dir, and their siblings) reject scope_filter: at build time, with a pointer to the for_each_dir + when_iter: pattern instead; the rule-major per-file kinds (filename_case, file_max_size, and their siblings) honor it as of v0.15.
scope_filter predicates
Section titled “scope_filter predicates”has_ancestor:keeps a file only when a named manifest sits somewhere in its ancestor directory chain. The engine walksPath::parent()upward (the file’s own directory counts) and stops at the first match, so a content rule scopes to just its ecosystem’s subtree in a polyglot monorepo. The bundled ecosystem rulesets use it to confine per-file rules to their package subtrees.changed_since: <git-ref>keeps only files in the<ref>...HEADmerge-base diff, the form--changed --base=<ref>uses (bare--changeduses a working-tree diff instead). It accepts{{env.X}}interpolation and resolves the diff once per run.include_manifest_paths:/exclude_manifest_paths:scope by membership in a path set a manifest declares (aCargo.tomlworkspace.members, apackage.jsonbin), so the manifest that owns the truth and the rule that depends on it stay in one place. An optionalderive_target: { from, to }regex maps a declared build output back to its source (dist/cli.jsback tosrc/cli.ts);expect_nonempty:(defaulttrue) warns when an include set resolves to nothing rather than silently matching no files.
A manifest value only gates which files a rule sees, never what it decides about them: extraction is pure parsing (no spawn, so it is safe inside an extends:’d ruleset), and alint explain <rule> prints the resolved set.
In practice
Section titled “In practice”Require an SPDX header, but only on the Rust files a PR actually touched, and only in a repo that has Rust at all:
version: 1facts: - id: has_rust any_file_exists: [Cargo.toml]rules: - id: spdx-on-changed-rust kind: file_header when: facts.has_rust paths: "**/*.rs" pattern: "^// SPDX-License-Identifier:" scope_filter: has_ancestor: Cargo.toml changed_since: "{{env.ALINT_BASE_SHA | default('origin/main')}}" level: error message: "add an SPDX-License-Identifier header"On a PR that adds one unheadered crates/core/src/new.rs:
error spdx-on-changed-rust add an SPDX-License-Identifier headerUnchanged Rust files, and every file outside a Cargo.toml subtree, are never considered.
Going deeper
Section titled “Going deeper”- Configuration is the field reference for every
scope_filter:predicate and its options. - The walker and git is the index these gates narrow, and where
git_tracked_only:is defined. - Changed mode is the run-wide
--changedcounterpart to the per-rulechanged_since:predicate.