Skip to content

Harden query evaluation: refused reads fail closed, never silently - #759

Open
awsmadi wants to merge 29 commits into
aws-cloudformation:mainfrom
awsmadi:evaluator-hardening
Open

awsmadi wants to merge 29 commits into
aws-cloudformation:mainfrom
awsmadi:evaluator-hardening

Conversation

@awsmadi

@awsmadi awsmadi commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Issue #, if available:

Description of changes:

The evaluator could pass or skip a clause it had no way to decide on the data it was given, and say nothing. A filter straight after [*] over a map holding a stray scalar was asked of the document instead of the entries, selected nothing, and the rule reported SKIP at exit 0. A map-key filter whose right-hand side the document never supplied selected no key and checked nothing. In an earlier state of this branch a not over a rule that failed only on a refused read turned that failure into a PASS. A template key holding an ESC or a bidi override could open a terminal escape sequence or reverse the rest of a report line. A rule that failed on its own when had no check under it to say so, so the sarif report had no result for it. I made the [*] filter decide on the map it was given, made the refused reads fail the rule closed with a reason, made every report line printable, and made the output formats carry the same reasons and the same failing rules.

What a rules author sees:

  • A read the evaluator refuses fails the rule closed at exit 19. The reason is recorded at the value it refused and carried whole into the console, the -o json, yaml, sarif and junit reports, and the cfn-guard test output, where a rule whose when failed closed names the refusal on its line as Reason.
  • A filter through a variable, %v[ ... ], reads the map v holds by its shape, classified as %v[*][ ... ] classifies it, in the cases docs/CLAUSES.md states, and parts from [*] where the [*] rule refuses or enumerates what cfn-guard 3.2.1 decided on the map. A map with no scalar entry is a collection and walks its entries, as the literal path and [*] do, except where it holds the predicate's key (alone, beside a member map that holds it too, or as the name of a member among members sharing a key) or holds one member map that lacks the key an absence predicate names: there it is one object, as 3.2.1 read it, while [*] keeps its walk and the literal K[ ... ] keeps its walk or its refusal. Those exceptions hold below the template's top-level sections only: a binding of a section itself (let r = Resources, let p = Parameters) keeps the walk whatever the member count, so %r[ DeletionPolicy !exists ] selects among the resources over a template of one as of two, where cfn-guard 3.2.1 read the section as one value; a let of another top-level key, a spec at a document's root, is not a section and reads by the shapes above. A map with a non-null scalar entry is one object, tested as [*] tests it, and a mixed map, a scalar beside a member that holds a key the predicate names, is one object too, as 3.2.1 read it, except where the literal K[ ... ] refuses the filter, when the variable is refused the same way; where the literal keeps one of the map's entries instead, one Warning: line per filter site says the two spellings disagree.
  • IN against a filter capture compares the left operand with the keys the capture holds as with a written-out list, over one key as over two: Prefix IN %buckets holds when the prefix is one of the keys and not when it is a substring of one (BktOne holds over the keys BktOne and BktTwo, Bkt does not). A string literal on the left, inline or let-bound, reads the same way. cfn-guard 3.2.1 reads a document value on the left the same way; it read a bare literal on the left as a key path and a let-bound literal against every key at once. A query rooted at the capture carries its enumeration too (%lit IN %t.Value after Tags[ t | Key == 'Env' ] is membership in the values), and a bare literal on the left of such a query is a key path, as everywhere.
  • A line the tool writes about a template key, path or value, on the console, on stderr and in the reports, is one printable line: a control, a bidirectional control or a zero-width character is written as its Rust escape (\n, \u{1b}, \u{202e}).
  • A not over a rule that failed only on refused reads fails closed and names the reads it rests on; a FAIL that is a decided no (a conjunct that answered no beside the refused read) is inverted to a pass as before.
  • The full behavior list is docs/CLAUSES.md in this diff, its behavior-change callouts and the tables that name each pinned clause's test, and the limits are docs/KNOWN_ISSUES.md.

How I tested it. The cell tables in guard/src/rules/eval_tests.rs pin each clause by name, one row per spelling over a document that shows the shape, most with the opposite polarity or a control beside them. The fixture rules and templates under guard/resources/validate/ and guard/resources/test-command/ run through guard/tests/validate.rs and guard/tests/test_command.rs across the console and the structured reporters. The table in guard/resources/validate/wildcard-filter-release-verdicts/README.md records, per rules file and document, what the released binaries answered beside what this branch answers; the branch column is asserted by a validate test, the release columns are recorded. CI runs .github/scripts/check-registry-corpus.sh over aws-guard-rules-registry at the pinned commit 7f7340c2 and holds the result against the state files under .github/registry-corpus-state/; the two rule failures those files expect are one malformed test input, a Resources: that holds Type and Properties directly instead of a map of resources, which earlier releases deselected in silence and reported SKIP. The Pull Request workflow run 40630805680 on this head passed.

Please review. Three readings you may want to weigh. A filter through a variable over a map of nothing but scalars under its own key is decided on the map, as [*] and 3.2.1 decide it, where the literal spelling is refused. IN %capture is membership over the capture's keys, one key or many, where 3.2.1 compared a let-bound literal against every key at once. %p[ "Fn::ImportValue" exists or this empty ] empty over { QueueName: "" } passes through the variable, the verdict the test's comment records for 3.2.1, where the literal spelling fails.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

*Issue #, if available:*

*Description of changes:*

This change hardens how the evaluator reads filters, map-key filters,
membership comparisons and wildcard spellings over the shapes
CloudFormation and Kubernetes documents carry, so a clause that cannot
be decided on the data it was given fails the rule closed with a reason
instead of passing or reporting SKIP. It also records every refusal's
reason at the value it refused, whole in the structured reports and up
to 4096 characters on the console with a marker where the console
shortened it, and announces once per run each reading that moved, with
the spelling that reads what the author meant.

## What changes for a rules author

Each item names the earlier behavior and the new behavior. Where an
item says measured, the earlier behavior was read on binaries built
from the upstream releases tagged 3.1.12 (also tagged 3.1.2; crate
version 3.1.2), 3.2.0 and 3.2.1; the other items describe the earlier
evaluator's code path. The groups follow the behavior-change callouts
in `docs/CLAUSES.md`.

### Filters after a wildcard (`[*]` and `.*`)

- A `[*]` over a map whose entries are all maps, lists or nulls
enumerates the members and selects among them, as `.*` and the literal
`Resources[ ... ]` do. Earlier (measured): a filter straight after
`[*]` over a map of members asked its predicate of the value the query
stood at before the wildcard (the document root for `Resources[*]`, the
resource for `Properties.Tags[*]`), so `Resources[*][ Type ==
'AWS::S3::Bucket' ].Properties.BucketName exists` over a map of buckets
selected nothing and was not applicable at exit 0, every bucket
unflagged. A map with a non-null scalar entry (an IAM `Statement`
written as one object) is one object, and a filter after `[*]` tests it
as it tests the one-element list.
- Over map-shaped tags, `Tags: { Env: dev }`, a filter whose every
clause reads `Key` or `Value` against a literal is decided by the map's
`{ Key, Value }` pair list, one pair per entry: `Properties.Tags[*][
Key == 'Env' ].Value == 'prod'` fails on `dev` and passes on `prod`,
and whatever follows the filter reads the kept pairs as it reads a
list's elements. Earlier (measured): the predicate was asked of the
resource that holds the tags, which has no `Key`, so the filter
selected nothing and the clause was not applicable at exit 0 over
map-shaped and list-shaped tags alike, and a template tagged `dev`
passed a check that read the exit code.
- A filter after `[*]` over one object whose predicate compares against
keys the object holds none of, keeps nothing, and is followed by a
query part that reads a key the object also lacks is refused in both
polarities. The text names the key that compares, the entry the
predicate compared against when the object holds one of that name and
the spelling that reads it, the spelling before `[*]` as written, and
the entry that made the map one object. Earlier (measured on map-shaped
tags and on a one-object statement): selected nothing and was not
applicable at exit 0. A predicate that keeps the object, names a key it
holds, or asks only whether its keys exist or are empty keeps the
one-object reading, so `Statement[*][ Principal == '*' ] empty` over a
statement with no `Principal` still passes, as its list twin does.
- A map that holds members beside a stray scalar entry, under a
predicate naming a key the members carry (`Resources[*][ Type ==
'AWS::S3::Bucket' ]` beside a string entry), is refused in both
polarities, naming the stray entry. Earlier (measured): the predicate
was asked of the enclosing document, nothing was selected, and the
clause was not applicable at exit 0.
- A bare key (`B:` with no body, the shape of a commented-out resource)
is a member the predicate sees as an absent value: a value check drops
it and the members beside it decide the clause, while an absence check
such as `Resources[*][ Tags !exists ] !empty` holds on it and the
assertion fails closed with the reason "needed value at [null] was
empty". Earlier (measured): the absence check passed at exit 0 under
`[*]` and through a variable bound to `Resources`, and for
`Resources.*[ Type == 'AWS::S3::Bucket' ].Properties.BucketName exists`
the rule read `FAIL` and the CloudFormation reporter then stopped with
an internal error at exit 101 in place of a failure text.
- A filter after `.*` over a map holding a scalar entry passes the
scalar over: `spec.*[ securityContext !exists ] !empty` over a `spec`
whose `replicas: 3` sits beside two sub-maps lacking the key keeps the
sub-maps and passes; over a `spec` of nothing but scalars it selects
nothing, so `empty` passes, `!empty` fails and a `when` condition on
the selection stays closed. Earlier (measured): failed on the scalar
with a retrieval error at exit 19 in both shapes. A predicate that
compares the entry itself (`this == 'prod'`, `this IS_STRING`) still
hands the scalar on to be tested. Two maps are refused in both
polarities under `.*`, because enumerating them cannot answer what was
written: a map of nothing but non-null scalars under a predicate that
asks about the map itself, naming a key the map holds or comparing its
entries as `this`, and a map whose own key the predicate names while
none of its map-valued entries holds it.
- A filter after `.*` over a list enumerates the list: `Resources.*[
Type == 'AWS::S3::Bucket' ] !empty` over a `Resources` written as a
list of two buckets passes. Earlier (measured): the predicate was asked
of the document root and the clause failed at exit 19.
- Either refusal above, met inside another filter's predicate, is the
undecidable marker for the map under test and fails the enclosing
clause closed: `Resources.*[ Properties.PolicyDocument.Statement.*[
Effect == 'Allow' Action == '*' ] !empty ] empty` over a policy whose
`Statement` is one allow-all object fails at exit 19. The resource is
neither selected nor deselected, its siblings are tested and reported,
and a `some` sibling in a rule body, or a decided failure in a `when`
condition, can still settle the clause past it.
- `validate` writes a `DEPRECATION:` line to stderr, once per run per
site, where a filter after `[*]` over a map of members would have been
decided by the value before the wildcard: it names the key, where the
earlier reading asked the predicate, which way the selection moved, and
the spellings that ask for the member by name or read each member's own
key. `Resources.*[ ... ]` enumerated the entries in every release and
carries no notice.

### Map-key filters (`[ keys ... ]`)

- Over an empty right-hand side, `keys != %exempt` and `keys NOT IN
%exempt` select every key and the check runs. Earlier: over an empty
exemption list they selected no key and the rule reported SKIP at exit
0 having checked nothing. `==` and `IN` over the empty set still select
no key and still report SKIP.
- A map the right-hand query names is read as its key set, including a
map reached by a key step after an enumeration and a map a `[*]` passed
on whole. Earlier: `let exempt = Mappings.Exempt.Buckets` over
`Buckets: { BucketA: true }` was one value no string key compares with,
so `keys IN %exempt`, `keys NOT IN %exempt`, `keys == %exempt` and
`keys != %exempt` all selected no key and reported SKIP at exit 0; an
exemption map that exempted nothing while empty switched the whole
check off the moment one entry was added.
- A right-hand value the query cannot reach fails the filter closed
under every comparator, with a reason naming the path where retrieval
stopped. Earlier: it selected no key under every comparator and
reported SKIP at exit 0, and `keys IN` over a right-hand side with a
gap in it admitted a key on the strength of the half that resolved. The
failure travels with the selection, so it reaches a `when` condition
(the rule fails instead of reporting SKIP), a comparison on either
side, and a `some` binding; a filter predicate over such a filter fails
the enclosing clause closed in either polarity. A rule written for an
optional exemption list keeps its escape hatch: `when %exempt exists {
... }` skips the block on a template without the list and runs the
check on one with it.
- Inside a function argument, `Map[ keys == to_lower(this.Wanted) ]`
reads `Wanted` from the map being filtered and nothing else; a map
without the key leaves the argument unresolved and the filter fails
closed naming where the read stopped. Earlier: `this` meant the
enclosing scope (the document root or the resource). The plain spelling
`to_lower(Wanted)` keeps the fallback to the scope beside the map when
the map does not hold the argument's first key; `this.` opts out of it.
A clause whose two readings differ prints a warning on stderr naming
both. To read the scope's value, bind it to a variable and pass `%name`.
- A function argument read from the scope beside the map,
`Resources.Cfg.Map[ keys == to_lower(Wanted[ Use == true ].Name) ]`
over a document whose top-level `Wanted:` is bare (null), fails closed
at exit 19 naming `/Wanted`, as it does when the map's own `Wanted:` is
bare. `Wanted: []` is the empty set, decided by polarity, and an absent
`Wanted` is the in-map miss, failing closed.
- A key filter whose right-hand side is refused on the data,
`Resources.*.Properties.M[ keys == to_upper(Tags[ Key == 'Env' ].Value)
]` over a resource whose `Tags` holds a string, records that map as
undecidable with the refusal as its reason. A rule over the one refused
map fails closed in either polarity, as before, and the failure is now
recorded at the map; under `some` over several maps a sibling whose own
selection passes settles the clause, where earlier the clause failed at
exit 19 with "Clause could not be evaluated against the incoming
context ... bailing". A condition whose operator is `exists`,
`!exists`, `empty` or `!empty` fails the rule closed on the undecidable
map whatever the siblings say, as it did.
- A report's reason for a key filter that met a value with no entries
reads `Map Filter for keys was not a struct <type> <path>` whichever
part sat before the filter. After a wildcard it used to read `Filter on
value type that was not a struct or array <type> <path>`.

### Membership and comparison

- For membership against an enumerating query, the query reads as the
list literal its results spell out, in every role (rule body, `when`
condition, filter predicate, through `not`, through a parameterized
call). Earlier: each result was compared on its own, a string result
being a haystack the left-hand string was searched in, so `VolumeType
IN Mappings.Allowed.Types[*]` passed any substring of an allowed type,
the empty string included, while the written-out list rejected it. Most
clauses that move go from PASS to FAIL and now report what the literal
spelling always reported; the few that move from FAIL to PASS each
denied something the literal spelling never denied, and each is listed
with its reason in `docs/CLAUSES.md`.
- For `some` against a right-hand query the document lacks, the failure
alone is filed. Earlier: beside each left-hand value's failure against
the absent value, a second verdict was filed for the comparison that
never ran, which the negated comparators turned into a pass, so `some
... != <absent query>` and `some ... NOT IN <absent query>` reported
PASS at exit 0, a `when` condition on them ran its body, and a filter
predicate selected every entry. The `match_all` spellings did not move.
- A right-hand query that stops at a key the document lacks
(`Parameters.Missing.Default` over a template with no `Parameters`) is
a value the document lacks rather than a query with no values, and a
comparison against it is a decided failure for each left-hand value,
under every comparator and in both polarities, with the path the
retrieval stopped at as the reason. `some` changes nothing here: with
no left-hand value that passed, the assertion fails, a `when` condition
reports its rule not applicable, and a filter predicate deselects every
entry. The escape hatch is `when Parameters.Missing.Default exists {
... }`.

### Filters after a key, and variables

- A filter after a key over a map with a scalar entry passes a non-null
scalar over under a predicate about keys, and is refused in both
polarities where reading the map as one object disagrees with the walk
over its entries: over an all-scalar `Properties`,
`Resources.B.Properties[ Foo !exists ] !empty` keeps nothing while the
predicate holds on `Properties` itself, so `!empty` and `empty` fail
and a `when` condition on it fails rather than opening or skipping,
with `X[*][ ... ]` named as the remedy. Earlier: every entry was handed
to the predicate, scalars included, so the clause selected every
property by the absence of `Foo` and passed, and the `when` condition
opened its block. A rules-file mistake inside the predicate
(`parse_int('abc')`) stops the run at exit 5 under both spellings.
- A variable bound to another variable that names a top-level key, `let
all = Resources` then `let r = %all`, is read as `Resources` wherever
`%r` is used. A filter or `.*` over `%r` on a template with no
`Resources` key, or an empty one, is not applicable, as it is for the
key written out. Earlier: `%r.*[ Type == 'AWS::S3::Bucket'
].Properties.BucketName == 'b'` failed at exit 19 through the alias
while the direct spelling passed, and a `!exists` or `empty` assertion
over the same selection passed through the alias.
- Through an alias of the population, `let r = Resources` then
`%r.B.Properties.Tags[ Key == 'Env' ] exists`, a template with no
`Resources` key fails on the key `B`, as the spelling without the alias
does. Earlier: the alias was not applicable there.
- A filter straight after a variable bound to a list below the top
level, `let cs = spec.template.spec.containers` then `%cs[ name ==
'app' ] exists` over `containers: []`, is not applicable, as the
literal filter is. Earlier: it failed at exit 19 through the variable,
and the negated filter and a `when` condition over it passed or opened.
A rule that demands a matching container therefore lets `containers:
[]` through at exit 0 through the variable; the authored
`containers[*][ name == 'app' ] exists` keeps its failure.
- `X[*].K[ ... ] exists`, a wildcard then a key then a filter, over a
bare top-level `X:` or over `X: {}` is not applicable, as it is over
`{}` and over `X: []`. Earlier: it failed on the key; the negated
spelling passed and a `when` condition on it opened its block.
- A filter over a template section's entries, under every spelling,
passes an entry under an intrinsic directive key over when the
predicate is about the members: any `Fn::` key at a template section
(`Fn::Transform`, an `AWS::Include`) and `Fn::ForEach::<Name>` at any
depth, since a loop's expansion, not the loop, is the members. A
predicate that names a key the directive's value holds is a rule about
the directive and is handed it.

