Skip to content

fix(check): report skipped inputs, add --strict gate profile - #1305

Merged
dekobon merged 9 commits into
mainfrom
fix/1055-check-gate-visibility
Aug 9, 2026
Merged

fix(check): report skipped inputs, add --strict gate profile#1305
dekobon merged 9 commits into
mainfrom
fix/1055-check-gate-visibility

Conversation

@dekobon

@dekobon dekobon commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1055 (already closed on the issue; this PR carries the change). Two attacker-controlled inputs silently removed files from the bca check gate: a generated-code marker (@generated, DO NOT EDIT, GENERATED CODE) in a file's head, and a .gitignore committed in the branch under test. Per the decision recorded on the issue — loud, not strict, plus a --strict profile:

  • Default stderr summary whenever the gate declined to look at something:
    bca: 2 files not checked (1 generated, 1 ignored) — pass --report-skipped to list them
    Emitted before the empty-input guard so the sole-ignored-input case names its cause above the exit-1 error. Clean runs stay silent; no exit-code changes. Spelled in the severity-free bca: family (not bca: note:, which would reintroduce the style(cli): unify diagnostic prefixes across the binary #609 double prefix).
  • --strict + presence-only [check] strict: the untrusted-input gate profile, equivalent to --no-skip-generated --no-ignore, applied after the manifest merge and recorded in --print-effective-config. The old exit_code(strict) parameter is renamed tiered to de-collide.
  • Docs: a "The gate's trust boundary" section in the CI recipe (both reproduction tables, the buried-marker form, the one-flag recommendation), --strict on the check page, extended generated/.gitignore sections in the commands index — with Japanese translations in po/ja.po — plus CHANGELOG and regenerated man pages.

How the measurement works (and why it changed twice)

The ignore crate's walker never yields ignored entries, so the ignored set must be derived. The first design (second no-ignore resolve, set diff) failed on real trees — on this repo it printed "2,489,366 files not checked" and paid a ~3.2M-entry traversal per gate run. The shipped design measures at the walk's prune points: the measuring walk records every directory it enters, and one read_dir per walked directory classifies the immediate children the walk did not keep (hidden entries, symlinks, exclude globs, and unrecognized extensions explain their own drops; what remains is the ignore rules' doing).

A follow-up multi-angle review then hardened the counts:

  • Only parser-owned files count on both sides (Cargo.lock matches @generated on every Rust repo and was inflating the generated tally).
  • Ignored directories are reported only under --report-skipped — ignored build trees exist in essentially every checkout, and a default that counted them would be permanent noise.
  • A file named explicitly on the command line bypasses ignore rules and is analyzed (Glob pattern matching inconsistency: cve-corpus/** vs ./cve-corpus/** #726), so it is excluded from the measurement by canonical path; overlapping seed spellings dedupe in the ./-stripped space.
  • Only the gate's measuring walk records directories (a WalkFilters.measure_ignored gate), so no other command pays for the channel; Windows honors the hidden file attribute, matching the walker.

This repo's own gate now prints bca: 34 files not checked (34 generated) — … (34 analyzable test fixtures carrying markers) at the walk's ordinary runtime.

Testing

  • 24 CLI integration tests in tests/check/check_skipped_inputs.rs: one per row of the issue's reproduction tables (marker on line 1, buried in a license header, DO NOT EDIT / GENERATED CODE, committed .gitignore, sole-input exit-1 nuance, clean-run silence), the --strict / manifest / constituent-flag matrix, --paths-from file and stdin forms, effective-config, and the count-honesty cases above. Unit tests cover the summary formatter and the presence-only manifest fold.
  • The pruned-directory fixture was hardened by mutation testing: a measurement that counts every directory child fails it.
  • Patch coverage 96.4% of added code lines vs 85.7% project line coverage (cargo llvm-cov).
  • make pre-commit green (BCA_GATE: pass), including the dogfooded self-scan at both tiers.

Known follow-ups (pre-existing or deferred, called out in review)

  • apply_check_exclude re-reads --paths-from, so a stdin (-) list combined with [check.exclude] re-anchoring reads an exhausted stream (pre-existing, fix(cli): bca diff --since subtree mis-pairs; [check.exclude] ignores --paths-from seeds #497 plumbing).
  • The measurement's read_dir pass is serial; and ignore 0.4.33's IncrementalIgnore/build_matchers API could replace subtraction-based classification with positive attribution (which would also attribute which ignore source dropped an entry). Worth a follow-up issue.

dekobon added 9 commits August 8, 2026 21:48
Two inputs the branch under test controls could silently remove
files from the `bca check` gate: a generated-code marker
(`@generated`, `DO NOT EDIT`, `GENERATED CODE`) anywhere in the
scan window, and a `.gitignore` committed in the tree. Per the
decision on the issue, make the gate loud rather than strict:

- Tally generated-skipped files (new `Config.generated_skipped`
  counter) and derive the ignore-dropped set by resolving the seeds
  a second time with ignore handling off and diffing (the walker
  never yields ignored entries). The second resolve is
  metadata-only and check-only; `--paths-from` is materialized
  once first so a stdin (`-`) list is not read twice.
- Emit a one-line stderr summary whenever the gate skipped
  anything, before the empty-input guard so the sole-ignored-input
  case still names its cause above the exit-1 error:
  `bca: 2 files not checked (1 generated, 1 ignored) — pass
  --report-skipped to list them`. Clean runs stay silent; exit
  codes are unchanged. `--report-skipped` now also lists each
  ignore-dropped file. The summary uses the severity-free `bca:`
  family, not the issue's proposed `bca: note:` spelling, which
  would reintroduce the #609 double prefix.
- Add `--strict` and the presence-only `[check] strict` manifest
  key: the untrusted-input gate profile, equivalent to
  `--no-skip-generated --no-ignore`, recorded in
  `--print-effective-config`. Rename `CheckOutcome::exit_code`'s
  `strict` parameter to `tiered` to keep the terms apart.

Fixes #1055
Both #1055 bypasses reproduce from the book now: a new
"The gate's trust boundary" section in the CI recipe carries the
issue's reproduction tables, the buried-marker form, the default
skipped-input summary, and the one-flag `--strict` recommendation
for PR gates. The check page documents `--strict` and the
presence-only `[check] strict` key; the commands index extends the
generated-code and `.gitignore` flag sections with the summary.
Japanese translations for the new and touched strings are filled
in po/ja.po (plus the mechanical 2.1.0 version-string fuzzies
msgmerge surfaced).

Refs #1055
The #1055 additions tripped the dogfooded gate in three places, and
each had a real simplification rather than a rebaseline:

- dispatch.rs repeated the same optional-counter bump four times;
  a `bump_tally` helper replaces the `if let Some(counter)` blocks
  and returns `validate_and_resolve_file` under its cognitive and
  halstead limits.
- The skipped-input measurement and summary move to a
  `commands/check/skipped.rs` submodule, the same boundary the six
  existing check submodules draw, taking check.rs back under the
  file-level ploc limit.
- The strict-profile flip becomes `CheckArgs::apply_strict`,
  alongside the type's other resolution methods.

`run_check` keeps one genuinely new pipeline step and lands 3% over
its recorded halstead.effort, so the baseline is refreshed with
`make self-scan-write-baseline-headroom` in this same change, per
the baseline-refresh discipline.

Refs #1055
The bump_tally helper landed between the doc comment and its
function, silently reattaching the pre-dispatch-filters doc to the
helper. Found by the simplify-rust review pass.

Refs #1055
The independent review ran the #1055 ignored-file measurement against
a real tree and it failed badly: the no-ignore second resolve
enumerated this repo's `enums/target/` into a
"2,489,366 files not checked" summary, paid a multi-second traversal
on every gate run (including bca init and the --explain-threshold
preview), duplicated every seed-resolution warning on stderr, and
could exit 1 on a seed unlinked between the two resolves.

Replace the transitive diff with prune-point measurement inside the
one and only walk: the visitor now records every directory it
enters, and `measure_ignored_entries` read_dirs each one, classifying
the immediate children the walk did not keep. Hidden entries,
symlinks, exclude globs, and unrecognized extensions explain their
own drops; what remains is exactly the ignore rules' doing. An
ignored file with a recognized extension is counted; an ignored
directory becomes one "N ignored directories not walked" clause and
is never entered, so the cost tracks the kept tree (2.2k read_dirs
here, ~0 ms against the walk itself) rather than the no-ignore
universe (407k directories). Only the gate's
`resolve_walk_files_with_ignored` turns the measurement on; the
single resolve also retires the duplicated warnings and the
vanished-seed race outright.

This repo's own gate now prints "38 files not checked
(38 generated); 16 ignored directories not walked" in the walk's
ordinary runtime. New tests pin the pruned-directory clause (a
build/ tree with an offender inside must not inflate the file
count), the non-analyzable-ignored-file silence, and
exclude-owns-the-drop precedence.

Refs #1055
The prune-point measurement tripped the dogfooded gate: bundle the
walk's two membership sets behind `WalkedSets` and merge the
hidden/symlink guards, taking `classify_dropped_child` from six
arguments and six exits to five and four; drop the measurement
branches from `expand_seed_paths`'s seed loop (walked dirs are
retained unconditionally, the conditional folds into `bool::then`);
and give `unchecked_summary` a `counted` pluralization helper. The
remaining soft-tier entry — `classify_dropped_child` at the nargs
hard limit — lands in the headroom baseline refresh, which is the
band that baseline exists for.

Refs #1055
clippy's obfuscated_if_else rejected the bool::then fold, and the
restored if/else put expand_seed_paths one over its cognitive limit.
Extracting the explicit-file-seed arm behind its own named, documented
helper is the boundary a reader would draw anyway (#726's rules were
already a self-contained comment block).

Refs #1055
The audit's mutation run proved the pruned-directory fixture blind:
with only one subdirectory present, a measurement that counted every
directory child still reported 1 and passed. A walked src/ sibling
makes the exact count discriminate; the mutant now fails.

Also retire run_check_walk's --paths-from materialization, whose
comment described the removed second seed expansion — with a single
resolve, expand_seed_paths reads stdin exactly once itself (the
stdin integration test pins it), and the stdin test's stale
rationale is rewritten to match.

Refs #1055
The multi-angle code review ran the summary against real trees and
found the counts lying in both directions. Four verified fixes:

- A generated marker in a file no parser owns (Cargo.lock opens with
  `@generated`) inflated the generated count on every Rust repo; the
  tally now applies the same analyzable rule as the ignored side, via
  a shared `WalkFilters::analyzable`.
- Ignored *directories* fired on essentially every checkout (target/,
  node_modules/), contradicting "clean runs stay silent"; the pruned-
  directory clause and listing now appear only under
  `--report-skipped`, where the audit asked for them.
- An ignored file named explicitly on the command line was analyzed
  (#726) yet still reported as "not checked": explicit seeds are now
  excluded from the measurement by canonical path, and overlapping
  seed spellings dedupe in the `./`-stripped space so one entry is
  never counted twice.
- Every command paid for the walk's new directory channel; the sends
  are now gated on a `WalkFilters.measure_ignored` flag (which also
  retires expand_seed_paths' sixth parameter and its nargs
  suppression), so only the gate's measuring walk records
  directories. `run_check_walk` also reuses the sanctioned
  `run_walk_resolved` seam instead of re-spelling the exit-1 contract
  from crate-root internals.

Windows parity: the hidden-entry mirror now honors the hidden file
attribute, not just the dot prefix, so attribute-hidden files are not
misreported as ignore-dropped on Windows. `counted` moves to
format_util as the crate's one count-with-noun spelling.

Refs #1055
@dekobon
dekobon merged commit d65b304 into main Aug 9, 2026
45 checks passed
@dekobon
dekobon deleted the fix/1055-check-gate-visibility branch August 9, 2026 14:42
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.

fix(cli/check): @generated marker and committed .gitignore silently bypass the gate

1 participant