diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 06ae175..fbd5b0c 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -353,13 +353,14 @@ files.glob = ["*.yml", "*.yaml"] `inherit.sets` names bundled sets to inherit; it does not add settings. There is no `true` shorthand — naming the sets is cheap, and what a repository -inherits should be written in the repository. Twenty-one are compiled into the +inherits should be written in the repository. Twenty-two are compiled into the binary and mirrored in [`policy/base/`](../policy/base), each **named by what it refuses** so the name predicts the rule list: | set | refuses | |---|---| -| `process-residue` | authoring and process residue in committed content — conflict markers, home paths, dated and status metadata, tracker and thread references, private data paths — and the residue a process leaves in the policy file itself: a rule transcribed out of a set. **Installs `pre-commit` and `manual`**, and the two report different things | +| `process-residue` | authoring and process residue in committed content — conflict markers, home paths, dated and status metadata, tracker and thread references in documentation, private data paths — and the residue a process leaves in the policy file itself: a rule transcribed out of a set. **Installs `pre-commit` and `manual`**, and the two report different things | +| `code-residue` | a tracker reference in a source, configuration, build or packaging file: the tracker rule of `process-residue` over every file that is not Markdown, reST or plain text, under its own id. Its own set because a scope is not something `[inherit]` lets a repository choose, and the release that widened the rule in place was undone by hand in every tree that measured what it reported. For a tree whose comments already cite durable contracts. **Installs `pre-commit` and `manual`**, the same ceiling as the set it was split from | | `credentials` | credential material — private keys and service tokens, literal credential values, populated environment files, browser profile and session stores. A literal credential value is two rules split on the quote: in source the value half must be a quoted literal, so `password: modem_config.password.clone()` and `token = raw.trim_start_matches('v')` are expressions and not findings; in a config file — `.env`, INI, YAML, TOML, JSON, XML, properties — the text after the separator is the value whether quoted or not, and `-in-config` reads it unquoted | | `unmanaged-pins` | a version pinned where no manifest holds it — a shell install line, a `releases/download/vX.Y.Z` URL, a versioned `curl` or `wget` | | `host-identity` | the machine the author is standing on — its username, home path, hostname and default route, read at scan time and searched for in content | @@ -562,18 +563,37 @@ Each is named separately because taking one is a separate decision: has on purpose, `host-identity` shells out to read the running machine, and `captured-fixtures` refuses the script a parser's own test corpus is made of. The `process-residue` set rejects GitHub issue and PR URLs and numbered tracker -references in source, configuration, and documentation, including -systemd `Documentation=` fields. Keep the durable explanation in the repository; -issues can point to code, but code must not require an issue to explain it. -Captured logs, bug reports, and benchmark results belong in issues. Benchmark -programs and synthetic fixtures belong in the repository. Git history owns edit -dates and change history; tracked files describe the current contract. +references in documentation — Markdown, reST and plain text — under +`no-task-tracker-references`. The same pattern over every other tracked file — +source, configuration, scripts, packaging, including systemd `Documentation=` +fields — is `no-task-tracker-references-in-code`, the one rule of the +`code-residue` set, and the two exclude each other's files so a line is under +exactly one of them. The scopes are two sets rather than one rule because a +scope is not something `[inherit]` lets a repository choose: it takes sets +whole and disables rules by id. The release that widened the docs rule to every +file in place was undone by hand in every tree that measured what it reported, +a docs-only copy under the same id in each; a set is the opt-in a copy was +standing in for. Keep the durable explanation in the repository; issues can +point to code, but code must not require an issue to explain it. Captured +logs, bug reports, and benchmark results belong in issues. Benchmark programs +and synthetic fixtures belong in the repository. Git history owns edit dates +and change history; tracked files describe the current contract. + +`no-process-history-references`, in the same set and over the same +documentation globs, refuses the narrative form alone — a sentence that says a +point was settled in a discussion, in an issue, a thread or a PR, and names no +record — and nothing the tracker rule reads. It used to carry the tracker URL +and the `issue #N` form too, so a +documentation line in either was reported twice under two ids, and a consumer +reading a doubled report disabled one of the rules. The two are disjoint now: +the record-naming forms, in any case of `issue` and `pr`, are the tracker +rule's. The static checks recognize tracker references, authoring-date headers, changelog filenames, and log filenames. Identifying a pasted bug report, benchmark result, or narrative edit history requires review; these checks do -not claim to recognize arbitrary prose. The tracker rule retains the set's test -directory and Go test exclusions for synthetic examples, and excludes `testdata`. +not claim to recognize arbitrary prose. Both tracker rules retain the set's test +directory and Go test exclusions for synthetic examples, and exclude `testdata`. Date and log rules exclude `tests/fixtures`, `test/fixtures`, and `testdata`. Unqualified all-uppercase identifiers are not treated as repository shorthand, and bare numbers need prose context, an opening delimiter, or a list separator diff --git a/policy/base/code-residue.toml b/policy/base/code-residue.toml new file mode 100644 index 0000000..a854f8c --- /dev/null +++ b/policy/base/code-residue.toml @@ -0,0 +1,86 @@ +# Base rule set: code-residue (a tracker reference in a source, configuration, +# build or packaging file -- the every-file scope of the rule that +# `process-residue` runs over documentation) +# +# The pattern `no-task-tracker-references` reads over Markdown, reST and plain +# text, read here over everything else a repository tracks: source, unit +# files, scripts, manifests, lockfiles. Pull in with: +# +# [inherit] +# sets = ["code-residue"] +# +# WHY THIS IS ITS OWN SET rather than the scope of the rule in +# `process-residue`. For one release it was: the documentation glob came off +# the tracker rule and the rule read every tracked file, in every repository +# that inherited the set, with no diff in any of them. Two consumers under one +# superproject measured what that cost. The first run under the wider scope +# reported 27 findings on a pointer bump that touched none of the reported +# lines; a sibling counted 276 across its source, packaging and scripts on a +# commit that touched none of them either. Both shadowed the rule with a +# docs-only copy under the same id, which is one release's widening undone by +# hand in every tree that noticed. A third superproject, whose source comments +# cite an issue as the place a measurement lives, disabled the rule in every +# policy it has. Each of those was a consumer choosing between a rule that +# refuses every commit and no rule at all, because a scope is not something +# `[inherit]` lets a repository choose: it inherits SETS, whole, and disables +# rules by id. Splitting the scope across two sets is what puts the choice +# back where the consumer can make it. +# +# WHO TAKES IT. A tree whose comments have already been rewritten to cite +# durable contracts -- a document in the repository, a named constant, the +# test that proves the behaviour -- rather than the tracker item that once +# argued for them. There the rule finds nothing on the day it is taken and +# refuses the next comment that reaches for an issue number instead of an +# explanation. A tree that has not done that rewriting takes `process-residue` +# alone and keeps the tracker rule on its documentation, which is the scope +# the rule shipped with and the one it is back on. +# +# THE TWO RULES ARE DISJOINT BY FILE, not by pattern. Both compile the same +# expression; this one excludes the three documentation globs the other +# selects, and both exclude the same test directories and fixture trees. So a +# line is under exactly one of them, a repository that inherits both sets +# never sees the same line reported under two ids, and a pattern edit lands in +# both or in neither. +# +# THE FIXTURE EXCLUSIONS ARE THE ONES `process-residue` STATES. A Go test +# sits beside its subject as `_test.go`, a Python or JavaScript test +# under `tests/`, and a synthetic fixture under `testdata/`; each legitimately +# carries example-bad content and each is left out here for the reason argued +# at the head of that set. + +# WHAT THIS SET MAY INSTALL. `stages` is the ceiling on the hooks a rule here +# may declare, and it is the ceiling `process-residue` carries, because this +# set is one of that set's rules under another scope and a rule that moves +# between the two -- which is how this set came to exist -- must find the same +# ceiling on both sides. Nothing here reaches it: the one rule is a content +# rule the scan owns, and it declares no hook. A rule added to this set that +# reached past this line would be refused at load. +[set] +stages = ["pre-commit", "manual"] + +[rule.no-task-tracker-references-in-code] +message = """ +Source, configuration, and build files must not depend on volatile issue or +PR records. State the durable contract and rationale in a comment, or link to +repository documentation. Issues may link to code; code must not depend on +issues. Keep logs, bug reports, and benchmark results in issues, and edit +chronology in Git history. +""" +# The expression is the one `no-task-tracker-references` compiles, arm for +# arm, and the argument for each arm is written above that rule: what an +# unqualified repository name needs, what a bare number needs before it, why a +# list separator counts, and why the words `issue` and `pr` are read in any +# case. It is duplicated rather than referenced because a rule is a table of +# its own and there is nothing in the policy language to point one rule's +# pattern at another's; the corpus holds the two to the same samples. +regexp = 'github\.com/[^[:space:])]+/(?:issues|pull)/[0-9]+|\b(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_.-]*[a-z][A-Za-z0-9_.-]*)#[0-9]+\b|(?:^|\b[a-z]+[\t ]+|(?i:\b(?:issue|pr)[\t ]+)|\(|\[|[,;/][\t ]*)#[0-9]+\b' +files.include = ["."] +files.exclude = [ + "*.md", + "*.rst", + "*.txt", + "**/tests/**", + "**/test/**", + "**/*_test.go", + "**/testdata/**", +] diff --git a/policy/base/process-residue.toml b/policy/base/process-residue.toml index 3e3b98f..9e0ec28 100644 --- a/policy/base/process-residue.toml +++ b/policy/base/process-residue.toml @@ -130,9 +130,20 @@ chronology in Git history. # standards designators as repositories or task-tracking prose. The separators # are admitted because a citation list names its repository once and leaves # every later item bare, and a comma directly before a hash number in a -# comment is such a list far more often than anything else. -regexp = 'github\.com/[^[:space:])]+/(?:issues|pull)/[0-9]+|\b(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_.-]*[a-z][A-Za-z0-9_.-]*)#[0-9]+\b|(?:^|\b[a-z]+[\t ]+|\(|\[|[,;/][\t ]*)#[0-9]+\b' +# comment is such a list far more often than anything else. The words `issue` +# and `pr` are admitted in any case, because a sentence that opens with one of +# them capitalised is the commonest way a document cites a tracker, and the +# lowercase prose-word arm reads past it. +# +# THE GLOB IS THE SCOPE THIS RULE HAD BEFORE IT WAS WIDENED, AND IT IS BACK ON +# PURPOSE. For one release the rule read every tracked file, and what that cost +# the consumers who measured it is written at the head of `code-residue`, the +# set that now carries the every-file scope under its own id. That rule and +# this one exclude each other's files, so a line is under exactly one of them, +# and a repository takes the wider one by name rather than by version bump. +regexp = 'github\.com/[^[:space:])]+/(?:issues|pull)/[0-9]+|\b(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_.-]*[a-z][A-Za-z0-9_.-]*)#[0-9]+\b|(?:^|\b[a-z]+[\t ]+|(?i:\b(?:issue|pr)[\t ]+)|\(|\[|[,;/][\t ]*)#[0-9]+\b' files.include = ["."] +files.glob = ["*.md", "*.rst", "*.txt"] files.exclude = ["**/tests/**", "**/test/**", "**/*_test.go", "**/testdata/**"] [rule.no-process-history-references] @@ -140,7 +151,14 @@ message = """ Do not reference process-history records (issue/PR/thread numbers) in final docs. Extract the durable rule or final state instead. """ -regexp = '(?i)\b(?:issue|pr)\s+#[0-9]+\b|\bas discussed in (?:an? )?(?:issue|thread|pr)\b|(?:https?://)?github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(?:issues|pull)/[0-9]+\b' +# One arm, the narrative form: a sentence that points at a conversation rather +# than at a record. The forms that name a record -- a tracker URL, and the word +# `issue` or `pr` before a hash number -- are `no-task-tracker-references` +# above, over the same files, and used to be here as well, so a line in either +# form was reported twice under two ids. A consumer reading a doubled report +# disables one of the rules, and the disabled one was this set's whole coverage +# of the number-free form. What each rule refuses is now disjoint. +regexp = '(?i)\bas discussed in (?:an? )?(?:issue|thread|pr)\b' files.include = ["."] files.glob = ["*.md", "*.rst", "*.txt"] files.exclude = ["**/tests/**", "**/test/**", "**/*_test.go"] diff --git a/policy/base/sets.lock.json b/policy/base/sets.lock.json index 3febb8d..097c677 100644 --- a/policy/base/sets.lock.json +++ b/policy/base/sets.lock.json @@ -108,7 +108,7 @@ }, "id": "no-process-history-references", "message": "Do not reference process-history records (issue/PR/thread numbers) in final\ndocs. Extract the durable rule or final state instead.\n", - "regexp": "(?i)\\b(?:issue|pr)\\s+#[0-9]+\\b|\\bas discussed in (?:an? )?(?:issue|thread|pr)\\b|(?:https?://)?github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(?:issues|pull)/[0-9]+\\b" + "regexp": "(?i)\\bas discussed in (?:an? )?(?:issue|thread|pr)\\b" }, { "files": { @@ -156,6 +156,11 @@ "**/testdata/**" ], "exclude_cfg_test": false, + "glob": [ + "*.md", + "*.rst", + "*.txt" + ], "include": [ "." ], @@ -164,7 +169,7 @@ }, "id": "no-task-tracker-references", "message": "Source, configuration, and documentation must not depend on volatile\nissue or PR records. State the durable contract and rationale here, or link to\nrepository documentation. Issues may link to code; code must not depend on\nissues. Keep logs, bug reports, and benchmark results in issues, and edit\nchronology in Git history.\n", - "regexp": "github\\.com/[^[:space:])]+/(?:issues|pull)/[0-9]+|\\b(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_.-]*[a-z][A-Za-z0-9_.-]*)#[0-9]+\\b|(?:^|\\b[a-z]+[\\t ]+|\\(|\\[|[,;/][\\t ]*)#[0-9]+\\b" + "regexp": "github\\.com/[^[:space:])]+/(?:issues|pull)/[0-9]+|\\b(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_.-]*[a-z][A-Za-z0-9_.-]*)#[0-9]+\\b|(?:^|\\b[a-z]+[\\t ]+|(?i:\\b(?:issue|pr)[\\t ]+)|\\(|\\[|[,;/][\\t ]*)#[0-9]+\\b" }, { "files": { @@ -191,6 +196,38 @@ "manual" ] }, + { + "commands": [], + "rules": [ + { + "files": { + "exclude": [ + "*.md", + "*.rst", + "*.txt", + "**/tests/**", + "**/test/**", + "**/*_test.go", + "**/testdata/**" + ], + "exclude_cfg_test": false, + "include": [ + "." + ], + "multiline": false, + "word": false + }, + "id": "no-task-tracker-references-in-code", + "message": "Source, configuration, and build files must not depend on volatile issue or\nPR records. State the durable contract and rationale in a comment, or link to\nrepository documentation. Issues may link to code; code must not depend on\nissues. Keep logs, bug reports, and benchmark results in issues, and edit\nchronology in Git history.\n", + "regexp": "github\\.com/[^[:space:])]+/(?:issues|pull)/[0-9]+|\\b(?:[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|[A-Za-z0-9_.-]*[a-z][A-Za-z0-9_.-]*)#[0-9]+\\b|(?:^|\\b[a-z]+[\\t ]+|(?i:\\b(?:issue|pr)[\\t ]+)|\\(|\\[|[,;/][\\t ]*)#[0-9]+\\b" + } + ], + "set": "code-residue", + "stages": [ + "pre-commit", + "manual" + ] + }, { "commands": [], "rules": [ diff --git a/policy/principles.toml b/policy/principles.toml index 1b8eeab..7fc05d5 100644 --- a/policy/principles.toml +++ b/policy/principles.toml @@ -86,6 +86,12 @@ private_owners_optional = true # rules. sets = [ "process-residue", + # The tracker rule over source, scripts and policy files, which + # `process-residue` keeps to documentation. Taken here because this tree's + # comments already cite the durable thing -- an ADR, a test, a named + # constant -- and the tree was clean under the every-file scope on the day + # the split shipped it as a set of its own. + "code-residue", "host-identity", "broken-links", "doc-claims", diff --git a/src/config.rs b/src/config.rs index 922b73d..373123a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,15 @@ pub(crate) const BUNDLED: &[(&str, &str)] = &[ "process-residue", include_str!("../policy/base/process-residue.toml"), ), + // The tracker rule of `process-residue` over every file that is not + // documentation. Its own name because a scope is not something `[inherit]` + // lets a repository choose -- it takes sets whole and disables rules by id + // -- and the release that widened the rule in place was undone by hand in + // every tree that measured what it reported. + ( + "code-residue", + include_str!("../policy/base/code-residue.toml"), + ), ( "credentials", include_str!("../policy/base/credentials.toml"), diff --git a/tests/base_set_corpus.rs b/tests/base_set_corpus.rs index e41f1c9..1f02235 100644 --- a/tests/base_set_corpus.rs +++ b/tests/base_set_corpus.rs @@ -45,6 +45,41 @@ struct Case { allows: &'static [&'static str], } +/// The lines both tracker rules refuse: the one `process-residue` runs over +/// documentation and the one `code-residue` runs over everything else. One +/// list, because the two compile the same expression and a sample added to one +/// scope and not the other would be the drift between them nobody sees. +const TRACKER_REFUSES: &[&str] = &[ + "See github.com/acme/widget/issues/12 for the argument.\n", + "Fixed in #451.\n", + "# See widget#711.\n", + "# See acme/widget#711.\n", + "Split across two (widget#7, #225).\n", + // A hit is one line, so the list form above cannot show that its second + // item was read: nothing else on these lines can match, and each one is + // refused by the separator alone. + "Two more landed beside it, #225.\n", + "The first half is in the estate; #8 has the rest.\n", + "#7/#8 are the pair.\n", + "Under the widget/#8 as well.\n", + // The capitalised forms, which the lowercase prose-word arm reads past. + // They were the history rule's until its record-naming arms moved here, + // and a sample for each is what says the move lost nothing. + "Issue #5 has the measurement.\n", + "PR #7 landed the rest.\n", +]; + +/// The near misses the tracker pattern was written to let through. +const TRACKER_ALLOWS: &[&str] = &[ + "The rule is stated here rather than in a tracker.\n", + // The forms the bare arm stays narrow for: a heading level, a colour, a + // unit, and a number a capitalised word introduces. + "## Heading\n", + "color: #fff\n", + "border: 1px, #1px wide\n", + "The #4 seed plays first.\n", +]; + /// What a set needs written beside `[inherit]` before it will load. /// /// A set that supplies checkers and never shims is refused in a repository that @@ -117,33 +152,16 @@ const CORPUS: &[Case] = &[ refuses: &["Status: draft\n"], allows: &["The status of a record is a field in the record.\n"], }, + // The tracker rule, over documentation. The same lines under `code-residue` + // below are the same pattern over every other file, and `TRACKER_REFUSES` + // and `TRACKER_ALLOWS` are shared so the two cannot drift apart a sample at + // a time. Case { set: "process-residue", rule: "no-task-tracker-references", path: "sample.md", - refuses: &[ - "See github.com/acme/widget/issues/12 for the argument.\n", - "Fixed in #451.\n", - "# See widget#711.\n", - "# See acme/widget#711.\n", - "Split across two (widget#7, #225).\n", - // A hit is one line, so the list form above cannot show that its - // second item was read: nothing else on these lines can match, and - // each one is refused by the separator alone. - "Two more landed beside it, #225.\n", - "The first half is in the estate; #8 has the rest.\n", - "#7/#8 are the pair.\n", - "Under the widget/#8 as well.\n", - ], - allows: &[ - "The rule is stated here rather than in a tracker.\n", - // The forms the bare arm stays narrow for: a heading level, a - // colour, a unit, and a number a capitalised word introduces. - "## Heading\n", - "color: #fff\n", - "border: 1px, #1px wide\n", - "The #4 seed plays first.\n", - ], + refuses: TRACKER_REFUSES, + allows: TRACKER_ALLOWS, }, Case { set: "process-residue", @@ -151,11 +169,21 @@ const CORPUS: &[Case] = &[ path: "notes.rst", refuses: &[ "as discussed in a thread, this is the answer\n", - "issue #12 covers it\n", - "https://github.com/acme/widget/pull/7 has the detail\n", + "As discussed in PR, the count is taken once.\n", ], + // The record-naming forms this rule used to refuse as well are still + // refused in this file, by the tracker rule and once; the test on a + // single id below is where that is asserted, because an `allows` line + // here would be asking the SET to pass them. allows: &["The decision and its reason are both written down here.\n"], }, + Case { + set: "code-residue", + rule: "no-task-tracker-references-in-code", + path: "src/estate.rs", + refuses: TRACKER_REFUSES, + allows: TRACKER_ALLOWS, + }, Case { set: "process-residue", rule: "no-tracked-private-data-paths", @@ -598,8 +626,8 @@ fn tracker_references_are_refused_in_configuration_and_source() { "scripts/build", ] { let case = Case { - set: "process-residue", - rule: "no-task-tracker-references", + set: "code-residue", + rule: "no-task-tracker-references-in-code", path, refuses: &[], allows: &[], @@ -623,6 +651,81 @@ fn tracker_references_are_refused_in_configuration_and_source() { } } +/// The lines of a report that open a finding under exactly this id. +/// +/// Counted against the whole line, because one tracker id is a prefix of the +/// other: `contains` on the shorter id is satisfied by a report that names only +/// the longer one, which is the confusion this test exists to rule out. +fn findings_under(report: &str, rule: &str) -> usize { + let opener = format!("policy check failed: {rule}"); + report.lines().filter(|line| *line == opener).count() +} + +#[test] +fn a_tracker_reference_is_under_exactly_one_rule_whichever_file_holds_it() { + // A repository that inherits both sets. The docs rule and the code rule + // compile the same expression and exclude each other's files, so the same + // line is reported once under the id for the file it is in -- never under + // both, which was the doubled report that had consumers disabling one. + for (path, refused_by, not_by) in [ + ( + "notes.md", + "no-task-tracker-references", + "no-task-tracker-references-in-code", + ), + ( + "src/estate.rs", + "no-task-tracker-references-in-code", + "no-task-tracker-references", + ), + ] { + let root = repository("[inherit]\nsets = [\"process-residue\", \"code-residue\"]\n"); + let file = root.join(path); + std::fs::create_dir_all(file.parent().unwrap()).unwrap(); + std::fs::write(&file, "Fixed in #451.\n").unwrap(); + support::git(&root, &["add", "-A"]); + let output = scan(&root); + let report = String::from_utf8_lossy(&output.stderr).into_owned(); + let _ = std::fs::remove_dir_all(&root); + assert_eq!(output.status.code().unwrap(), 1, "{path}: {report}"); + assert_eq!(findings_under(&report, refused_by), 1, "{path}: {report}"); + assert_eq!(findings_under(&report, not_by), 0, "{path}: {report}"); + } +} + +#[test] +fn a_docs_line_naming_a_record_is_reported_once_and_under_the_tracker_id() { + // The de-duplication inside `process-residue`. Each of these was refused + // by both rules of the set, so a reader saw the same line twice under two + // ids; the record-naming arms now live in the tracker rule alone and the + // history rule keeps the narrative form nothing else reads. + for sample in [ + "issue #12 covers it\n", + "Issue #5 is where the measurement lives\n", + "https://github.com/acme/widget/pull/7 has the detail\n", + ] { + let case = Case { + set: "process-residue", + rule: "no-task-tracker-references", + path: "notes.rst", + refuses: &[], + allows: &[], + }; + let (code, report) = verdict(&case, sample); + assert_eq!(code, 1, "{sample:?}: {report}"); + assert_eq!( + findings_under(&report, case.rule), + 1, + "{sample:?}: {report}" + ); + assert_eq!( + findings_under(&report, "no-process-history-references"), + 0, + "{sample:?}: {report}" + ); + } +} + #[test] fn a_quoted_credential_in_a_config_file_is_one_finding_and_not_two() { // The source rule's exclude and the config rule's glob have to name the