### Notices and reports

- A `DEPRECATION:` notice describes the rule, not one evaluation of it,
so `validate` writes each once per run. The structured outputs (`-o
json`, `yaml`, `sarif`, `junit`, and `--structured`) write the set once
on stderr before the document; the single-line output writes each
notice beside the first data file that drew it and, after its last
file, one `NOTE:` trailer per distinct list of later files that drew
it, naming at most three files and counting the rest.
- A warning on stderr from `validate`, `test` or `rulegen` is one
printable line whatever it quotes: a control character, a bidirectional
control or a zero-width character in a key, a YAML tag, a test case's
name, a file name or a quoted key is written as its Rust escape (`\n`,
`\u{1b}`, `\u{202e}`), so a key cannot open a terminal escape sequence
or reverse the rest of the line; a backslash in a key or a tag is
written as `\\`, so two names that would print alike print apart.
- A query a report renders with the parser's own name for a filter
joins an unnamed filter without a stray `. ` (`Resources.*
(filter-clauses)`, `Resources (map-key-filter-clauses).Type`); a named
filter, a quoted key and a numbered index keep their `.`.

### Input reading

- `validate` warns on stderr, one line per key, when a key merged in
through `<<` collides with a key the mapping already holds in another
form (the integer `1` beside the string `"1"`), saying which value is
evaluated; such a key used to be dropped in silence. Two keys of one
mapping that share an anchor's position get the duplicate-key warning;
they used to collapse into one entry in silence.
- A merge key reached through an alias (`&m <<:` once, `*m:` elsewhere)
merges as a written `<<` does, and `*m: abc` is refused as `<<: abc`
is, as a parse error at exit 5. It used to load as an ordinary key
named `<<` with the mapping under it hidden from every rule.
- An explicit `!!int` tag resolves by the same forms as a plain scalar,
so `!!int 0o755` and `!!int 0x1ED` are both 493 where the document used
to be refused at exit 5; a tagged decimal with a leading zero that YAML
1.1 and 1.2 read as different numbers is refused with a message naming
both integers.
- `json_parse` refuses an embedded string whose member names differ as
written but read as one key (`{1: "a", "1": "b"}`), and the clause that
called it fails closed naming the property and the key; the second
member used to replace the first in silence.

## Why

Refused reads are never silent. Earlier a clause could select nothing
because its predicate was asked of the wrong value, because a
right-hand side could not be read, or because a map was read as one
object when the author wrote a selection among members, and the rule
then reported not applicable or SKIP at exit 0. A pipeline that reads
the exit code let the template through with nothing checked. Now such a
clause is refused with a text that names the key, the path where the
read stopped, the entry that fixed the map's shape, and the spelling
that reads what the author meant, and the rule fails at the validation
failure code.

A decided mismatch is not raised. A comparison against a value the
document lacks is a decided failure for that value: it deselects the
entry in a filter predicate, settles a `when` condition as not
applicable, and fails one value in an assertion. `some` is settled by
its absences as its `match_all` spelling is.

A refusal's reason stays with its value. It is recorded at the value it
refused and reaches the structured report whole and the console up to
4096 characters, with a marker where the console shortened it, per
value, with the other values' verdicts beside it; every name or path
the reason takes from the template is bounded at 256 characters, a path
shortened in its middle so its top and its leaf stay readable; a notice
names the spellings the author can write; and every stderr line is one
printable line, so a key or a file name cannot hide or reorder the text
that explains the verdict.

## Compatibility and testing

What passes before and now: `.*` straight after a key over a map of
members; one-object statements under single-key policy predicates
(`Statement[*][ Principal == '*' ] empty`, `Statement[*][ Action == '*'
] empty`); the `match_all` spellings against an absent right-hand
query; `keys ==` and `keys IN` over an empty right-hand set (still
SKIP); a `when %exempt exists { ... }` hatch around an optional
exemption list.

What fails closed now, at the validation failure code (exit 19): a
one-object filter compared against keys the object lacks and followed
by a read of another missing key; a mixed map under a member-key
predicate; an absence check over a null entry beside a member; a
refusal met inside a filter predicate; a map-key filter whose
right-hand side does not resolve, under every comparator and in both
polarities, and a filter predicate over it; `this.` inside a function
argument over a map lacking the key; `.*` over an all-scalar map under
a predicate that names the map's own key; a filter after a key over a
map with a scalar entry where the one-object reading and the walk
disagree; membership against an enumerating query where the written-out
list rejected the value; map-shaped tags under a pair-shaped filter
whose value does not match. Where the fixture set measured the earlier
verdict, it was: not applicable at exit 0 for the pair-shaped filter
over map-shaped and list-shaped tags and for the mixed map under
`Resources[*][ Type == 'AWS::S3::Bucket' ].Properties.BucketName
exists`; PASS at exit 0 for the absence check over a null entry after
`[*]` and through a variable bound to `Resources`, for `Resources[*][
Type == 'AWS::S3::Bucket' ] empty` over a map of buckets beside a
string entry, and for `Statement.*[ Effect == 'Allow' Action == '*' ]
empty` over a one-object allow-all statement; and a failure with a
retrieval error at exit 19 for `.*` over an all-scalar map.

What moves from PASS at exit 0 to a decided failure at exit 19: `some
... !=` asserted against an absent query; `Resources[*][ Type ==
'AWS::S3::Bucket' ] empty` over a map of buckets, which are selected
now; and a two-key predicate such as `[ Effect == 'Allow' Action == '*'
] empty` after `[*]` over a one-object allow-all statement and its
one-element list twin, and after `.*` over that list twin, where the
statement satisfies the predicate and is kept. The `NOT IN` spelling of
the same `some` clause as a `when` condition moves from PASS to not
applicable at exit 0.

What moves from SKIP to a run check: `keys !=` and `keys NOT IN` over
an empty right-hand side, and all four comparators over a map on the
right-hand side, now select and check keys; on an input every key of
which satisfies the check they read PASS.

What moves from a failure or from not applicable to pass: a filter
after `[*]` over a list, under a predicate the elements satisfy, from
not applicable or a failure to pass; `.*` then a filter over a list
under a predicate the elements satisfy (where the elements satisfy a
two-key predicate under an `empty` assertion the clause moves from PASS
to FAIL, as the statement rows above show); `.*` over a map with a
scalar entry; a filter or `.*` through a variable aliased to a
top-level key the template lacks; a filter straight after a variable
bound to an empty list below the top level; `X[*].K[ ... ] exists` over
a bare or empty section; `some` over several maps where a sibling's key
filter passes beside one refused map.

Exit codes are 0 for a run whose rules pass or are not applicable; 19
for a validation failure, which every refusal above raises; 5 for a
rules-file or data-file parse error, including a rules-file mistake
inside a filter predicate and a document the YAML reader refuses.

The key-filter reason string, the query rendering of an unnamed filter,
and the shape of merged-key and duplicate-key warnings change as
described above. A consumer that matched the old strings byte for byte
sees the new ones.

For code that uses cfn-guard as a library, `cfn_guard::Error` gains
`Unevaluatable(String)`, `MissingDocument`,
`UndecidableComparison(String)` and `UnsupportedDocument(String)`, and
`cfn_guard::InternalError` gains `EventRecordMismatch(String)` and
`UncheckedParserInvariant(String)`; neither enum is
`#[non_exhaustive]`, so an exhaustive `match` outside the crate needs
an arm for each or a wildcard arm. A document the reader cannot parse
through `run_checks` is reported as `ParseError` (FFI code 5) where it
was `YamlError` (FFI code 2).

The suite pins each behavior above by name, as a fixture rule over a
template that shows the shape, or as a test over the evaluator, with
the opposite polarity and the earlier verdict's twin beside it where
one exists:

- Wildcard filters: the fixture set under
`guard/resources/validate/wildcard-filter-release-verdicts/` holds one
rules file per spelling over a bucket beside a null entry, a bucket
beside a string entry, a one-object statement and its one-element list
twin, map-shaped and list-shaped tags, and a `spec` with a scalar
beside its sub-maps; its README tables the verdict read on each
measured release beside the current one and whether the member notice
fires, and two tests read those tables and assert every current verdict.
- Map-key filters and membership: fixture rules under
`guard/resources/validate/in-over-enumerating-query/` cover empty,
map-valued and absent right-hand sides under every comparator, the
`when %exempt exists` hatch on templates with and without the list, a
filter predicate over an undecidable key filter in both polarities, and
membership against an enumerating query in every role, including every
clause that moves from FAIL to PASS; a partly absent right-hand side is
pinned by the evaluator's tests.
- Nine tables in `docs/CLAUSES.md` and items 11, 15 and 16 of
`docs/KNOWN_ISSUES.md` list the verdicts the tests pin, naming the test
and case for every clause and shape the text uses, with a row marked
unpinned where no test asserts a spelling; each printed remedy spelling
is produced by the author's refused clause and read back unrefused over
the shape it is printed for and over its twin.
- Notices and reports: once-per-run notices under the structured
outputs and the trailer under the single-line output; the escaping of
every control, bidirectional and zero-width character in a warning
line; and the anchor a notice points at in `docs/KNOWN_ISSUES.md`,
which a test reads from the file and fails on when the file or the
anchor is missing.
- Input reading: merged-key and duplicate-key collisions with the value
that stands, the alias merge key and its refusal, the `!!int` radix
forms and the ambiguous leading zero, and `json_parse` over duplicate
member names, with `validate`, `test` and `run_checks` pinned to one
reading of a document.
- Rules in the aws-guard-rules-registry at its pinned revision were
re-run through `cfn-guard test` against the release before the
membership change and against this one: no rule, test case or output
byte moved.

---
*By submitting this pull request, I confirm that my contribution is
made under the terms of the Apache-2.0 license*
A comparison that meets a marker among its right-hand values (a
refusal's marker, or the one a key filter leaves for a map it cannot
decide) reads an absent operand on either side as a decided failure,
under every comparator and in both polarities: a left-hand value the
document lacks, an empty collection, or a right-hand reference that
resolves to nothing. The verdict of an absent operand does not depend
on the other side. A filter such as `Resources.*[ Properties.Size >
%limits ]` deselects the resources lacking `Size` whatever `%limits`
holds, and a `when` condition over a missing default is not
applicable. Under a positive comparator, `some` is settled by its
absences too, so the `some` and `match_all` spellings deselect alike.

The marker's pair under a positive `IN` keeps a FAIL status and a
record naming the marker's reason for every left-hand value no
resolved entry accepted; a value a resolved entry accepted passes.
Under a negated comparator the pre-pass reads either marker kind, so
`!=` and `NOT IN` over a key filter's selection agree. A refusal to
compare kinds under membership beside a marker is unknown, since
membership refuses a value only when no entry can be compared with
it. The bookkeeping that tracks which left-hand values are decided
keys them by identity, never by path alone, and notes a literal left
operand once.

Records and reports name refusals as refusals. A comparison record
whose right-hand value is a refusal's marker carries `refusal: true`
and the marker's text; a left-hand refusal marker against a plain
right-hand side records the refusal whole; the console and the
structured report print "the value to compare to could not be read"
plus the refusal, and the CloudFormation console prints the refusal
on its `Error =` line beside a missing template value. A comparison
raised beside several refused reads carries one numbered text,
`(1) ... (2) ...`, built from the refusals of either side in
registration order; the refusal test accepts such a text part by part
without registering the joined text, so the count of refused reads
stays the count of reads. A kinds refusal beside a refusal's marker
under a negated comparator is worded as its own verdict ("Comparing
<l> <op> <r> was refused: X and Y are kinds the comparator does not
compare"), with the comparator spelled as a rules file spells it;
beside a key filter's marker alone it is no refusal. Every template
value quoted in a refusal's record or sentence is bounded, so a long
key prints cut and marked and no line passes 4096 bytes.

An empty left-hand selection beside a refusal's marker is raised in
every role; beside a key filter's marker alone it keeps its decided
answer. A document `[]` on the right is a present value: a left-hand
marker beside `NOT IN []` is raised rather than silently not
applicable. An absent right-hand reference beside a marker is raised
under `some` and declined under `match_all`, since the marker's pair
may be the one the value passes against. The strict-role raise asks
whether the clause passed; an assertion whose statuses already fail
keeps its fold and its record.

Not changed here: the kind of a raise still travels as the error
variant; a gate's or predicate's plain raise records `refusal: false`
and the console cuts it at 320 characters; a left-hand refusal marker
beside a right-hand query that selected nothing is neither registered
nor reported; the unary record keeps `refusal` false. docs/CLAUSES.md
and the README state each of these rules.
The report arms read a refused operand by the kind of its unresolved
value alone, not by the record's flag: a right-hand value the evaluator
refused to read keeps "could not be read", an absent right-hand value
beside a refused read keeps the missing-property sentence, and the
unary arms keep the record's kind. Every value a record quotes from the
template, the value a query traversed to and the path of a resolved
value go through one bound (`LONGEST_TEMPLATE_PART_IN_A_REFUSAL`
printed characters, then `...[N more characters cut]`), shared by one
formatter that `PathAwareValue`'s `Display` uses as well; a key of
three thousand characters is cut, not quoted whole.

A refused marker on the left of a comparison against a right-hand side
the document lacks records the retrieval's own reason first ("Could not
find key ... inside struct at path ...") and the marker's reason after
it, so the missing right-hand property is named; the lead is bounded as
a template part before the sentence is built, so a reason that quotes a
whole value cannot push the refusal past the console's bound. A key
filter's marker on the left beside an absent right-hand side is led by
the absence too, with no refusal flag.

An empty left-hand selection beside a key filter whose right-hand query
was refused is raised: the asserted form fails with the refusal
recorded, the gate form declines on it, and the key filter's wrapped
sentence ("the right-hand side of the key filter could not be
evaluated at ...: ..., so the keys it selects cannot be decided") is
registered when the reason inside it is a registered refusal. The
registered reason is collected as a refused read, and it is found
behind an unregistered marker that stands before it among the
right-hand values. A strict raise's reason is a refused read the
comparison met, on either side: the refusal met beside a left-hand key
filter's marker is the raise, a key filter's own reason is promoted to
the first refused read only when it is not itself one, and a resolved
pair's undecided reason is never promoted over. An absent right-hand
reference beside a refusal is raised under `some` and decided under
`match_all`, which writes the "was decided by a value the document
lacks" line naming the refusal and the reference.

The console prints a refusal once. The `Error =` line carries the
record's sentence opened (no nested "Check was not compliant ...
Error = [...]"), bounded to one line of at most 4096 characters with
the cut marker; the `Reason =` line is left out when the printed line
carries the reason and written, bounded, when the cut dropped it; an
absence beside a refused read keeps its `Reason =` line, which names
the retrieval's reason. A raised marker-alone `IN` prints its one
message under "Could not be evaluated:". The verbose tree prints the
record as recorded and is the named exception to the bound.

A kinds refusal under a negated ordering comparator quotes the
comparator as written, `>`, `>=`, `<` or `<=`: a leading `not` is
folded into the clause's negation flag and stands before the clause,
so the sentence never spells `not >`.

docs/KNOWN_ISSUES.md gains two items: a second refusal met inside a
`let`-bound selection read bare is neither joined into the raised text,
registered nor recorded (the raise names the first alone), and a
negated membership with a refused marker on the left is decided by the
kind refusal, which is pinned as it reads and left open as a question.
docs/CLAUSES.md says that a refusal inside a key filter's right-hand
side is never silent, with the own-key example, and the README's report
clause says what each report arm reads.

Tests pin each move: the report arms over a refused right-hand read and
a bounded unary record; the absence-led record beside a left marker
and beside a key filter's marker, in the records and on the console;
the key filter's refused right-hand side on an empty left under
`NOT IN`, `==` and `IN`, asserted and gated, with the budget marker and
the string filter as controls; the registered reason collected and
found behind another marker; the raise beside a left-hand key filter's
marker and its mirror, with a no-refusal control; the absent reference
under `some` and `match_all`; the console blocks for a right-hand
refused marker, a left-hand refused marker and an absence beside a
refusal, each verbatim; the refusal printed once from a short and from
a 5000-character lead; the kinds sentence under `not ... >` and the
comparator table; one kind pair and two refusal records beside a
marker; the `.aab` read beside a key filter's marker over an empty list
entry; a decided gate once the walk beside a capture is read; and a
let-bound bare read beside two refusals.

Limitations, as recorded: whether a negated membership with a refused
marker on the left should be decided by the kind refusal is open; the
pin of the unbounded unary sentence moved to the bounded one; three
report cells have no measured red on the base engine, their patches are
the record; the lead of a numbered raise is the first collected refusal,
not the deciding reason, and a numbered text whose registered part holds
the next marker is rejected; the gate `NOT IN` row with the right-hand
value absent is not pinned; whether the `.aab` read of a handed-on
empty list yields an absence is read, not pinned; the per-resource
block arm and the resolved report arms print their messages unbounded;
and a left marker's record beside a right marker prints the left
refusal on the `Error =` line and again on `Reason =`.
The README's 3.2 bullets on a marker among a comparison's right-hand
values are one entry per move, each with one example and a link to the
behavior-change entry in docs/CLAUSES.md; that entry gains the two moves
the README pointed at and the entry lacked (the refused-read line, and
the asserted raise recorded as the refusal it is whichever side the
refused read stood on), each with the tests that pin it. The alias merge
key gets one entry per change as well. An entry that states a release
answer no fixture row holds (membership settled by a listed value,
`some` against a negated comparator with no match, set equality with a
left-only mismatch, an absent operand beside a marker) says so, in the
entry and in the README bullet that mirrors it: unmeasured on the
releases, the baseline being this evaluator's reading before the change.

The "pinned by" cells of the clause tables cite a test as
`<file>:<fn>`, with the case labels it carries where one run is meant
(`(case "...")`, `(cases "...", "...")`) and a `, fixture ...` part
where the test runs a fixture file; `unpinned` or `unpinned: <what>`
marks a spelling no test asserts and comes last. Every cell of
docs/CLAUSES.md resolves under that grammar: each label is a named or
unnamed rstest case, a string literal of the fn's body, a literal of a
row table the body names (one declared in the fn first, else one of its
module), a rendering of a formatting macro's template, or a row of the
wildcard-filter fixtures README. A label is written as a plain Rust
string literal writes its text, a quote inside it as `\"`, and a
literal the source writes raw is read in that spelling, so a rule text
holding a quoted value can be cited. Fixture names the cells carried
are kept. The CLAUSES preface says what the check verifies: a label is
a string the fn carries, not a run it asserts, and a fixture part names
a file that exists under guard/resources/, whether or not the test
reads it.

The integration test guard/tests/docs_pinned_by.rs checks every table
under the clause-table header in docs/*.md against the four test
sources, parsed once with syn: a renamed test, a missing case, a
malformed cell, a bare `unpinned` that is not the last part or a
fixture name no file matches fails it, naming the document and the row.
Doctored cells are refused and accepted before the real cells are read,
each carrying whether it is refused with its labels unread, and a
row-table control shows a fn reads its own `const` and never a
sibling's. A format template's placeholder stands only for a string the
cited fn carries (a case, a body literal or a row-table literal), so a
label whose fixed pieces fit but whose placeholder text no run writes
is refused, with such a cell among the doctored controls. Raw literals
are read with `\r\n` as `\n`, and raw and plain literals written across
lines resolve under either line ending. The row floor is per document
(CLAUSES.md 120 rows and 105 pinned, KNOWN_ISSUES.md 240 and 215, each
about a tenth under its count at this commit), so a document whose
tables were deleted or emptied fails by name, with no cover from the
other document's rows. The labels of every document resolve. The
KNOWN_ISSUES.md labels that were written as prose are listed in
KNOWN_UNRESOLVED_ROWS by document, line and label (136 labels over 131
rows): a listed row is still parsed and its test and fixture names
checked, its other labels must resolve, a listed label that resolves
comes off the list, and a listed label not met at its line is reported
by name, with the rows to add or remove in one message. Six
KNOWN_ISSUES cells rewritten into case form resolve: every named case
is declared on the fn the cell cites. The labels-read-alone path gets
its controls (six doctored cells refused either way, one label-only
fault accepted there and refused with labels resolved). The prose of
the two clause-table documents is read as well: inside a "pinned by"
span, which ends at the next `)` or `;`, a backticked token must be an
fn defined in a cited test source, a cited source, or `<source>:<fn>`,
and anything else is reported; a backticked `.yaml` or `.guard` name is
a fixture only where the prose says "fixture", and a fixture name is a
file name alone (letters, digits, `_`, `.` and `-`) that must exist
under `resources/`, so a notice's sample text that ends in `.yaml` is
prose.

Two moves the behavior-change entry on a marker among a comparison's
right-hand values described in prose are now listed with the others,
each with its released verdict, its new one and the tests that pin it,
and the README carries one bullet for each. The lists keep the
empty-selection move beside a refusal's marker and say what its one
fixture row measures: the released binaries stopped with an internal
error on a document whose resolved value stood beside the marker, so
whether a release reached the empty selection itself is unmeasured;
this evaluator reported the shape as the empty selection at exit 0 with
the refusal registered and nothing written for it, and now raises it.
An empty left-hand selection beside a KEY FILTER's marker under
`NOT IN`, `!=` or `IN` is a clause on that entry, pinned, not an entry
of its own: a released binary spends no regex budget and never leaves
such a marker, the engine's own comment on the `Skip` arm of
`binary_operation` says the released evaluator read the shape not
applicable, and this evaluator reads it the same way, the empty
selection's own answer; `NOT IN` is the pinned spelling, `!=` and `IN`
take the same arm unpinned. Beside either marker, `==` and the
orderings are never raised: the operator's own not-applicable answer
for nothing on the left, unpinned. The same selection beside a refused
read's marker is raised, as the entry already said. The `some` raise on
an absent right-hand reference beside a marker is stated once, in the
narrower form, and links the entry that lists the move: a `some` whose
pairs are an absence, decided noes and an undecided marker, with no
passing pair, fails closed, since the entry the marker hides can decide
it, where `match_all` is decided by the absence; the key filter's half
of that sentence is pinned and the refused read's half is not. A
refused read on the left of a comparison against a plain right-hand
side is recorded as the refusal it is, so the `error_message` of the
json and yaml reports and the verbose console line read "the value to
compare from could not be read. Error = [...]" in place of "property []
to compare from is missing. Value traversed to [...]", and the console
prints the refusal whole; a consumer that matched the replaced sentence
sees the new one. A left-hand value the document lacks with no refusal
behind it keeps the released sentence, and the wording beside a
right-hand value the document lacks is not part of this change.

The kept-literal alias example in the README is the merge that
`an_aliased_kept_literal_keeps_its_form_wherever_the_anchor_was_written`
pins in its `anchor_as_a_value` case, with the held value standing and
the anchor written before `outer`, since an alias refers back to an
anchor already read. It had written an anchor beside its quoted
spelling as two explicit entries of one mapping, which is the
duplicate-key report with the last value standing, not the merged-key
collision the sentence describes. The CLAUSES preface's fixture span is
a code span, and the one-run cell cites one case.

Seven rows of the one-object wildcard table cited their tests with
abbreviated case labels, a plural written with "and" and labels ending
in an ellipsis, which the citation test refuses because no case or body
string reads that way. Each now names the cases as the test writes them:
the six remedy-spelling rows (the `X[*] { Key == value }` and
`X[*].Key == value` pair over a bucket and a queue, the in-block
`{ Key == value }` over a rule, the member spellings `X.*.Key` and
`X.* { Key == value }`, the three absence remedies and their in-block
forms, the `X.<key>` read of a map of pairs), each over the one object
and its one-element list twin, and the `Resources[*] { Type exists }`
row over a map and a list of resources, with and without a resource
lacking `Type` and with one named `Type`. The verdicts and notices of
the rows are unchanged.

The fixtures README says again which binary the 3.1.12 column was read
on: the one built at the commit the 3.1.12 tag names, whose manifest
reads 3.1.2 and whose binary prints that version, as
docs/KNOWN_ISSUES.md records; the 3.2.0 and 3.2.1 tags name the
version-bump commits, and no release column was re-measured since the
table was written.
The known-unresolved labels of docs/KNOWN_ISSUES.md were listed by
document, line and label, so inserting a row above a listed one
renumbered every entry below it and the test asked for the list to be
rewritten. The list is now keyed by document, label and the row's
clause cell (its first column as written): a row may move without
touching the list, a reworded clause cell is re-listed with its new
text, and two rows of one document that write the same clause and the
same label share one entry, which two pairs do today (a row repeated
in a later table with another verdict or notice), so the list holds
134 entries for the same 136 failing labels. The list is a set: an
entry written twice fails the test. The miss report names each entry
by document, label and clause cell, and a listed label that resolves is
reported with the clause of its row. The clause-table reader returns
the clause cell beside the pinned-by cell for every row.

An entry keyed by clause cell and label alone named no row, so a row
copied or re-cited with a listed clause cell and a listed label would
have inherited the exemption in silence. The test counts the rows each
entry is met on, with the label still failing, and reports an entry met
on another number of rows than it is listed for: one row, or the count
SHARED_UNRESOLVED_ENTRIES gives for the two entries that two rows write
today. A copied row therefore goes red until its entry's count says so,
and a twin row that is gone brings the count down.

Every label of a cell is read before the cell's first failure is
returned, so a listed label after an unlisted failing one is still
recorded as met and the report never says to drop an exemption the row
still needs. A row that fails on an unlisted label is reported with its
clause cell, the text the entry to add is written with, and an entry
whose label resolves on one row and still fails on another is reported
as both, with the remedy, instead of as coming off the list. The
per-document row loop is a function of its inputs, so a doctored table
and list are read as the real ones are; controls cover the listed label
after a failure, an entry met on two rows, an entry listed under no
row's clause, and the clause cell in a failing row's line.
The entry for a marker among right-hand values names its two baselines
once: the released binaries where a fixture row measured the shape, and
this evaluator before the change where no release row holds it, with
"before the change" defined there. Its four unmeasured bullets, and the
four README bullets that mirror them, put that hedge in the main clause
instead of asserting a release result and retracting it in a
parenthetical, and every reading called released where no fixture row
holds the shape (the two membership readings, the two shapes kept under
the absent-operand move, in both documents) is this evaluator's before
the change. For the key filter's marker the four sentences say what the
releases did, on a static read of the 3.2.1 sources, unmeasured against
a built release: the same fancy-regex 0.13.0 with no limit configured
returned an error for a key whose regular expression spent the engine's
limit; path_value.rs turned it into a RegexError, the key filter
propagated it, eval.rs recorded the clause as failed and returned the
error out of eval_rules_file, validate.rs returned it before report_eval
ran, and main printed "Error occurred" with the regex error on stderr
and exited 255, so no report was written for the data file, the recorded
clause was never printed, and the releases left no marker and never
reached the comparison; the same read shows the key filter resolved its
right-hand side before any key was matched, and the two absent-operand
sentences add that whether a release ran the key filter at all over an
empty left-hand selection is not read here. The sentence that justified
a no-marker verdict by a grep for "budget" names the mechanism instead.
The same entry says the `Skip` arm raises beside a refusal's marker or a
key filter's marker whose reason is a registered refusal, while an empty
left-hand selection beside the budget marker keeps the answer this
evaluator gave before the change, unmeasured on the releases, under
every comparator, since the operators answer not applicable for nothing
on the left before any comparator is read; the README's empty-selection
sentence says the same and cites the same cell, with the `NOT IN` gate
and predicate as the pinned spellings and the other comparators
unpinned. The 320-character bound on a recorded message is this
evaluator's, not a release's: the refusal-raise entry attributes it to
this evaluator before the change, and the README says whose bound it is;
the "bailing" record over a continuing run that the same sentence
attributed to the released binaries, with no fixture row behind it, is
this evaluator's before the change as well, marked unmeasured on the
releases, in CLAUSES and in the README's mirror of it alike. The
absent-operand move names the four comparators its tests carry, `==`,
`!=`, `IN` and `NOT IN`, and says an ordering beside a marker is pinned
by no test; the README's example of that move compares with `IN`. The
refused-read line's released baseline names the fixture rows it rests
on, and says those gate rows record SKIP and nothing of what the
releases wrote of the refused read; the "nowhere" and the "no such
line" that line and the README's mismatch bullet attributed to the
releases are this evaluator's before the change, and the README's
missing-left-value sentence labels its released wording a static read
of the 3.2.1 sources, unmeasured against a built release. The README's
membership bullet says the passing clause carries no failing child
record, which is what its pin asserts. The README's alias example uses
the case's own `explicit` and names both cases with the loader test by
source. One citation written without backticks is in backticks, so the
reader sees it.

docs_pinned_by reads the README's prose too, with the loader tests as a
citable source; every backticked `.yaml` or `.guard` name is a fixture
unless a named list of README example files exempts it. A "pinned by"
span runs to its first `)` outside a backtick or to the next "pinned
by", which opens a span of its own, strict again, so a second citation
written inside the first is read in full (one CLAUSES row has that
shape, and renaming its second test now fails the check); the two names
of the membership rows that sat past a `;` are written
`eval_tests.rs:<fn>`, bound to their source whatever the span. Every
name before the first `;` outside a code span is a test fn, a source or
`<source>:<fn>`, a fn bound to the source named beside it; past the `;`
a fn-shaped name is a citation and is checked unless it is one of
Guard's own names, listed in GUARD_FUNCTION_NAMES (the eighteen
underscored built-in names eval_context.rs resolves, the type checks
among them, and the match_all spelling; a name without an underscore is
never fn-shaped), and the unbound-name failure reads "no cited test
source defines `fn x(`". A whole table-cell citation may be written in
prose and is read as a cell is, cases and all, so the README's alias
paragraph cites its two cases that way and a renamed case fails there
too. A line continues onto the next only when a backtick run it opened
is closed by a run of the same length on that next line, as CommonMark
pairs them, or when its last "pinned by" span is still open and the line
does not end a sentence; the next line must be prose at the same
blockquote depth (a blank line, a table row, a fence, a heading or a
list item ends the join), a fence line or a table row never joins, so a
wrapped citation is read whole while a stray backtick or a fence pulls
no unrelated line in and a fixture named on the next line keeps its
line number; a backtick a span never closes is reported as odd instead
of ending the scan. No citation in the three documents wraps at this
commit.

The known-unresolved list is keyed by the cited test as well, so an
exemption holds for the test the label was written against and a row
re-cited under another test is unlisted; the cell's first failure is
returned after every citation's labels are read; the list and the
shared entries are classified by a function of the rows read, so an
entry the list writes twice or a shared entry that names one row joins
the report, and a row counts once however often its cell writes the
label. With the cited test in the key, a label resolves or fails alike
on every row of its entry, so a resolving entry comes off the list and
the half-resolved arm is gone; the miss report says a renamed test is
re-cited and re-keyed rather than dropped, a failing row of the shape
pass names its clause cell, and a listed label a cell cites twice for
one test is named as a repeat. A singular `(case ...)` holding several
labels is malformed. A `|` inside a code span splits the row as GitHub
renders it, so a clause-table row holding a bare one is refused
whatever its cell count comes to, and the refusal names the span as
written up to the bare pipe; the reader follows CommonMark: a backtick
run that no run of the same length closes later in the row is literal
text and opens no span; inside a span a backslash escapes only the `|`
it precedes (as a table cell is read); outside a span a backslash
escapes any ASCII punctuation after it, so `\\` consumes both
characters and a backtick after them opens its span. Every other table
of the three documents is read for the same fault and named by line,
and the one such row (a three-column table of capture spellings) is
corrected. A plain literal written across lines, or continued with a
`\` at the end of a source line, is spelled as the compiler reads it; a
placeholder stands for a non-empty carried string. Every report line
names one path per document, the README at the repository root and the
others under docs/.

The template matcher's floor is pinned at its edge: a template with
twelve characters of fixed text is admitted and one with eleven is
refused, read from `template_pieces` directly; the empty-fill control
uses an admitted template, and the double-backtick pipe control
asserts the refusal's text as it is now written, the span named up to
the pipe. The control for a label a cell cites twice asserts through
the classifier that the entry is neither miscounted, missing nor
resolving. Controls cover a listed label under another test, a listed
label in a later citation, a copied row's miscount, a repeated entry,
the document in the key, the prose reader's bindings and its second
span (a second span naming a Guard word reads it as odd, which a first
span's relaxed tail would have passed), a wrapped cell citation red and
green, a span with names on both sides of a break, a span left open
before a blank line, a list item, a heading and a blockquote-depth
change, a stray double run, a triple run with no triple closer on the
next line and a fence line that join nothing, `match_all` drawing
nothing beside a red fn-shaped word, the bare and the escaped pipe in
both backtick shapes, the escaped backtick outside a span, `\\` before
a span with a bare pipe, and the empty fill.
A refusal the evaluator mints inside a filter predicate's run marks
that run's entry refused and cuts the entry's captures, at any depth:
the block spelling's fold, the key spelling one level down, a fold an
inner predicate's `!empty` re-raises, a fold in a rule the run calls.
The entry is marked off the refusal token the ending raise carries,
compared against the count the run opened at, so a marker that carries
no token (a key filter's own undecidable, a regular expression out of
budget) leaves the entry plain. The token is an opaque type: the second
field of the library's `UndecidableComparison` was a bare count that
code outside the evaluator could build or read, and it is now
`RefusalToken`, a type with a private field and no public constructor
that a `match` can name and nothing else; the marker's kind carries the
same type, and the kind prints as its two facts and never the count.

Two paths lost the token on the way, the key filter's own fold and the
block over it, so the entry was filed as a plain marker and a clause
over it read no refusal; both carry it now. Where two refusals meet in
one fold the later-minted pair is the one the raise names, reason and
token together, so a refusal minted inside the run marks the entry
whichever order the values were met in, where the first-met refusal
won before and the reversed selection read the other kind. The pair
rule lives once, read at the raise through one fold that reads every
marker, where a helper the unary operators used stopped at the first
token it kept. A plain marker left by an inner run over a refusal
minted before that run opened still carries the token for the
enclosing run, which marks its own entry by it where the refusal was
minted inside it; a refusal minted while a file-level binding resolves
marks no entry's run, since the binding's query runs once and every
later reader reads the memoized marker, so two entries over one binding
read one kind in either resource order. Nested runs rank a token by its
ownership depth, the number of live frames from the outermost inward
that read it as minted inside their run, kept on a stack the root scope
holds and every run pushes and pops through one helper; ranked by the
innermost frame's bit alone, a nested run's entry read plain or refused
with the resource order; one cell pins both resource orders and both
conjunct orders. A raise carrying a refusal token records as a refusal
whether or not its text is registered, so a refusal delivered as a
marker and re-raised by `exists` or a comparison prints whole with its
remedy instead of as a value with an error appended.

A marker that carries a refusal's token is a refused read by that
token, whatever its text, on both sides of a comparison: the left-hand
readers, the right-hand fold, the empty-selection raise, the positive
`==` pre-pass, the records' flag and the collection gate share one
reading, token or registered text. Before, the right-hand readers read
a marker by kind or registered text alone, so a plain marker handing a
token on with an unregistered text (the key filter's wrap of a numbered
text of refusals) read as a budget marker on the right: an empty
left-hand selection `NOT IN` it was not applicable, the refusal silent,
where the same marker on the left was raised. The key filter registers
its pairing sentence once, at the selection, for the kept pairing whose
inner reason the registry holds, never a numbered inner text, so the
marker it leaves for a map has a registered text behind it as every
tokened marker must. An asserted comparison decides the kind of its
raise once, where the outcome is read with the collection complete and
from the raise's text and token: one registered read as the registered
refusal it is, several as the numbered text nobody registers, and a
text only the token vouches for as the refusal it carries; the arms
used to choose at the raise, before the collection had written to the
registry, and read two kinds for one registered text.

A left-hand value the document lacks, a left-hand marker, or an empty
left-hand collection compared against right-hand values that hold
several markers gets one record that names every marker's sentence
once, the refused read's first, then the marker the fold kept, then
the rest in value order, numbered as a raise over several reads is,
through one helper that splits a numbered input and never nests a
numbering; the record is built once per comparison, by the arms that
write one. A record's numbered text is read as its producer shapes it:
the registry's parse first, then a lead the registry does not hold
followed by registered reads, then the plain split where each marker
occurs once, so a marker quoted inside a budget sentence is no longer
cut as a numbering. A list of sentences carries a pointer to the list
only where the pointer is shorter than the list, its path escaped as a
path is written, and the console bounds what it prints of a record
(320 characters for a plain record, 4096 for a refusal's) while the
`-o json`, `yaml`, `sarif` and `junit` reports carry the text whole;
a tab in a quoted key prints as itself in a path.

An empty collection on the left under `IN`, `NOT IN` or an ordering
comparator is answered as the empty collection, and between two
queries `==` and `!=` compare the empty list whole with each right-hand
value and refuse the kinds, with a record that carries the operators'
own reason; one cell pins the rules-file empty list read as a value and
another the absent operand decided under every comparator beside a
marker. After `[*]` over a map whose own key names a
member, the block and key spellings (`X[*] { Key .. }`, `X[*].Key`, and
the key read after a binding of the wildcard, `let r = X[*]` then
`%r.A.K`) are refused per value in every role, with the remedy naming
the direct path and the member spellings, where the released evaluator
answered them from the map's own entry; the filter spelling keeps the
one-object reading. The fixtures under `guard/resources/validate/`
(`own_key_block_gate.guard`, `own_key_block_per_value.guard`,
`own_key_key_spelling_compare.guard` and their templates) measure those
readings end to end, and a quoted string left of a comparator is read
as a key in the own-key cells.

README and the clause reference describe each move with the cells that
pin it; `docs_pinned_by` resolves every citation. The known-issues list
gains two entries: the console's two report arms word a plain marker
differently by the side it stands on (pinned by the cell that selects
the comparison's own records by the comparison and the marker operand
and reads both wordings), and a filter's capture over a map holds the
keys the filter kept, not the members, so `%s.Name` reads off a string
and the clause is decided by that retrieval (pinned in all three roles,
each beside its `Sub.%s.Name` control). The map-shaped `IN` control
row prints its three
readings as their own cells, and every ordering comparator is pinned
over the empty collection and the pair record.
One engine arm, a test-only marker equality, test harness boundaries,
and documentation citations.

`RecordTracker::end_record` files a finished record under the open one
and, with none open, keeps it as the tracker's one top-level record. A
second top-level record replaced the first with no error, and `extract`
handed back the last. A run never does this on its own: the file's
record is opened first and every record nests under it. A harness that
drives a recording function bare does, and so would a run whose record
stack a pairing fault above had emptied; one such harness read one
record where a comparison over two left values had written two, with
every row green on the record it happened to see. The arm now returns
the recorder's own error, the same variant the two imbalance arms
raise, in every build, naming the record's context, and the first
record stays held. The field's doc states the contract: one top-level
record per tracker; a harness that drives more than one opens an
enclosing record around them or reads `extract` between them. Two
tests pin it: a tracker driven bare over two top-level records gets
that error on the second and still holds the first, and the same two
records under one enclosing record both come back as its children, in
order. The harness that met the fault cites the refusal instead of
describing the silence.

Twenty-four tests drove a query, a `let` or a rule's status on a root
scope with no record open. Each of those drives writes a record per
entry or per key, and a rule evaluated after the file record closed
opens a rule record of its own, so the second record of the drive was
refused. `RootScope::drive_bare`, test-only, gives such a drive its
boundary: it clears the tracker, opens one enclosing record, runs the
drive, and closes the record, returning the drive's own result as it
came so an expected error can still be matched. It runs the drive
through one free function over any `RecordTracer`, which the
block-scope test uses too instead of pairing the records by hand, so a
`?` inside leaves no record open behind the error it returns; the
enclosing record is closed as the file record a run writes, not as a
`Filter`, which the record walkers read as a query-filter gate, and
the drive's own error wins over the recorder's pairing error when the
drive failed with a record of its own open. The key filter cases, the
three `let` drives, the direct comparison drive, and the three post-run
`rule_status` reads use it. No assertion changes.

`UnResolvedKind` had a hand-written `PartialEq` that ignored the
refusal token, the field `would_replace` and
`RefusalFrame::minted_inside` decide on, and the derive spread it
through `UnResolved`, `QueryResult` and every record type. Production
builds now have no `==` over any of them: the kind exposes
`reading()`, the two facts `Debug` prints, and `Debug` is written
through it; under `cfg(test)` the kind is equal by `reading` and the
carriers derive `PartialEq` over it, so the record-dump tests keep
their `assert_eq!` and the compiler, not a comment, says no production
site compares two markers whole. The one production site that did,
`stands_for_no_entry`, matches the token-free variant instead. A pin
compares the whole `Debug` rendering of every variant with its
`reading()`, so the dump and the test-build equality cannot drift.

The clause reference says an empty left-hand collection fails every
binary operator except `==` and `!=` against a right-hand list, which
compare the two lists as lists, and cited one test for it, the
`Ports == []` control. The populated side of the exception had no row:
`Ports == [1]` over `Ports: []` fails as a plain mismatch naming no
empty collection, and `Ports != [1]` passes. Two cases pin that in the
assertion test, and two more pin the condition role, where the same
comparison decides the gate as a list comparison: `when Ports == [22]`
leaves the rule not applicable and `when Ports != [22]` opens its
block. The `when` sentence said the not-applicable reason names the
empty collection and left the exception inside a parenthetical that
contradicted it; the exception is now its own clause, stated for a
list resolved from the document as well as a list literal, with `when`
and comparison rows for `Ports == Populated` and `!=` over an empty
`Ports`. Two document collections both empty are pinned under `==` and
`!=` in the clause and `when` roles: two lists, equal, so `==` holds
and opens its gate and `!=` fails as a plain mismatch, neither naming
an empty collection, where the orderings over the same two lists
flatten both to nothing and fail at the block level. The two-query
`==` records the two lists as a comparison with no message, so the
gate has no reason text to read; the test reads the comparison record
itself, in the clause role and under `when`: one failing record whose
`from` is the document's `Ports` and whose `to` holds the other list
(its path for a document list, its element for a literal), and no
recorded message naming an empty collection, against a control on that
route whose message does; the doc comment says why the reason is not
read. The KNOWN_ISSUES row for `Parameters[*] { Default !exists }` now
says the block arm refuses it, with the remedy spellings in the
refusal's own grammar, instead of a plain FAIL. The capture-cutting
entry and the `refusal()` doc gain the one case where a refusal met
inside a predicate's run marks no entry: a key filter whose right-hand
query has no answer on the data arrives as unevaluatable with no token,
so the map's marker carries none and the entry stays plain, where the
same side refused as an abandoned comparison carries its token and
marks the entry; the no-token cases stay in one parenthetical and the
token-carrying contrast is its own sentence with its own citation.

Prose citations in CLAUSES and KNOWN_ISSUES named their tests without
the backtick the documentation check keys on, or without the
parenthesis that closes the span on one line, so a renamed test would
have left them pointing at nothing while the check stayed green, and
the check once read on to the next code span and reported a query
fragment as a test name. Each is now one code span per test the check
reads: the seven-test span in CLAUSES.md and the two-test spans in
KNOWN_ISSUES.md are split one span per name, the parenthetical before
the first closes before the sentence that names `error_message`, which
the check read as a test name, and the either-order claim about two
refused maps cites the two tests that pin it instead of a test with no
refusal among its values. The empty-collection citation sits after the
`when` clauses it pins rather than after the query-filter sentence,
the backslash and bound citation names what it pins, a sentence that
said "pinned" twice says it once, and the check's exemption list drops
the label a row no longer carries.

A unit test compiled `docs/CLAUSES.md` into the test binary, which
fails to compile in a crate packaged without the docs directory. It now
reads the file at run time, skipping with a line on stderr when
`GUARD_DOCS_ABSENT=1` is set, the escape the documentation check uses.
The `compile_fail` doctest on `RefusalToken` names E0603, the
private-constructor error, so a path that stopped resolving reads as a
red doctest rather than a pass for the wrong reason; its note says
rustdoc checks the code on a nightly toolchain only, so the resolving
doctest beside it is the check that runs everywhere. A KNOWN_ISSUES
citation that wrapped onto the next line after "pinned by", where the
documentation check opens no span, sits on one line with its source.

Several tests claimed more than they checked. The own-key table's
`refused` column read the filter arm's opener alone, so a decided FAIL
and the block arm's refusal both read `false`; it is now an enum naming
the arm, and every row asserts its arm's text present and the other's
absent. The rstest over a filter after `[*]` on a one-object map took
a bool, `expect_refusal`, that was `false` on about seventy cases whose
names say they are refused, so those rows were checked by FAIL alone,
which a decided failure that had lost its refusal would also read. The
bool is now an `Expect`: `Decided` reads the status alone,
`Refused(needles)` reads the record for one message flagged a refusal
that carries every needle. The twenty-three refused rows name the
sentences the by-name test pins in the record dump for the same
clauses over the same inputs (the opener, the key named "a key of the
map itself", the stray entry, the array-or-single reading), and the
two own-key rows over `BucketA` add the binding of `X`, "where `X` is
`Resources`", beside `X.*.Key`, so a remedy an author cannot fill in no
longer passes. The sentence about the map handed on as one object
prints its path with a location, so its path needles end at the `[`
that opens the location and `/Resources` no longer matches a member's
path under it; the block-arm row of the own-key table reads every part
of the refusal on one flagged message rather than gathering them from
the whole tree's dump, with the remedy in its own grammar, `X.*.Key`
and "where `X` is `Parameters`", and KNOWN_ISSUES says the sentence
names the path with its location. The comments on those rows and the
KNOWN_ISSUES rows for them said the refusal spells `Resources.*.BucketA`
out; it writes its remedy in its own grammar, and the texts now say so.
A gate test read `refusal: true` anywhere in the tree, which the
binding clause's own record satisfied; it reads the gate's own
comparison records. Two token-identity reads passed when the marker
they compared against was absent; each row now says whether that
marker is recorded, with the reason: where the refused read is the
comparison's right operand the raise numbers it into the entry's text
and records no marker under its path, and in the two-refusal folds
neither map's marker is recorded, so those rows assert the absence and
read the refusal's reason and token off the entry's text and kind, and
a change that records the marker flips the row and brings the token
read back; the rows whose marker is recorded assert it before
comparing its token, and their comments name the arm that mints the
marker (the `[*]` key arm, and the block arm in the block fold), not a
key filter. The two-refusal fold test tells later from earlier where
the raise numbers both: the refused read the fold kept leads the
numbered text and the earlier follows it, in both document orders; the
comment in the capture test that said the in-run marker stands at
`Allowed` says where it does and where the recording leaves it out.

Every case of the own-key rstest reads the record: a decided case
asserts that nothing on record is flagged a refusal, so a clause that
drew one is not read as decided by its FAIL alone, and the `!empty`
spelling over the scalar-entry map, which the by-name test and
CLAUSES.md read as refused, is a refused case naming the stray entry.
The two one-object maps whose sub-maps share no key name, the resource
metadata and the all-structure properties, read that the refusal leaves
the member spellings out, which KNOWN_ISSUES claimed and nothing
checked; the check compares against the engine's own constant. The
refusal texts are one set at module level, read by the by-name test in
the record dump and by the rstest coupled with the refusal flag; the
by-name test's copies are gone, the two-needle dot-wildcard set has its
own name beside the bare sentence, and the comment that said every
needle has a by-name counterpart says which rows do not.
The two-list test names the record kind each arm writes (the two-query
arm an `InComparison`, the literal arm and the empty-collection route a
`Comparison`) and reads the `when` gate's not-applicable reason for
what it is: absent where the record carries no message, which both
two-list arms leave, and present naming the empty collection on the
control, so `-o json` carries a reason for the one gate and none for
the other two. The literal needles match the value's own `Debug` shape
rather than a bare number, so a location's figures cannot match.

The own-key rstest evaluated every case twice, once for the status and
once for the record; it evaluates once, and the status and the messages
come from the same run. The marker-kind pin renders a plain marker
that carries a token for an enclosing run as a plain marker, which is
what the reading says it is.
A refusal is a read the evaluator could not decide: a regular
expression that spent the engine's limit, a filter applied to a
scalar, a one-object filter whose walk was refused, or a key filter
whose right-hand query met one of those. Before this change the fact
that a value carried a refusal was re-derived from its text wherever
it was read, and several folds lost it on the way, so a `not` over a
rule that failed only on refusals passed, a `when` gate skipped its
block in silence, and the stderr report of a refusal decided past by
a sibling was missing or named the wrong clause.

Refusals travel as tokens. The registry keeps every refusal sentence
with the token of its latest delivery; a marker, an undecidable
comparison error and a key filter's wrap carry the token of the
refusal they stand for, and a comparison that registers a read it met
keeps that read's own timing instead of minting a new one. A filter
predicate run reads only tokens minted at or after its start, so a
refusal delivered inside the run marks the entry refused while one
bound before the run does not. Where two refusals compete for a
pairing the later minting wins, with a marker's own token ranked over
one recovered from the registry.

A FAIL carries its basis. Every evaluation answers a status and, on
FAIL, whether that FAIL rests on a decided no or on refused reads;
folds combine the bases of their branches (`and` for conjunctions and
every-entry blocks, `or` for disjunctions and `some` blocks) and the
rule-status cache stores the pair. `not` over a referenced rule whose
FAIL rests on refusals alone now fails closed, with a record naming
the rule and each refused read; a FAIL with a decided no is inverted
as before. In a gate or a filter predicate a refusal is raised, never
turned into a status, so `when not r` neither opens nor skips its
block on a refusal.

A refused one-object selection inside a block body is judged beside
its siblings, in the assertion and the gate roles alike: the block's
FAIL is decided when its verdict over the other values agrees with
the verdict over them plus the refused value, and rests on the
refusal otherwise. A pending selection is settled on every exit of
the block, so its record is written and its refusal reaches the
report whatever ends the block. A key the one object holds is read as
nothing, as the dotted spelling reads it; only a key it lacks is the
refused read.

A filter over a scalar met inside a filter predicate is the refusal
it is outside one: the member is undecided, its siblings are still
tested, and `!empty` over a selection holding a refused member fails
closed. The notice for that reading is written once per filter site,
only where a verdict moved.

Every refused read a clause decides past is reported on stderr at the
clause's own position, with the clause as the author wrote it and
one verdict phrase: passed by a value that holds, passed by a sibling
clause or entry that holds, decided by a sibling clause's or entry's
failure, or passed by the negation of a rule that failed. A raise
absorbed by a block fold, a conjunction or a disjunction is reported
once by the clause that decided past it; a filter predicate's raise
becomes the entry's marker and is reported by the clause that reads
it; a raise that reaches the top is reported as the failure itself.

The console lays out a refusal record from the record's parts: the
retrieval's lead on `Error =` and the refusal once on `Reason =`,
each line bounded on its printed width, controls and bidirectional
overrides escaped, a cut never falling inside an escape. A left-hand
marker decided by an absent right-hand side names the side and
carries the absent value as what it was compared with. Retrieval
reasons bound their key, path and quoted value where they are
written, so a long key or value no longer pushes the refusal past the
cut. The structured report keeps `error_message` byte for byte and
prints a clause's own block record after its children.

The user documentation describes each behavior: README entries per
change, the clause reference's items on markers and the refused-read
line, and known-issues entries for the two readings of a negation,
a refusal's own timing, and the record order of a deferred raise. A
test checks that every "pinned by" citation in those documents names
a test case that exists.
A FAIL that no branch folded a basis for was read as a decided no, so
a `not` over a rule that failed only on refused reads could pass. Such
a FAIL is now an undecidable with no read and fails closed by
construction; the folds that build a strict failure's basis share one
rule, the refused reads a site collected win, and with none the first
undecidable error's basis stands, flagged, instead of an empty one.
Every site that fails closed on an undecidable that is no refusal (a
spent regex budget, a value the engine could not read) keeps that
error's text as the cause, and the `not` that fails closed over it
names the text in its record and its raise. A basis names at most
four causes in the order met and counts the rest once; the record of
a `not` that fails closed names at most four refused reads the same
way, and a gate's raise beside a refusal names at most four markers.
A count of one is written in the singular. A strict FAIL's basis
carries each refused read's token, so the fallback raise and a
fail-closed negation read the token off the basis. A unary clause that
failed several values closed on undecidables that are no refusal, and a
comparison that met undecided pairs and no refused read, join every
such cause into their basis under that cap, the first four named and
the rest counted once; the comparison names the reason its raise names
first and then the others in the order met, and the raises are
unchanged. A `not` over a unary clause whose values mix an undecidable
that is no refusal with a refusal marks its fail-closed record a
refusal whatever failed first; the behavior before this change marked
it only when the refusal failed first. Under a rule's assertion, a
strict comparison over right-hand values that are all markers a map-key
filter left for maps it could not read, with no refused read among the
values and more than one reason recorded, fails with a basis of every
marker's reason, the first four named and the rest counted, where the
behavior before this change raised with one reason. The raise arm asks
for the right-hand causes itself, so one recorded reason beside two
right-hand markers joins two causes. A strict comparison whose raise
carries a token, a refused read among its values, keeps its error path
and its refusal flag, and this return is taken only when the raise
carries no token and no refused read was collected. A refused left read
is filed by the per-pair arm and collected there, so the collection is
the one registrar of refused reads and a refused left read keeps its
flag on the strict return. The compared-nothing arm keeps a net for a
refused left read that behaves the same in every build: it sets the
left-marker flag, the undecided reason and the marked read that the
per-pair arm sets, and notes the read for the collection; no known
shape reaches it. Its own scan, which had fixed the first marker's token
for a text before the collection ran, is gone. That FAIL rests on the
undecidable through the clause's judgement, which writes the clause's
record in the clause-error shape with the refusal flag taken from the
basis, so a `not` over it fails closed and names at most four causes.
A decided basis is judged as
before. A block that holds such a comparison keeps that basis through
its fold. A strict `some ... NOT IN` over right-hand values that are
all key-filter markers, several of them undecided, returns its FAIL
with the joined basis under the same guard, no refused read and no
token, so it fails closed on every marker's reason and its record
names the first four and counts the rest, as the positive IN does. The
compared-nothing arm and that raise decide whether there are several
causes by one test on the basis already built, so no second basis is
made and dropped where the raise declines. In every other case, a
`when` gate included, the comparison raises as before and the verdict
is unchanged. The comparison collects its undecided reasons only in the
strict role, the one that builds a basis from them, in one ordered map
of distinct reasons, and renders the right-hand markers' reasons only
where a basis is read, so a comparison that builds no basis renders
none; the basis is built only when no refused read was collected, and
the unary clause folds the bases of its errors only when it collected
none, since the reads win. The block's per-value arm folds each value's
basis the same way, and its own-key refusal record keeps on its basis
the token its registration returned, the run's own, so a fail-closed
negation reads that token off the basis. The unary raise notes and the
frame notes map the reads, which now carry tokens, back to their texts
and name the same texts as before. Joining two failing bases extends
the first in place, with the reads indexed by their text. The basis
keeps its reads and that index in one type with private fields, so the
index changes only where the list does; two bases are equal by their
ordered reads, and a clone is equal.

The every-entry block reads its decision off the folded basis. A
failure that rests on a decided no decides a sibling's raise; one that
rests on an undecidable decides nothing for it, and the raise is the
block's own. In the gate role an every-entry block whose entry nests a
block over a lone refused one-object selection, beside an entry that
raised, now raises and the rule it gates fails closed; the assertion
role prints the converter's error and the refusal once each, and a
gate prints the condition's sentence naming the error alone. A
refused one-object map beside a one-object or a list sibling reads
the same way.

The conjunction and block folds rank a refusal above a held error that
is none, whichever came first, so the record is flagged and names the
refused read in both clause orders. A held comparison or held error
with no token is ranked by the token its registered text carries, on
both sides and in either conjunct order. Inside a filter predicate run
a held comparison takes an arriving registered refusal as a pair, the
text and the token the registry holds for it, ranked by the run's
own frame: a refusal minted inside the run replaces the held text and
token together and marks the entry refused, and one minted before the
run does not make the entry the run's, so a refusal minted inside the
run outranks one minted before it whichever arrives first, and the
held error keeps its variant. The raise that ends a run recovers its
token from the registry by its text when the error stored none. A
refusal met before a filter predicate run replaces a held budget
comparison inside the run; this is a deliberate move to the stricter
side from the behavior before this change, where the comparison stood
and the run went on with it. Outside a run the swap goes by the
refusal's token or its registered text, in the held error's own
variant. An arriving refusal that is itself registered outranks a held
one by token rank, in either order, inside a run and outside one, and
replaces it whole, so a join holds one refusal sentence at most.
Outside a run a held refusal comparison meeting an arriving registered
refusal is ranked by token too, so both variants, in both orders, end
on the later-minted refusal and the held variant is kept. Outside a
run the error a fold returns carries the own token of the side it
keeps, and none for an arriving plain refusal, so a folded error reads
as an unfolded one does. While a run is live, the fold stores the token
it ranked the kept side by when that side carried none of its own, so
the run reads that token at its end whatever the registry holds by
then. The registry gives a second reason to hold it. It lists the
newest token written for a sentence and the newest each live run reads
as its own, and a write cuts the list to those, so the list is bounded
by the live runs plus one: a sentence registered twenty times under
one run lists one token, the newest. A read, and the token that a
comparison's or a wrap's registration hands back for its marker, is
the newest token that the innermost live run owning any of them reads
as its own, else the newest of all. Every collector of refused reads
(the comparison's, the unary clause's and the join of two bases) is
handed the ranking of the frames its caller holds and keeps, for a
sentence it meets twice, the token that ranking reads as the run's own,
so the entry reads the same whatever order the values arrive in; the
live runs' ranking is read only where no caller holds frames, and a
join made with no live run ranks by minting alone. A read's carried
token and the registry's are folded by the pair rule under the run's
ranking: the run's own token wins, a read that carries none takes the
registry's, and a text no registry holds keeps the token it carries.
The comparison's raise, strict or not, takes its token by the same
rule at all four sites where it raises, when a
marker's carried token and one recovered from the registry differ, so
the token the run reads as its own is raised, where the behavior before
this change let the carried one win. A comparison's raise inside a
filter predicate therefore carries the token the predicate's run owns
when the registrations recover one, and not the token a marker brought
from outside the run: where a marker memoized by a file-level let
carries an older token for a sentence the predicate's own body also
raised, the entry is marked refused, where the behavior before this
change marked it plain and the record reported a spent budget. The
verdict does not move. The rule applies at every comparison raise,
inside a predicate run or not: outside a run a raise that carried no
token carries the one the registrations recovered, and the verdict is
unchanged, while the record's flag and its ranking against held errors
can change. The compared-nothing raise takes the right-hand marker's
token and folds it with the recovered one by the same pair rule as every
other fold, so a marker token minted inside a binding resolution, which
the run excludes, no longer beats a token the run owns. A
run reads its ending token and pops its frame in one place, so the
token is settled while the run is still live and a later write of the
same sentence cannot strip it; the token of a run that has closed stays
listed until the next write. The fallback for a text no live run owns
is minting, kept in one place and used for a text whole, for a single
part with no owner and by the wrap writer, so every reader of the
registry answers the same token, the newest; the fallback takes no
ranking. Choosing among the parts of a numbered text is the pair rule
under the frames' ranking, because the parts are different refusals
and a part a run owns outranks a later-minted part the run excludes.
Each block and each conjunction builds its ranking once when it starts,
and every fold and hold of it uses that one. Outside a run
the newest answers. A run owns a token it is eligible for, except a
token minted since a binding resolution that is still open began, when
the resolution opened after the run. So a file-level let first read
later in the run, whose query raises the byte-identical sentence under
a newer token, no longer marks the entry plain for a refusal the run
raised itself, whether it was held as a plain error or as a
comparison, and a run opened inside a binding's query reads its own
token, not the enclosing run's. The entry reads refused and the
refusal is reported, where the behavior before this change read the
entry plain and reported no refusal; the known issues list the move.
Where the selection is the left-hand operand of the outer comparison,
a passing sibling decides the `some` and the refused member is
reported; where it is the right-hand operand, the clause fails closed.
One case still reads plain: a nested run whose raise carries no token
and registered nothing inside the run reads the enclosing owner's
token. A tokened text that no registry holds, such as a wrap of a
numbered text, is never registered, and against a registered refusal
the outcome depends on the order they arrive in: a registered refusal
held as a plain error keeps its place against a later wrap, one held as
a comparison with no token gives way to a wrap minted after it, and a
wrap held first stands against an arriving registered refusal of
either variant. A refusal arriving after a plain error names the
refusal's sentence first and the held cause after it, cut to its
template part with its backslashes as written and a control in the
cause carried as its escape; the joined text is registered under the
token, so the raise names both and the record is flagged.

A refused-read line drops a reader only from the holds its text names,
by the hold's sentence or by the token of a registered text, so a
clause run over several resources no longer loses another resource's
notice by document order. A capture declared where nothing can bind it
is announced once on the warning channel, outside a filter predicate as
inside one: its reason is keyed, so the refused-read line that a
decided clause writes for the refusals it never raised leaves it out,
and a clause decided past by an or sibling no longer reports the
capture twice in two wordings. A called rule whose own `when` refused
keeps its sentence and its refusal kind on its record. A call that
carries a `<< >>` body keeps them beside the body: the rule report
holds the body as the custom message and the sentence as the error
message, flagged a refusal when it was one, so an evaluation refusal no
longer reads as an ordinary compliance failure on the console or in
`-o json`. A body of blanks is no body. The record tree written by
`--print-json` carries the displaced sentence as displaced_message; its
kind is not written. A failing block whose children report nothing,
as the per-value record of a refused selection leaves when the block
exits before its fold, is listed in the structured report by its own
record, with its message where it has one and otherwise a sentence
saying that the block failed and nothing recorded under it reports
why; a rule whose failing block's children reported nothing was listed
with empty checks before this change. When the block's fold raised a
refusal, the block's own record carries the refusal's sentence, flagged
a refusal, into the structured report, and its text is the error's own
text whichever error variant the fold held. A rule that failed closed
on its `when` condition keeps the condition's sentence as its own
message and its checks stay empty, as the behavior before this change
did, so the console still names it by that message. The condition's
children are listed only for a failed rule that has no message of its
own, and for a type block whose `when` raised, whose type check carries
none. A block run as a filter predicate's clause sits under the
Filter record, which the walker does not visit; there the clause's own
record carries the refusal. The reporter's generic sentences, a
childless block's included, are the fallback and are flagged as no
refusal. A rule run recorded under a call inside a block or `when` body
whose failing block is silent is listed with the generic sentence,
never with empty checks, where the behavior before this change listed
empty checks. A parent that has a message of its own is listed by it,
and its silent child blocks are not listed beside it. Under a parent
with no message, a silent failing block beside a reporting sibling is
listed with the generic sentence, where before this change only the
sibling was listed, and the generic line for nested silent blocks
carries the innermost block's context. A silent failing block is listed
with the generic sentence once per block clause, never once per value,
as before. The count of marked FAILs
advances once per deciding call that marks one. The scalar-filter
sentence is registered only in the arm that builds its refusal marker;
a capture after `this` over a scalar builds its marker through the
shared refused-capture path, carries its own sentence under a token,
and fails closed, so a kept-member `!empty` over such a list no longer
passes. A filter predicate's frame of absorbed raises is settled in one
place for both readers, and a debug build asserts an answered predicate
leaves it empty. The reader recount and the hold-walk and hold-entry
counts run in test builds only; the hold store's reader total is summed
where a test reads it.

The console takes the cut flag from the cutter that cuts, so the
backstop marker is added only to a text that was cut, and the
unattributed section's plain messages are escaped as rule records
are, a cut never ending inside an escape. A clause's message is
divided at every semicolon, line feed and carriage return, and its
error is joined into one line first, so a line break is joined as the
one-line forms join it. Each console line of the per-resource output,
and every line of the failed-clause tree, is escaped once, at a sink
that escapes the finished line; the escape is idempotent, so a field
escaped earlier prints unchanged. The strict escape set is the default
on every console line and field: keys, paths, contexts, values, errors
and reasons, because an evaluator's sentence quotes keys and the
record stores it that way. That holds in the section for findings that
belong to no resource too, and the contexts of that section are
escaped. A clause's position writes its rules-file path with each
character that breaks or hides a line as its Rust escape and a
backslash as written, in the console, the notices on the error stream
and the structured reports alike, and a text escaped once is not
escaped again; a path with nothing to escape is borrowed. The relaxed
set, which lets joiners and marks print as typed,
applies only to the rule author's message text, in the per-resource
output and on the tree alike. The custom slot of a rule's record says
whose text it holds, the evaluator's sentence, the author's body or a
refusal, and the console takes the relaxed set for the author's body
and the strict set for the other two. The Rule block prints the
author's body whole under the relaxed set, as a clause's message is
printed, where that line used to cut it at a bound meant for text the
evaluator quotes from the template. It prints the sentence a call body
displaced on its Error line through the pipeline of its kind: a
sentence that is no refusal's is collapsed, escaped and cut at the
recorded-message bound, and a refusal's goes through the refusal
pipeline and is printed whole under its own bound. The section for
findings that belong to no resource joins the two halves as a
clause's are; a negated call reads two entries naming the refusal
there, because the called rule's refusal and the negation's
fail-closed block each explain their own record. A tree line that can
carry the author's message is written through one helper that escapes
its body strictly as a whole and appends only the message, last,
through the relaxed set, so a field added to such a line later cannot
print a joiner raw. A clause context is printed as it is, spaces
included, unless it spans lines, which are joined on its Check line
where a break sits between two pieces that hold text, so a context
ending in a carriage return is not trimmed. The comparison writers
escape each field they take from the template before it is formatted
in, and their output is written behind a backstop that keeps only the
two sequences the writers emit, the green that opens an excerpt line
and the reset that closes it, and writes any other control or sequence
as its escape; a source excerpt keeps its colour and its tabs, and
takes the colour as a parameter. An escape byte no longer reaches the
terminal or makes the JUnit report invalid XML. An attribute value
writes a tab and a line feed as the character references &aws-cloudformation#9; and
&aws-cloudformation#10;, which a parser reads back as the character where a literal one
in an attribute value would be read back as a space, and the markup
characters as entities. A carriage return is written as its escape,
because a reference to one decodes to a raw carriage return for every
consumer, which a terminal reading a CI tool's output acts on; a
carriage return and line feed pair is written as the escape followed by
the reference, with no fold. A backslash is written as it is. So no
control other than the tab and the line feed reaches a consumer raw,
and a name reads back as it was typed, a Windows path included, except
that a control reads as its escape: a name holding a control and a
name holding the typed characters of its escape read alike, as an ESC
already did. Every other control, a zero-width joiner and a
bidirectional mark are written as their escape, since a name is an
identifier read against other names. The test command's case names
take this path. A case name reads as typed in the attribute, where a
warning that names the case reads it with its backslash doubled, and
two case names that differ only by a control and its typed escape get
one JUnit name. A text node keeps its tab
and the line feeds that join messages and treats a carriage return and
line feed pair as one line feed, and a lone carriage return is written
as its escape. U+FFFE and U+FFFF, which XML 1.0 forbids, are written as
escapes in a text node and in an attribute value alike. The failure
element of a test case carries the rule record's own message when no
check under the rule says anything, as a rule that fails on its own
`when` has no clause under it: the author's body goes under the
relaxed set, the evaluator's sentence and a refusal's under the strict
set, and a clause's `<< >>` message in the failure text is written
under the relaxed set too. The messages of a rule's checks were walked
a doubling number of times per level of a call chain; they are
collected once per level and that slice is tested for silence, so a
chain of four rules walks them 4 times. The SARIF
results are unchanged. The line written when a fold finds an entry its
invariant says cannot be there now says it is an internal error and
asks for a report with the rules file and the input data.

Tests read a key filter's marker by every field (kind, flag and
reason) in the gate and the assertion roles, with controls that must
read false; the two-hold, order, fast-exit and frame cells cover the
token and raised paths; a run's error is its own cell in every loop;
every loop table carries its length. README, the clause reference, the
query table and the known-issues entries say which cause a refused
capture names, where a refused right-hand side owes its line, that the
ranking's limits and the unrecorded-entry line exist, and state what
ran as fact; the shapes the releases were not run on are recorded as
unmeasured on the releases.
The structured report walks a failed condition's records only for a
failed rule with no message of its own, a run-ending error whose record
tree is still reported before the run exits, and for a type block whose
`when` raised. It now keeps a record for what it is, never for the
wording of its sentence, and filters beneath a disjunct's clause, block
and `when` records too. A per-value check that carries a refusal flag
is kept. A reference that failed on an error is kept and rendered with
its own message, and one that failed on a refusing error keeps the
refusal flag. A refused value's block is kept with its sentence and
flag. A block or clause-error record is kept where it explains
something or where its children survive. A record whose children were
all decided answers and which explains nothing is dropped. A called
rule and a disjunction are kept where something that carries a raise
remains, or where their message is the evaluator's, not an author's
`<< >>` body written onto a decided no. A decided answer is not kept:
a missing property in the first disjunct of a `when` is the rule's test
of whether it applies, not a violation. Before this change a walked
condition listed every record beneath it, a decided answer included, so
a missing property in a disjunct read as a violation. The line between
a decided answer and a raise is structural: a block record says whether
it was written on a raise, and the walk keeps it by that, by its
children or by its refusal flag, so an explained empty selection
that carries a message is never listed as a finding. The new record
fields are all serde-skipped, so they add no bytes to the validate
`-o` outputs; the SARIF and JUnit changes described below are
separate, and so is the test command's new optional `reason` key in
its json and yaml.

A rule that failed closed on its `when` condition keeps its own
sentence as its message and its checks stay empty, so the console,
which names such a rule by its message, is unchanged. The sentence also
names, once each and after the held one, every other refusal the
condition's records name, so a `when` of two refusing disjuncts reports
both refusals, where the behavior before this change named the held one
alone. The refusals join the evaluator's sentence, the one a call's
`<< >>` body displaced where there is one; an author's body is reported
as written and is never edited, in the console, in JUnit and in SARIF,
and the section for what could not be evaluated prints a rule's body
when none of its checks is rendered per resource. A sentence that gains
refusals keeps the kind of the sentence it joined, so a rule whose
verdict a plain error decided is not flagged a refusal, and the
refusals ride as parts of their own. The console prints a joined
sentence part by part, escaping the plain head and cutting it at the
recorded-message bound, one number it now reads, and printing each
refusal whole, as described below. A rule that has a text of its
own, a call's body or the evaluator's sentence, is walked with the
generic fallback off, so a silent block adds no generic entry in the
body's place. A refusal is named once. A wrapped refusal and the bare
one are the same refusal, read through one normalization, and a refusal
is identified by its token or its exact text, never by containment, so
a bare "A" beside a numbered "(1) A (2) B" is listed in either order.
A called rule's own sentence and its children's refusals are both read,
and a remainder after a refusal is a part of its own only where it
names a registered refusal; an unregistered remainder is never a
refusal. A disjunction's or `when` gate's wrapper message, which quotes
the held refusal, is skipped by its kind. A type block whose `when`
failed closed lists its refusal once, which a report cell checks. The
generic sentence still lists one entry per silent block clause.

A sentence that gained refusals carries them in three groups behind
three joiners, each ending in a colon and a space. The refusals of the
record's own failed condition follow "the condition also met", those of
a condition nested in a body follow "a nested condition also met", and
the marks of clauses that ran, a block's clauses, a disjunction's
disjuncts and a called rule's body clauses, follow "a body clause also
met". The parts hold the groups as separate fields, so the refusals
alone are returned without joiner text, and the console, which rebuilds
the joined line to bound it, reads each joiner from the part. A refusal
is never told twice across the groups. A refusal read under a failed
condition is a condition refusal wherever it is joined: a block's and a
disjunction's sentence take the refusals beneath them in the two groups
every other host words apart. Those read under a condition nested
beneath the record, an unreached record's among them, follow "a nested
condition also met", and the marks of the record's own clauses follow
"a body clause also met", each refusal once across the two. A sentence
that gained only body marks says the rule failed. The head that says an
error ended the evaluation is written when the rule's record says
one ended its body or its own condition, a flag that no structured
output carries.

One bounded cutter cuts every refusal line. It backs up to a word
boundary at most 16 characters, never across the last real joiner and
never below a floor, and otherwise cuts by count, trimming trailing
whitespace before the ellipsis. The floor is applied last: a trim, or a
move back out of a split escape, that would take the cut under it is
not taken. A move back that would fall under the floor cuts at the
escape's start, untrimmed, so no fragment of the escape prints. Every
"; " on a refusal line is a floor, whether the line is a joined
sentence or arrives as one string (an entry that lists refusals alone,
a rule reference's text with a callee's sentence in it, a numbered
raise). The floors are one rule, read off the assembled, escaped line
itself, whichever way the text arrived: after every "; " inside a body,
after a known head that stands at the start of a body or right after a
"; ", and after every joiner text a body holds. The start of a piece is
a boundary for the head rule exactly as a "; " or a joiner end is, so a
line arriving as one string and the same line joined from pieces take
identical floors and identical cuts, and a cell pins that over an ASCII
sentence with a head after a "; ", a head right after a joiner, and a
multi-byte head. A head gets its floor at every boundary, not at the
string's first character alone, so a callee error that opens with a
head after a "; " keeps the refusal's first characters. A refusal's
prefix is never dropped, and one text is cut the same way on every
record kind. The single-string cutter is the joined one over one body,
with no second scan. There are two cut pipelines, and the docs say
which lines take which. The three sentence joiners are one shared
table, read by the sentence's parts and by the cutter, and the table
and the sentence's three group texts are typed by one length constant,
so a joiner added without its group, or a group without its joiner,
fails to compile instead of being dropped. The line is built the same
way whether it fits or is cut, and its joiner offsets are counted once
as it is built, not recounted for each "; ". The floor for a joiner
text inside a body is a character offset, and it is tested against the
character bound as one: the byte offset of the joiner's end is counted
as characters, with one cursor over the line, before the test, so a
refusal line holding multi-byte text keeps its prefix at the cut. On
the per-resource block line the head yields first: it is cut to the
room the backstop leaves after the join and the refusals, at least a
floor of 80 printed characters, ellipsis included, and at most the
plain bound. The floor counts the printed head, so the cutter's own
floor is three characters less, because the ellipsis is added after
the cut. A head that fits prints as it would without the refusals,
a line one character over the backstop loses one character before the
ellipsis, and the refusal marker appears only when the join and the
refusals alone exceed the backstop.

A clause block with a message always writes its own entry, holding its
context and the author's text. The records beneath it defer their marks
entries, and the clause joins the refusals no entry beneath carries to
its message, as a rule's sentence does. Only a block entry beneath
tells a clause's message, so a negation of a called rule keeps its own
sentence beside the run of the called rule. A parent's sentence beside
entries is written only when it is one of the evaluator's, an error
behind its wrapper or head or a registered refusal, and it is told only
when an entry beneath carries the error it quotes, compared through one
reader; the heads and tails of those sentences are two lists shared by
the writers and the reader. An author's text is listed only when
nothing beneath it reports. A called rule's body clauses that met
refusals no check names add an entry to the rule's checks holding the
refusals alone, with the context of the record that carries its first
refusal, so a gate mark wins over a deferred condition record above it;
when every check is silent the sentence joins them behind the body
joiner. One walk of the gate marks serves the mark groups, the deferred
condition records and that context lookup. That one walk yields the
refusals of the records the reporter never reaches, and the deferral a
rule with a text and an access clause hand their children covers the
message-less records the reporter reaches. The walk for
marks stops at a failed block, disjunction, `when`, type block or
access clause that carries a sentence only where the reporter writes
that sentence or entry, which holds while every record above it is one
the reporter descends into with its original children. Beneath a
record kind the reporter ignores, such as a filter's predicate records,
or inside a condition's filtered copy, it does not stop, so a refusal
under a messaged `when` there is walked and joined to the nearest
ancestor that reports, and is not told by nobody. A failed `when` or
type block the reporter does not reach writes no entry, whatever its
message. Beneath it the walk reads every refusal its failed condition
met, a gate mark or any other refused record, as it does for a reached
record, and joins them at the nearest reporting ancestor, whether or
not the unreached record has a sentence of its own. Every host that
joins the marks beneath it reads these refusals off the one walk:
a type block's, an inline `when`'s, a block's or a disjunction's
own sentence takes them into its nested group, a type block or `when`
without a sentence lists them in its refusals-alone entry, and a rule's
sentence or an access clause's entry takes them with the message-less
ones. A reached record with a sentence still writes its own entry. A
failed called rule the reporter does not reach, such as one beneath
a filter's predicate records, has its run reported by nobody, so the
walk treats it as it treats an unreached `when` or type block: its
condition's refusals and its own refusal sentence are read as the walk
of a failed condition reads a called rule it meets, and joined once at
the nearest reporting ancestor behind the nested joiner, under a rule
with a text and under a type block or inline `when` with a sentence
alike. A reached called rule still reports its own run. A mark under
the evaluator's own-condition shape counts in the record's own group,
and a check inside the condition does not make it nested. The nested
joiner appears on a type block's or a `when`'s sentence only where
the nested record's own entry does not tell it, that is, for a nested
record the reporter does not reach. A nested `when` beneath a record
kind the reporter ignores writes no entry, so its refusal is joined
to the nearest ancestor that reports, behind the nested joiner, and
a cell pins that shape. The walk tracks whether the reporter reaches
a record apart from whether it may stop there. Beneath a rule's or a
type block's condition the reporter writes no clause entry, because
it reports a filtered copy, so the walk is not stoppable there, but
it does report a failed called rule's run. The walk therefore enters a
failed called rule only where nobody reports its run, so a gate mark
inside such a run is told by an ancestor and never by nobody, and a
rule inside a condition tells its own marks once. An entered called
rule counts as a check between a mark and the record whose walk it
is, so a mark under that rule's own condition reads behind the nested
joiner. A reached nested record with no sentence defers its refusals
to the host above it where the host is a rule with a text or an access
clause that writes its own entry; a type block and an inline `when`
hand their children no deferral, so a reached nested record beneath
one writes its own entry. A mark under a record that did not fail is a
decided answer, not a finding. A cell pins that a refused read which a
sibling's decided no decides past, in the gate role, leaves the calling
rule not applicable, and that the refusal is told in the warning on the
error stream and on the called rule's record, not as a finding.

A refusal whose own text holds "; " is recognized wherever it sits in a
joined sentence: the parse tests every span from a start to a later
boundary, bounded at sixteen boundaries per start, and keeps a span
only where the registry holds it as a refusal. A numbered raise of
refusals whose text holds "; " is registered as its parts. The debug
check for converted reads uses the same parse.

A gate that fails closed on a plain error, such as a converter's error
in a sibling clause, names the refusal its selection kept in the rule's
sentence, once, because the gate-role mark of a refused body carries it
there. The verdict is unchanged. Before this change the gate's sentence
named the error alone and nothing named that refusal, which known issue
25 said; it now says the refusal is in the rule's sentence, and that a
registered refusal replaces a held cause where an unregistered one
keeps it joined. A refusal met beside a gate's plain error also reaches
`-o sarif`. The mark is a serde-skipped field of the block record,
so the context string and the `--print-json` tree are unchanged,
and it is recognized by that field alone, never by the shape of
the record: a strict-role record of the same shape is a finding
and is listed with its sentence and flag. A block that is listed
tells the refusals of its marks itself: joined to its message when
it does not already carry them, as its sentence when it has none,
and as an entry of its own beside its children's; it never displaces
a parent's own message, and a refusal told by a nested entry
is listed once. A type block, an inline `when`, a disjunction or a
block with a sentence of its own joins the gate marks beneath it,
through message-less child blocks, whether or not its children listed
entries, and a refusal a clause entry already carries is found from the
entry's parts, not from the tail of its rendered text. A message-less
type block or inline `when` whose deep marks no entry beneath carries
lists one entry holding the refusals alone, with the refusal flag, in
every output, so a refusal is never lost. A gate block with several
refused body values lists one entry, its own. A filter predicate's
body keeps no sentence, because the enclosing clause's record carries
its refusal. A type block's failed condition is read for refusals as a
rule's and an inline `when`'s are. A block record says whether it was
written on a raise, at every writer: the refused body, the settle
record, the type-block gate condition's raise and the disjunction's
run-stopping error say so, and the per-value record of a decided
refusal in the gate role does not. A raised record with no sentence is
listed by the silent-block sentence, never by the retrieval fallback,
which was false there. A failed rule reference stores the error it
failed on once, its text, its token and its refusal, and the walk and
the filter both read it, so a refusal a reference failed on is named
once in the rule's sentence whatever its message words, and the
reference's report prints that stored text after the reference's
sentence. The console and JSON rows of the gate cell each read the
refused map's path once per gated rule. The access clause's fail-closed
arm asserts in a debug build that it is reached only in the strict
role, since a non-strict undecidable there would read as a condition
that did not match. The comment on the strict undecidable return says
its implication rests on a debug assertion that no token was recovered,
unchecked in a release build, so a path that recovered one would drop
it. Known issue 27 and the README no longer claim a verdict is
unchanged without a reading of it. When a condition raises an
unevaluatable error and its rule, inline `when` or type block fails
closed on it, the entries the condition noted are re-noted as leaf
failures and no refused-read line is written for them; the refusals are
told once, by the report walk of the failed condition. The closing
record of each of the three converting sites keeps those reads, and the
walk compares them with the refusals it collected and the entries
the arm wrote: a read the registry holds as a refusal and nothing
names trips a debug assertion, and a release build runs nothing
extra. Every converted read reaches that record on every path,
including a called rule's record rebuilt for a call with a body and
a type block's two run-ending exits.

A failing rule whose checks say nothing now has a SARIF result carrying
its own record, such as a call's body over a silent block or a gate's
sentence with its refusals, where the behavior before this change gave
it none, so a failing rule is never absent from a format. A clause or
block result keeps its text byte for byte, trailing space included;
only a rule-level result, which is new in this change, joins its
non-blank parts with one space and keeps the body as typed. The level
travels with each message through the nested walk, so a rule's own
record is rule-level wherever the rule sits.

`cfn-guard test` names a joined refusal when an expectation is unmet.
The plain line ends ", Reason = <sentence>", built by the console's
pipeline: split on line breaks only, escaped, each part bound by its
kind. The JSON and YAML reports gain a reason field on the failed rule,
and JUnit appends ", Reason = .." to the failure text; they carry the
parts whole. Before this change the output of the test command named no
message for such a rule.

The console header of a resource, in the CloudFormation and the
Terraform reports, writes the name, the type and the CDK path through
one shared writer that escapes them like every other template text,
before the name is colored. Before this change they reached the
terminal raw. The writer's buffer is read lossily, so one bad byte
keeps the excerpt where the whole excerpt was dropped. The closing
sentence of the internal-error line asks for a minimal reproduction
with sensitive values removed, where it asked for the rules file
and the input data.

The pair-rule folds recover a token through the context's frames, the
set the ranking reads; in production the two sources are pushed
together, so this changes no result. Inside a run, a converter's error
and a refusal arriving together are order dependent, which the docs
name as a known limit and a cell pins in both orders, inside and
outside a run. A fold that finds no basis asserts in a debug build and
floors to a neutral cause in a release build, with a cell for each.

The compared-nothing arm no longer has a net for a refused left read.
The parent commit, "Fail closed on an undecidable basis and name the
cause it rests on", had a loop here that, for a refused left marker,
set the left-marker flag, filed the undecided reason, marked the read
and noted the refusal for the collection. A refused left marker never
reaches this arm, because the per-pair arm files a status for it, and
a hand copy of that arm's state changes would leave states the code
after the match does not assume: the early error returns run before the
decision that settles a marked read, and the strict arm answers an
empty query result whose basis must be decided. The loop is replaced by
a debug assertion that names a defect in that status filing if a
refused left marker ever reaches the arm. No known input reaches it,
and nothing runs in a release build.

The condition arm is stated once in the comments and the census: it
matches both conditions, and a message-less failed rule over a failed
condition is a run-ending error, reported once before the exit; an
ordinary run reaches it through a type block's condition. The type
check carries a message only for a condition error that is not an
unevaluatable one. The filtered records are built directly, not by
cloning a subtree and replacing its children, and the doc names the
known limit: a plain raise whose only record is a per-value comparison
with a clear refusal flag reads as decided. The filter's header says
what it does with the three block kinds: kept for a surviving child, an
own message or a refusal flag, or no children, dropped otherwise. The
rationale for the gate's per-value record says a decided condition is a
RuleCondition FAIL under a RuleCheck SKIP that the walker skips, why
the record carries no sentence by choice, and the `when A or B` shape
where a sentence would still print. The raise order reads the same in
the README and the clause reference: the kept read first, then the
others as collected. The README's JUnit bullet is split from the
console one and states the case of a body over a silent block. Its
escape bullet is qualified: the change covers the failed-clause tree
and the comparison writers' lines, the `Reason =` lines and the
`Error =` line of a refusal's record already escaped a joiner, and the
`Error =` line of a plain record printed it unescaped. The README no
longer says "as before" of the condition sentence. It names the three
joiners, the head floor and which count it takes, the access clause's
entry in `-o json` and `-o yaml`, and the move of the refusal from the
checks into the rule's sentence in the json report. It says that a
refusal beneath a record the reporter does not reach is joined to the
nearest ancestor that reports, that the nested joiner appears on a type
block's or a `when`'s sentence only where the nested record's own entry
does not tell it, that a block and a disjunction are among the hosts of
the nested joiner, and that the access clause's entry is written
whenever none of its own value checks reports.

Rows cover the shapes the filter meets: a reference that failed on an
error and one that failed on a refusing error, a refused value, a
called rule over a decided answer and a silent block. The type-block
row builds its real parent, a type check with no message over the type
block over its condition, and holds a refused disjunct beside decided
ones: one is a block record over a unary check and one keeps a message
yet lists nothing, and a plain raise's clause-error record is kept with
no refusal. The raise rows assert the text and the flag. Unit rows
cover the added sentence and that it never repeats the held one, the
dedupe by identity in both orders, a nested disjunction's held refusal
listed once, each wrapper kind, the kind a joined sentence keeps,
the two spellings of one refusal, the head cut (a plain head cut, a
refusal head uncut, an unjoined head untouched), the JUnit text of a
call's body and a refused missing-block value with no message. Report
cells cover a type block whose `when` fails closed, read through the
not-compliant path with the decided disjunct never listed, a `when`
of two refusing disjuncts whose checks are empty, a 639-character
quoted value that is cut while the joined refusal stays whole with its
path in the console and in `-o json`, and the refusal's path in the
`-o sarif` document for both gate templates. The gate cell takes rows
over one shared runner, and its control plants one broken line per
condition: cut early, cut late, no remedy, no path, no second part. A
console cell asserts that a failing call's `<< >>` body over a silent
block still reaches the output. Further cells cover a refusal with a
semicolon in each position and through the dedupe, the last refusal
past the span bound, the head floor with a head of words under it,
a joiner five characters before the bound, the three joiners, a called
rule with a body over a silent block and over a type block with a mark,
through the evaluator, the record builders and the SARIF results, and
a source scan that requires each listed sentence head and tail to be
defined once and fails when the crate's own sources are absent unless
CFN_GUARD_SKIP_SOURCE_SCAN is set to 1; an empty value or 0 does not
skip the scan. The scan blanks comments with a character scanner that
tracks string, line-comment and block-comment state, so a comment
opened after code, a second block on one line, or a "/*" inside a
string literal is read as what it is. It searches a character literal's
closing quote past the escaped character, so `'\''` ends at its real
quote, and a unit check covers the four literal shapes and the text
after them. The cell that pins the context of a rule's untold-refusals
entry is named for the carrier of the entry's first refusal. The cutter
cell lists all ten of its assertion blocks and names the cutter each of
its rows refutes, and gains an input of two refusals where the first is
4,087 characters long. A cell puts a probe clause with a message under
a record of each failed kind and asserts that the probe's entry is
written exactly when the marks walk can stop there, with the condition
kinds, where the reporter reads a filtered copy, as the documented
exception. The agreement cell probes a sample of every record kind and
builds its kind list, its names and its match with no wildcard arm
from one set of rows, so a new kind does not compile until it has a
sample, and it uses the real ignored kinds, with its row for an absent
container back beside the rows the list derives from. It asserts the
reason for each documented exception: the walk
does not enter a reached failed rule, and the reporter reaches a
probe through a rule's and a type block's condition. The docs of the
two predicates say a called rule is left out of the first because the
marks walk enters one only where the reporter does not reach it. The
docs of the cells in the reporters and the writer now open with "Fails
when ..." and name what makes each cell fail, and the case names that
known issues cite are renamed to match the cells, for instance "GKr2"
and "GKr2r" to "GKr2w" and "GKr2rw". New cells pin the called-rule arm
directly for both flags, a messaged `when` beneath a filter under a
type block's sentence, a mark under an entered called rule's condition,
and a failed called rule inside a type block's condition. Two cells pin
the deferral test. In the first, a reached `when` with a sentence under
a type block with a sentence tells the refusal its condition met in its
own entry, and the type block's sentence stands alone with no nested
joiner, so the refusal is told once. In the second, a type block with a
sentence that the reporter never reaches, beneath a filter's predicate
records under a type block with a sentence, is deferred whatever its
message, and the refused comparison record under its condition is
joined once on the outer sentence behind the nested joiner; the cell
also reads the deferral test directly for a messaged failed type block
and `when`, deferred where the reporter does not reach the record and
not deferred where it does. A further cell pins the type block whose
sentence an entry beneath already carries, over an unreached `when`
with a refused comparison record: the refusal is told once, as the type
block's refusals-alone entry, and the sentence once, by the entry that
carries it. Cells that could pass whether or not their behavior held
now can fail, and repeated rows report every failure.
`cfn-guard test` prints a verdict per rule and no messages, and no
refused-read line is written for a rule that failed closed on a
refusal in its `when`. The rule's sentence, with the refusals joined
to it, was the reason only of an unmet expectation; a FAIL expectation
met on such a rule said nothing about the refusals. Now a rule
evaluated FAIL carries its reason on the met line as well: the plain
report prints `<rule>: Expected = FAIL, Reason = <sentence>`, the
`json` and `yaml` reports carry `reason` on the passed rule, and the
`junit` report writes it as the passed case's `<system-out>` text. A
passed rule with no reason serializes as before, byte for byte, and
the walk that finds the reasons runs once per case, on the first rule
evaluated FAIL or whose expectation is unmet.

A rule that failed closed on one refusal, with nothing joined to its
sentence, now carries that sentence as its reason too, in every output
and for a met and an unmet expectation alike; the plain report bounds
it as it bounds a refusal-headed line. An author's `<< >>` text and a
plain error with nothing joined are no reason.

Cells pin a met FAIL on a rule that failed closed on two refusals
(both named once in every output), an unmet PASS on a rule that failed
closed on one, a passed rule with no reason (its `json` text pinned),
and a plain failure under an author's text with no reason.

The README's `test` bullets and the unit-testing guide describe the
reason on the met line and in each report. A unit table in the console
reporter pins the two shapes of reason over each kind of custom-message
slot: a joined sentence yields its joined text whatever the slot, and
with nothing joined only a refusal's own sentence is a reason.
The sarif report prints the package version from Cargo.toml. The
aggregate_over_two_buckets golden carried 3.2.0 and the package is
3.2.1, so the byte-for-byte comparison in
an_aggregate_over_two_buckets_is_reported_at_the_shared_path failed on
its sarif case. Only the two version lines of the golden change.
@awsmadi
awsmadi force-pushed the evaluator-hardening branch from 47fcddf to 465d6a5 Compare October 26, 2026 22:42
The Compare Directories check in the Github Action CI workflow builds
action/dist with Node 20 and compares the result with the committed
files byte for byte. The committed index.js and index.js.map came
from another install of the same package lock and put the bundled
modules in a different order, so the check failed.

These two files are the output of a build from the same sources and
the same package lock, and index.js matches the check's own build
byte for byte. guard_bg.wasm, licenses.txt and sourcemap-register.js
were already identical and are unchanged.
On Windows the unparsed-rules-file-name spec failed. It expected the
resolved rules path with backslashes, such as D:\a\...\missing.guard,
and received the same path with forward slashes, D:/a/.../missing.guard.

validate turns a Windows path separator into `/` when it puts a file
name into the report, as it already does for data file names, because
the name sits inside a JSON string. The spec now builds the expected
name with the same normalization, so it compares one separator on
every platform. It still checks that the file name appears and that
RULES_STDIN does not. Linux paths have no backslash, so nothing
changes there.
@awsmadi
awsmadi marked this pull request as ready for review October 26, 2026 22:57
The spell check flags six words in test names, case labels and doc
comments: entrys, querys, propertys, leafs, childs and anothers. Each is
a possessive flattened into an identifier. Rename them to the words typos
expects (entries, queries, properties, leaves, children, others) and
update every reference, including the test docs/CLAUSES.md cites by
name, so the citation check still resolves each one.
Condition 3 of check-registry-corpus.sh failed on any rule that ran and
did not meet its expectation. The pinned registry carries a test input
this evaluator answers differently: the "No Method set in API GW Stage"
case puts Type and Properties directly under Resources, so the rule's
predicate walks a string and a map that are not resources. The evaluator
refuses that read, and a rule-level when meeting a refusal fails the
rule rather than skipping it, so both API Gateway rules evaluate FAIL
against an expected SKIP. That is a known state of the pin, not a
regression.

Compare the failed-rule list against
.github/registry-corpus-state/failed-rules.txt with cmp and diff -u, the
way conditions 2 and 5 compare their lists, so a rule that newly fails
appears and one that stops failing disappears, and either turns the job
red. The list holds the two API Gateway lines. Update the script header,
the summary line and the pin comments in pr.yml to say so.
…d reading

Thirteen tests in guard/src/rules/eval_tests.rs and guard/tests/validate.rs
failed at assert_all_agreed or on a console count. Every one went red in the
commit that judges a FAIL by its basis; the later basis and record commits
moved none of them. docs/CLAUSES.md is the spec for each cell: where it
describes the test's expectation the evaluator is changed, where it describes
what the evaluator does the cell is updated, with the text it now matches
named below.

Evaluator changes:

- A key filter's function argument that met a refusal inside a predicate
  (`to_upper(Tags[ Key == 'Env' ].Value)` over a string `Tags`) now wraps the
  refusal as the query arm wraps a bare right-hand query, so the map's marker
  carries the key filter's wrap and nothing confirms the scalar filter's
  pending notice (CLAUSES.md, the filter over a scalar inside a predicate:
  never a notice for a scalar met inside a key filter's right-hand query or a
  function argument). Fixes the notice cells of three tests and the marker
  reason cells of two.
- A gate raised on a resolved pair's own undecided reason beside a refused read
  leads with that reason and names the refusal after it, `(1) <engine> (2)
  <refusal>` (CLAUSES.md, the raise beside a refused read and other markers,
  pinned by an_engine_led_gate_beside_a_refusal_names_its_reason_first_and_the_refusal_after_it).
- A marker whose reason is a numbered text of the refused reads themselves is
  no marker reason of its own, so a raise no longer numbers a numbering inside
  itself, `(4) (1) .. (2) ..`.
- A refused-read line for a raise beside a refusal and a spent regex budget
  names the registered refusal alone; the budget's reason has no line owed
  (CLAUSES.md, the refused-read line).
- A refusal a value's body raised as a plain error inside an every-entry
  block (a `when` condition re-raising the registered refusal a `let` holds)
  is the value's answer the siblings may decide past, written under the
  condition's own clause when a sibling's decided failure settles the block
  (CLAUSES.md, a failure that rests on a decided no still decides a sibling's
  raise). Where nothing decides, the block hands that error up as before, so
  the undecided members its query read keep their holds.
- A scalar literal against a right-hand query that names several scalar
  values without enumerating a collection (a filter capture holding keys) is
  one membership question over the set, as the document-left twin is
  answered, not one haystack per value.
- The single-line summary lists nothing beneath a passing disjunction, so a
  refusal raised under an `or` that another disjunct decided is printed once,
  by the leaf record that carries it as the rule's failure.

Cell updates, each naming the text it now matches:

- the_hand_up_reading_of_an_undecidable_value_inside_a_unary_filter_is_pinned:
  the marker's reason is the key filter's wrap (CLAUSES.md: "the key filter's
  wrap of it carries the refusal's token on the marker"); the needle reads the
  wrap's shared opening.
- a_wrap_of_a_numbered_marker_registers_nothing_and_still_travels_as_a_refusal
  and a_sentence_registered_again_inside_a_run_is_the_runs_own_refusal, cell
  (2): a record's context renders a filter by its capture or as `<map>
  (map-key-filter-clauses)` and drops the written predicate, so the needles
  read the rendering; asserted, the flagged record of a unary clause over a
  marker is the per-value one (CLAUSES.md: "the pair's record flagged
  `refusal: true` and the clause's own block record not").
- a_sentence_registered_again_inside_a_run_is_the_runs_own_refusal, cell (2b):
  a `some` over a filter predicate's selection passes on a kept member, which
  witnesses the existential, and reports the refused entry on the clause's
  refused-read line; the cell now asserts the line beside the entry's REFUSED
  mark, and the two CLAUSES.md sentences that said the `some` fails closed are
  reworded to this reading (the fail-closed reading stays for the every-entry
  and comparison shapes, where the undecided member could move the verdict).
- an_asserted_refusal_raise_prints_the_refusal_whole_and_the_run_continues:
  the raise's sentence is printed where the block record is the clause's one
  line and not where a pair record speaks for the clause; the case that found
  it before was reading the origin clause's raise under a passing `or`, the
  line the once-only test counts.
- a_retrievals_reason_escapes_a_bidi_control_in_a_key_and_keeps_its_backslash:
  the fixture is read by the YAML loader, which decodes `\n` and `\x1b`; the
  rules-language value parser kept them as the characters written, so the row
  never held a control.
- frame_entries_decide_by_kind_and_a_rule_call_runs_in_its_own_frame, row H1:
  the quiet rules file now holds over its document (`Cfg2.Mode` has no
  `Kind`; that row failed on a decided absence, not a refusal).
…as the wildcard spelling does

The slot the reader inserts after a `%variable` root took its own path for a
filter: `%v[ f ]` over a map walked the map's entries whatever the map held,
while the author's `%v[*][ f ]` tested a map with a non-null scalar entry as
one object. A published rule written as `let m = Resources.Metadata` then
`%m[ "AWS::CloudFormation::Authentication" exists ]`, over a resource whose
`Type` string sits beside the authentication block, therefore selected nothing
and reported SKIP at exit 0 on a template that defines a password, where the
wildcard spelling reported FAIL. A gate reading the exit code saw a clean run.

The slot now takes the one-object reading for exactly that shape: a map with
a non-null scalar entry whose keys the predicate does not name as member keys
is handed on whole and the predicate is tested on it, as under `[*]`. A
collection, a mixed map (a scalar beside a member that holds a predicate key),
a map whose own key the predicate names with no scalar entry, and the
all-scalar own-key refusal keep the walk, its DEPRECATION notice and its quiet
records, so the population idiom (`let r = Resources` then `%r[ ... ]`) and the
instance-binding wrapper selection are unchanged. A one-object map the filter
kept records its quiet verdict at the site, so a filter over such a map is
re-walked once per site rather than on every evaluation.

The registry's cfn_authentication_rule tests read PASS on the insensitive
template and FAIL on the sensitive one, 8 of 8 cases met, the verdicts
cfn-guard 3.2.1 gives them; the corpus check's known state is the two API
Gateway lines alone.

Tests that pinned the walk over a one-object map now pin the wildcard's
verdicts; the notice and once-per-site tests re-base their fixtures onto maps
with no scalar entry, where the walk and the notice still fire; fixtures no
test names any more are removed. docs/KNOWN_ISSUES.md item 11, the
"Filtering through a variable" sections of docs/CLAUSES.md and
docs/QUERY_AND_FILTERING.md describe the reading by shape, and no doc or
fixture states that a release walked a single map a variable holds.
…tokens where they were minted, and tidy the raise records

Four corrections to the previous commits, each a defect found by reading
them against their own scenarios, plus the comments and docs that stated
what the code no longer did.

`IN` and `NOT IN` against a filter capture were decided by the number of
keys the capture held: a literal on the left read one key as a haystack and
two keys as a set, so `'a' NOT IN %x` failed over the one key `Fallback`
(the `a` inside it) and passed once a second key was bound, and adding an
entry to a deny set turned a FAIL into a PASS; a document value on the left
searched the keys for a substring whatever their number. A capture now
reports its keys as one entry set, the shape `query_projects` already read
off a bare capture, so either left operand is compared against the keys as
against a written-out list, membership for one key as for two, and the
count-based arm is gone. `Prefix IN %buckets` therefore holds when the
prefix is one of the bucket keys and no longer when it is a substring of
one; the tests and the fixture that pinned the substring reading now pin
membership with a key as the positive control. A plain `let` or key path
that names one string is still a haystack.

The function-argument arm of the key filter wrapped a text-only
undecidable comparison with no token: a numbered text tripped the debug
assertion that every numbered text of registered parts carries one, and a
single registered sentence minted a token at the wrap, marking the enclosing
run's entry REFUSED for a refusal registered before the run. The token is
recovered from the registry as the query arm recovers it.

A raise's recorded reason was replaced by a resolved pair's own reason with
no refused read beside it, so with no refusal met the raise named the
engine's reason where it names the first reason recorded alone. The
replacement now needs a refused read.

A block that folded a plain registered refusal beside a tokened one built
its record from the folded error and returned the plain one, so the block's
record and the rule's condition could name different raises. The record is
built from the error the block returns.

The registered-parts reading of a numbered text (`(1) <refusal> (2)
<budget>`) moves into `refusal_reads_of`, so the refused-read line and the
fail-closed `not` record name the same reads for one raise; the console
summary's passing-disjunction arm gets a test of the plain case, where a
failed rule whose `or` a later disjunct decided lists nothing beneath that
disjunction in the console or the structured report; the corpus check's
notes describe its three evidence sites and the failed-rule list that stays,
and its failed-rule comparison no longer turns on the data lines'
indentation. Test and case names that the spell check had pluralized from a
possessive (`entrys own`, `childs`, `querys`) are rephrased without the
possessive, and docs/CLAUSES.md says what the console tests pin for each
strict shape.
…ring matches on Windows

The published-address check joined `docs/KNOWN_ISSUES.md` to the repository
directory as one string, so on Windows the error for a missing file named
`...\docs/KNOWN_ISSUES.md` with mixed separators, while the test that
provokes that error builds the same path with two `join`s and looked for
`...\docs\KNOWN_ISSUES.md`. The windows-latest unit test job failed on that
one assertion with every other test green. The check now joins the address's
path components one by one, so both render alike on every platform.
…ication in every map case

The previous commit gave the slot the reader inserts after a `%variable`
root the one-object reading for one shape only, a map with a non-null scalar
entry, and left three shapes on the walk the wildcard spelling never took.
Each of the three parted from what cfn-guard 3.2.1 answers for `%v[ f ]`
over one map, run on the released binary:

A map with no scalar entry whose own key the predicate names was walked,
selected nothing, reported SKIP at exit 0 and wrote the bound-map notice.
The published registry rule's shape without its `Type` scalar, and the
common `Resources.*[ Metadata exists ].Metadata` projection filtered for
`"AWS::CloudFormation::Authentication" exists`, therefore passed a template
that defines a password, where 3.2.1 applied the rule and reported FAIL. The
slot now reads such a map as one object: the predicate is tested on the map,
the map is selected when it holds, a `when` gate on the selection opens and
a key read below it is decided on the map.

A mixed map, a scalar entry beside a member that holds a key the predicate
names, was walked and announced where the literal `K[ f ]` is refused:
`%c[ Owner !exists ]` over `Cfg: { Mode: fast, A: { Owner }, B: { Owner } }`
passed `empty` and skipped a `when ... !empty` gate. The slot now takes the
literal's reading through `filter_over_a_named_map`: refused in both
polarities where the walk passed a scalar over under a predicate that can
hold by absence and holds on the map read as one object (3.2.1 passed that
check on the map as one value, a pass its members fail), and selecting the
member the literal selects otherwise, so a positive predicate carried by a
member still selects that member.

A map whose entries are all scalars under a predicate naming its own key was
refused. `%m[ Status == 'Enabled' ]` over `{ Status: Enabled }` now selects
the map, as `[*]` and 3.2.1 do; the literal keeps its own-key refusal.

A collection, a map with no scalar entry whose own key the predicate does
not name, keeps the walk, the DEPRECATION notice and the quiet records, so
the population idiom is unchanged. The notice classifies the map first and
returns before its probe for every shape the slot hands on or refuses, so a
one-object map pays neither the probe nor the re-walk. The slot marks the
enumeration for a one-object hand-on as the walk always did, so `x IN
%v[ f ].Key` compares against the entries selected, as `x IN %v[*][ f ].Key`
does, and never as a substring of one value. A capture on the slot binds the
one object, as `%v[*][ id | f ]` binds it.

The notice-plumbing tests keep their purpose on collections that still lose
a selection; the tests that pinned the walk, the notice or the refusal over
the three shapes pin the new readings, each beside the `[*]` twin where one
exists. docs/CLAUSES.md states the four map cases and how each differs from
`[*]`, copies the full one-object refusal condition, corrects the
launch-template example and the mixed-map sentences, and names a release
verdict only for a shape run on the 3.2.1 binary; docs/KNOWN_ISSUES.md item
11, its per-operator table and its pinned-cells rows, the fixture README and
docs/QUERY_AND_FILTERING.md say the same. The refused key-spelling control
rule moves into its own rules file so the subject fixtures exit 0, and the
fixtures whose verdict was the same under both readings gain a clause only
the selected resource satisfies.
…rator

`two_rules_files_sharing_a_basename_get_distinct_locators` looked for
`first/incomparable_membership.guard` in each notice, so on Windows, where the
locator renders the rules file as `...\first\incomparable_membership.guard`,
neither notice matched and the test failed although both locators were
distinct and named their directory. The expected tail is now built with
`Path::join`, as the known-issues address check builds its path, so the test
reads the same rendering the locator writes on every platform.
…he literal refuses it

A filter through a variable over a mixed map (a scalar entry beside a member that holds a key the
predicate names) took the literal `K[ f ]` reading: the entries were walked, the scalar passed over, and
the member the predicate held on was selected in the map's place, with no notice. Where the literal
selected a sub-map, the verdict moved from cfn-guard 3.2.1's with nothing said: `%p[ Name exists ].Name
== 'b'` over `{ Name: a, Inner: { Name: b } }` compared `Inner`'s `b` and passed, where 3.2.1 tested the
bound map as one value, compared `a` and failed (observed on its binary); `%m[ Metadata."AWS::
CloudFormation::Authentication" exists ]` over a resource whose `Wrapper` entry carries the path selected
`Wrapper` and failed the password check, where 3.2.1 selected nothing.

The slot after a variable now reads a mixed map as one object, the reading 3.2.1 gave every filter
through a variable over one map, except where the literal refuses the filter over it: there the variable
is refused the same way, since the release's pass over such a map (`%c[ Owner !exists ] !empty` over
`{ Mode: fast, A: { Owner: o }, B: { Owner: p } }`, a check its members fail) is a silent pass the literal
refuses. The literal's reading is asked quietly, through a scope that records nothing; where it refuses,
the literal is run again for real so the refusal is raised and recorded as `K[ f ]` raises it. The slot's
routing of a map (one object, refused-or-one-object, or read as `[*]` reads it) is one function the walk
and the bound-map notice share, so the notice no longer re-derives it by hand.

Also:

- `IN` over a query rooted at a filter capture is pinned: `%lit IN %t.Value` with `let lit = 'prod'` after
  `Tags[ t | Key == 'Env' ]` is membership in the values, as the inline `Tags[ Key == 'Env' ].Value` reads
  (3.2.1 binds no capture on a filter over a list; a bare literal on the left of a query is a key path, as
  everywhere); `keys IN %t` over a capture of maps selects no key, over a capture of
  keys it is membership in them (as 3.2.1 reads it); `IN %v[ f ].Type` over a one-object `%v` is
  membership in the one value under both spellings (as 3.2.1 reads it). The `IN` table and the README
  record the bare-capture move: `Prefix IN %buckets` is membership in the keys, `'a' NOT IN %x` over one
  key holds.
- check-registry-corpus.sh unindents both sides of the failed-rules comparison.
- The doc comments that said a capture "names its value" say it is one entry set.
- The quiet mixed-map cells assert the refusal sentence or the absence of one, so a refusal and the
  one-object FAIL are told apart; the reach-outside probe rows run over a collection-shaped map, where the
  probe would run but for the reach; the launch-template control's refusal, the order dependence of the
  notice at a site holding a refused and an announced map, the shared-key exception of the own-key rule
  and the `when`-body limit of that rule are stated where the rule is written.
…the predicate's key beside a member that holds it

A filter through a variable over a map with no scalar entry that holds the predicate's key itself while a
map-valued entry holds the same key was walked as a collection: the own-key test exempted a key a member
holds, so `%md[ "AWS::CloudFormation::Authentication" exists ]` over a `Metadata` holding the authentication
block beside a `Wrapper` entry with a block of its own selected `Wrapper` and a password check below the
selection read `Wrapper`'s block (PASS), where cfn-guard 3.2.1 tested the bound map as one value and read
the map's own block (FAIL, observed on its binary); the negated predicate selected the block the map holds
where 3.2.1 selected nothing. The slot after a variable now reads such a map as one object, the reading 3.2.1
gave every filter through a variable over one map, in both polarities. The literal `K[ f ]` and the author's
`[*]` walk that map and select `Wrapper`, as before, and neither refuses it, which is why the variable is
one object there and not refused. The same rule makes a Deployment `spec` carrying a `securityContext`
sub-map one object whether or not its `template` holds one too, so no selection is gained through the
variable; the author's `%s[*][ ... ]` still walks it and is announced.

Two more maps with no scalar entry take the same reading through a variable. A map whose own key the
predicate names where that key is also the name of a member among members sharing a key (`let r =
Resources` then `%r[ BucketA exists ]` over `{ BucketA: { Type }, TableC: { Type } }`) selects the map, as
3.2.1 did (observed on its binary: `!empty` PASS); it was walked, selected none of the members and announced
a lost selection, so `!empty` failed. The literal `Resources[ BucketA exists ]` is refused for naming the
map's own key and `[*]` enumerates the members; the asymmetry is recorded in the known-issues item. A map
holding one member map that lacks the key an absence predicate names (`%md[
"AWS::CloudFormation::Authentication" !exists ]` over a `Metadata` holding only an init block) selects the
map, as 3.2.1 did (observed on its binary: a key read below the selection reads the map, `.config exists`
FAIL); it was walked, selected the init block in the map's place and said nothing, the one entry read as a
population of one. Such a population cannot be told from the map by the key the predicate reads: the walk
keeps the one member for want of the key where the one-object reading holds on the map for the same want,
so the two agree on the entry and differ only in what is selected. A map of two or more members keeps the
walk and the notice the walk writes.

The check that decides whether a mixed map is refused through a variable asks the literal of the filter
alone, not of the query after it: `%p[ Name exists ].Spec[ Kind == 'x' ] !empty` over `{ Name: a, Spec: {
A: { Kind: x } }, Inner: { Name: b, Spec: s } }` ran the rest of the query under the member the walk kept,
met the filter over `Inner`'s string `Spec`, read that error as the literal's refusal and refused the map,
where 3.2.1 read the map's own `Spec` and passed (observed on its binary). Only the filter's own two
refusals count now, told apart from every other error by construction rather than by `is_err()`; a real run
that refuses nothing after the quiet run refused falls back to the one-object reading. The quiet run reads
under the measurement every probe reads under, so a `let` only the discarded walk reaches leaves no memo
behind, and a counter makes its cost visible beside the notice's probes. Where the literal keeps one or
more of the map's entries while the variable reads the map as one object, one DEPRECATION line per filter
site says the two spellings disagree and names the path spelling as the way to select among the entries.

Also:

- The per-operator table of the filter-through-a-variable item is written from one shape run on the 3.2.1
  binary and on this engine, names that shape in every row, and carries the mixed-map rows (one object
  where the literal does not refuse; refused where it does) and the own-key-beside-a-member row; the
  prose that still described the variable announcing where the literal refuses, an all-scalar map refused
  through the variable, or a selection gained through the variable is rewritten to the shipped reading,
  with one vocabulary for the slot's classification (one object, walked) and one for the notice's test
  (population, fields).
- The `IN` table rows and the README bullet for `IN` against a capture name the rule and template each
  3.2.1 verdict was read with.
- Fixtures that could not tell a right reading from a wrong one now can: the metadata-projection rules read
  a key below the selection that the selected sub-maps lack and the map holds; the test-command cases
  carry no second map, so their expectation is SKIP; the `Type` check on a selection is its own rule
  expected to PASS; a filter in a parameterized rule's body has a fixture of its own.
- Two tests pin what had none: a gate raised beside no refused read names the first reason recorded, not
  the engine's; a key filter whose function argument holds a numbered marker raises over the wrap with
  the marker's token and text and registers no text of its own. The registry fallback for a token-less
  registered text in the key filter's right-hand arms is documented as unreached by any shape found.
- Probe-count tests that read a parsed policy or a literal of one sub-map through a variable now read
  two sub-maps sharing a key, since a map of one member map that lacks the absence key is one object
  and is never probed; their counts are the walked collection's.
- The test helper that reads a refusal off the record matches the pass-over refusal by a fragment the
  `[*]` one-object refusal does not share.
…stead of deprecating on a mixed map

The three one-object readings the slot after a variable gives a map with no scalar entry (a key the map
holds beside a member that holds it too, a member named like the predicate's key, and a map holding one
member map that lacks the key an absence predicate names) no longer apply to a binding of a top-level
section, the population alias: `let r = Resources`, `let p = Parameters`, `let m = Mappings`, `let c =
Conditions`, `Outputs`, `Metadata`, `Rules`, or a chain of such aliases. A `let` of another top-level key
(a Kubernetes `spec`) is not the alias and keeps the one-object readings, as the released evaluator read
it. Under the alias the walk stays, the documented reading of this spelling. `%r[ DeletionPolicy !exists ].Properties.PubliclyAccessible !exists` over a template of one
`AWS::RDS::DBInstance` selects the instance and fails, as it does over two resources; before this the
one-member reading selected the whole `Resources` map, `Properties` was not found below it and the check
passed with nothing said, so the verdict depended on how many resources the template held. `%p[ Type ==
'AWS::EC2::KeyPair::KeyName' ].AllowedPattern exists` over a `Parameters` section holding a parameter named
`Type` selects the `KeyName` parameter and fails on its missing pattern, where the own-key-a-member-holds
reading compared the section's `Type` entry, a map, with the string and reported SKIP. A predicate naming
a logical id (`%r[ Bucket.Type == 'X' or Properties.Tags empty ]`) selects nothing over compliant tagged
resources again, where the member-named-like-the-key reading selected the whole section and failed the
gated body on its missing `Type`. cfn-guard 3.2.1 read the section as one value in each of these shapes
(run on its binary: the first check PASS over one resource as over two, the second SKIP, the third's gate
opened on `Resources` and its body FAIL); the walk under the alias is the behavior this evaluator documents
for the spelling, and over a uniform population it is quiet by the bound-map notice's listed limit. Below
the top-level sections the three readings stand (`let md = Resources.Box.Metadata`, `let r =
Stack.Resources`). The alias question is asked of the resolver only where it decides the routing, through
a closure both the slot arm and the bound-map notice pass.

The mixed-map line moves from the `DEPRECATION:` channel to a `Warning:` line, the channel the refused-read
line uses. It reports a lasting difference between two spellings of one filter (the variable reads a mixed
map as one object where the filter on the map's own path keeps one of its entries), not a reading being
retired, so a run that treats deprecation lines as failures no longer trips on it. One line per filter site
as before, keyed by the site under its own warning kind; a second mixed map read at the same site is counted
into the line, and the test command folds the cases of one spec file into one line the same way. The
`DeprecationKind` variant is gone; `EvalContext::record_mixed_map_warning` records it.

Tests. The per-clause reset in `probe_predicate` is reached again: the tagged two-resource row of
`the_population_probe_reads_each_clause_on_its_own` reads SKIP through the alias and goes red when the
reset is removed. The three alias shapes are pinned with their 3.2.1 verdicts in the test docs
(`the_one_object_readings_do_not_apply_to_a_top_level_section_alias`), beside the same maps one level down,
which keep the one-object readings. The member-named-like-the-key test moves its binding below a section
(`let r = Stack.Resources`) and pins the alias as walked and announced. The mixed-map tests count the
`Warning:` lines and assert no `DEPRECATION:` line. The metadata-projection test-command cases hold the
role's one-member `Metadata` again, a one-object map inside a binding that holds several maps, and expect
PASS (the branch's verdict; 3.2.1 FAIL, both maps read whole); the fixture's comment describes the routing
as it is.

Docs: CLAUSES.md, KNOWN_ISSUES.md, QUERY_AND_FILTERING.md and the wildcard-filter README state that the
three readings hold below the top-level sections only, with the alias shapes and what 3.2.1 answered, and
name the mixed-map line as a warning.
The slot after a variable kept one one-object reading in place under a binding of a top-level section:
a map with no scalar entry whose own key the predicate names. The other no-scalar one-object readings
were exempted there, so one predicate read differently by resource count alone. Over `BucketA` beside
`TableC` the key `BucketA` is a member's name among members sharing a key and `let r = Resources` then
`%r[ BucketA exists ]` was walked, selecting no member; over `BucketA` alone the same key is the map's own
key and the same filter selected the whole section, so `!empty` passed where it failed over two. The
population probe read the same way: `%r[ Bucket.Type == 'X' or Properties.Tags empty ]` selected nothing
over two compliant tagged resources (the gate stayed closed, SKIP) and selected the whole section over one
(the gate opened and the body failed on the `Type` the section lacks). The alias walk was there to remove
exactly that dependence, and the docs and the comment at the routing claimed it without the caveat.

Under the section alias no no-scalar one-object reading applies now: the four shapes (the own key alone,
the own key beside a member that holds it, a member named like the key, one member map lacking an absence
predicate's key) take the plain walk every collection takes under the reader's slot, not the author's
`[*]` own-key enumeration, which refuses a map that kept no entry through a resolving clause and is written
for a map the author chose to enumerate. So `%r[ BucketA exists ]` selects none of the members over one
member as over two (`!empty` FAIL, `empty` PASS), announced once per site by the bound-map notice in both,
since the predicate holds on the map by the key it names; the one tagged resource under the population
probe reads SKIP as two do. cfn-guard 3.2.1 read the section as one value in each (observed on its binary:
`%r[ BucketA exists ] !empty` PASS and `empty` FAIL over one member as over two, with or without a `Type`
on the member; the probe's gate opened on `Resources` and its body failed over one resource as over two).
Below the top-level sections the own-key reading stands (`let r = Stack.Resources` over the one member
`BucketA` selects the map, as 3.2.1 did), as do the other three. The alias question the slot arm and the
bound-map notice each asked in their own words is one function both call.

Tests: `the_one_object_readings_do_not_apply_to_a_top_level_section_alias` gains the own-key rows (the
one member named like the key, with and without a `Type`, under `!empty`, `empty` and a key read below the
selection; the same map one level down as the one-object control; the author's `[*]` refusal as the
control for the enumeration the walk is not) and counts the bound-map notices per row. The one-resource
rows of `the_population_probe_reads_each_clause_on_its_own` are re-pinned (the tagged one SKIP) and its
doc says why the two rows agree; both tagged rows go red when the per-clause reset is removed.

Docs: CLAUSES.md, KNOWN_ISSUES.md, QUERY_AND_FILTERING.md and the wildcard-filter README say every
no-scalar one-object reading holds below the top-level sections only, with the one-member alias rows and
what 3.2.1 answered; the known-issues sentence that still read the two-member alias as one object says
walked, and the sentence on the two-map binding names which map is announced and which is quiet.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant