Skip to content

fix(ci): markdown that is compiled into Rust is not a docs-only change - #2081

Merged
justinchuby merged 3 commits into
mainfrom
holden/docs-only-embedded
Aug 25, 2026
Merged

justinchuby merged 3 commits into
mainfrom
holden/docs-only-embedded

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

Closes #2077.

Detect change scope classifies a PR as docs-only and skips both required checks — Fast (Linux x86_64) and Rust quality:

fast-linux:   { needs: changes, if: needs.changes.outputs.docs_only != 'true' }
rust-quality: { needs: changes, if: needs.changes.outputs.docs_only != 'true' }

Two markdown files under docs/ are compiled into Rust with include_str!, so a pure markdown edit can break cargo test while classifying as docs, skipping the checks that would catch it.

crates/onnx-genai-metadata/tests/capability_catalogue.rs:9
    include_str!(".../docs/genai/INFERENCE_METADATA_DECISIONS.md")
crates/onnx-runtime-ep-cuda/src/kernels/mod.rs:1686
    include_str!(".../docs/execution/CUDA_COVERAGE.md")

Falsified, not argued

Both edits below change only the .md — git diff --name-only returns one path, so docs_only=true. Baseline on 0ce253f4a is 3 passed; 0 failed.

Rename an HTML comment marker. The test locates its table with .expect(...):

panicked at crates/onnx-genai-metadata/tests/capability_catalogue.rs:12:10:
  capability catalogue start marker
test result: FAILED. 2 passed; 1 failed

Delete one catalogue table row:

assertion `left == right` failed: update the normative capability catalogue when the built-in vocabulary changes
  left:  {..., "input_presence", "linear_effects", ...}
  right: {..., "input_presence", "kv_cache", "linear_effects", ...}
test result: FAILED. 2 passed; 1 failed

The second is the exact edit that test exists to police. The test is skipped by the class of change it was written for.

The fix is derived, not a special case

The property is not "these two paths are special", it is a file whose bytes are compiled into an artifact is source, whatever its extension. So the set is scanned, not listed, and cannot go stale when a third embed is added (Rule 10):

if embed_hits="$(git grep -I -n -oE 'include_(str|bytes)!\s*\(\s*"[^"]+"' -- '*.rs' 2>/dev/null)"; then :
else [ $? -eq 1 ] || embed_scan_ok=false; fi

then resolved relative to each including file with realpath -m --relative-to. 143 embedded targets today, of which exactly two are docs-classified. A hardcoded two-path exception would be correct today and silently wrong on the third embed — the same shape as the .gitignore pattern list that #2036 replaced after four consecutive predictions failed.

Documented limitation: only literal-path embeds are visible. A computed path (concat!/env!) is not, and stays classified as docs.

The dangerous way to get this wrong, which I hit in my own patch

My first draft used a bare assignment. Under the default bash -e step shell, v="$(git grep ...)" exits the step when grep matches nothing — and a dead changes job skips fast-linux and rust-quality, and a skipped required check satisfies the ruleset. The fail-open form of this fix is worse than the bug it fixes.

bash -e -c 'v="$(grep zzz /dev/null)"; echo REACHED'    -> rc=1, REACHED never printed
bash -e -c 'if v="$(grep zzz /dev/null)"; then :; ...'  -> rc=0, REACHED

A git grep error (rc>1) forces docs_only=false, matching the classifier's existing fail-closed-on-ambiguity contract. I did not change that contract; the file is careful about it and says so.

Validation

The battery drives the classifier extracted from ci.yml via yaml.safe_load — never a text split, because #2052 shipped a battery that text-matched a job name and therefore could not distinguish a job from a key nested inside another job — against real commits, under bash -e to match GitHub's step shell.

== structural ==
  job list identical to main: True (9 jobs)
  fast-linux / rust-quality byte-identical to main: True
  'changes' step count unchanged: True (2 -> 2)
  only changed job: ['changes']

== behaviour (want | new | main-control) ==
  [OK] plain root doc          want=true  new=true  main=true
  [OK] plain nested doc        want=true  new=true  main=true
  [OK] EMBEDDED metadata doc   want=false new=false main=true    <-- changed
  [OK] EMBEDDED cuda doc       want=false new=false main=true    <-- changed
  [OK] EMBEDDED marker rename  want=false new=false main=true    <-- changed
  [OK] embedded + plain doc    want=false new=false main=true    <-- changed
  [OK] rust source             want=false new=false main=false
  [OK] doc + rust source       want=false new=false main=false
  [OK] LICENSE                 want=true  new=true  main=true
  9/9, 4 arms change verdict vs main  (0 would mean the fix is a no-op)

== shell arms ==
  unmutated control            rc=0 docs_only=true
  grep finds nothing (rc=1)    rc=0 docs_only=true    survives bash -e
  grep errors (bad regex)      rc=0 docs_only=false   fails closed

Every arm is paired with main's implementation as the control, so an arm cannot pass by the harness failing to exercise anything — the four <-- changed rows are the proof the battery has power.

Blast radius

This edits a required workflow. An invalid workflow does not fail its checks, it removes them — observed on #2052, where a four-space indent made one job a key inside another and the PR showed 14 green with both diff-guard checks simply absent, satisfying pending=0 && failed=[]. That is why the structural gate above parses the YAML and diffs the job list rather than reading the diff. changes is the only modified job; both required jobs are byte-identical to main.

Note this PR is not docs-only, so it exercises the full required set on itself.

justinchuby and others added 2 commits August 25, 2026 05:33
`Detect change scope` classifies a PR as docs-only and skips BOTH required
checks, `Fast (Linux x86_64)` and `Rust quality`. Two markdown files under
docs/ are compiled into Rust with `include_str!`, so a pure markdown edit
to either can break `cargo test` while classifying as docs -- skipping the
checks that would catch it.

  crates/onnx-genai-metadata/tests/capability_catalogue.rs:9
      include_str!(".../docs/genai/INFERENCE_METADATA_DECISIONS.md")
  crates/onnx-runtime-ep-cuda/src/kernels/mod.rs:1686
      include_str!(".../docs/execution/CUDA_COVERAGE.md")

Falsified on 0ce253f, both edits touching only the .md (baseline 3/0):

  rename an HTML comment marker -> panicked at capability_catalogue.rs:12:10:
    capability catalogue start marker              2 passed; 1 failed
  delete one catalogue table row -> assertion `left == right` failed:
    update the normative capability catalogue...   2 passed; 1 failed

The second is the exact edit that test exists to police, so the test is
skipped by the class of change it was written for.

The property is not "these two paths are special", it is that a file whose
bytes are compiled into an artifact is source whatever its extension, so
the set is derived by scanning rather than listed and cannot go stale when
a third embed is added (Rule 10). Scan finds 143 embedded targets today, of
which exactly two are docs-classified. Only literal-path embeds are visible;
a computed path (concat!/env!) is not, and is documented as staying docs.

The scan is assigned inside `if` rather than as a bare assignment. Under the
default `bash -e` step shell a bare `v="$(git grep ...)"` exits the step when
grep matches nothing, and a dead `changes` job SKIPS fast-linux and
rust-quality -- and a skipped required check satisfies the ruleset. The
fail-open form of this fix is worse than the bug. Demonstrated:

  bash -e -c 'v="$(grep zzz /dev/null)"; echo REACHED'   -> rc=1, never reached
  bash -e -c 'if v="$(grep zzz /dev/null)"; then :; ...' -> rc=0, reached

A `git grep` error (rc>1) forces docs_only=false, matching the classifier's
existing fail-closed-on-ambiguity contract.

Battery 9/9 plus 3 shell arms, driving the classifier extracted from ci.yml
via yaml.safe_load against real commits under `bash -e`. Four arms change
verdict against main's implementation, so the fix is not a no-op; the
unchanged arms (plain docs, LICENSE, rust source, mixed) confirm no
regression. Structural gate: job list identical to main, fast-linux and
rust-quality byte-identical, `changes` the only modified job.

Closes #2077

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Independent review found a fail-open in the guard I had just written, in
the direction that matters: a docs-only edit to an embedded markdown file
still skipped both required checks when the embed is written across two
lines.

`git grep` is line-oriented, so `include_(str|bytes)!\s*\(\s*"..."` only
matches when the macro, the paren and the opening quote share a line.
rustfmt wraps any call over 100 columns onto its own line, and 15 of this
repo's 163 embed sites are already in that form. The two motivating .md
embeds are 87 and 86 columns -- an unrelated reformat that pushed either
past 100 would have silently reopened #2077, while the comment directly
above the scan claimed it "cannot go stale". The guard's central claim was
false.

Isolated with a three-way control on one commit pair whose diff is exactly
[docs/genai/INFERENCE_METADATA_DECISIONS.md], the embed wrapped on the base
so it sits outside the diff:

  origin/main   (no embed logic)   docs_only=true    the #2077 bug
  79aa1ec     (line-based scan)  docs_only=true    the fail-open
  HEAD          (whole-file scan)  docs_only=false   fixed

The scan is now whole-file via `perl -0777` over `git ls-files -z`, so the
paren and the path may be on different lines. It also picks up raw-string
forms (r"", br"", r#""#), and the tab-separated output removes the previous
`file:line:match` parsing, which would have mis-split a path containing a
colon. 158 hits vs 148, 149 resolved targets vs 143, still exactly two
docs-classified -- so no docs PR starts running full CI unnecessarily.

Second finding, same class: `embedded="$(printf | while | sort)"` was a
bare assignment. The step declares `shell: bash`, which GitHub runs as
`bash --noprofile --norc -eo pipefail`, so pipefail is ON and a failing
iteration would have killed the step -- and a dead `changes` job SKIPS
fast-linux and rust-quality, which satisfies the ruleset. I had guarded the
first assignment against exactly this and left the second one open. Both
are now `if !` with `embed_scan_ok=false`.

Third: my battery ran the script under plain `bash -e`, not the real
`-eo pipefail`, so it could not have observed either finding. Fixed; every
new failure mode this patch introduces is a pipefail mode, so the harness
was blind to precisely the class of defect the patch could add.

Battery 10/10 under the real shell flags, 5 arms changing verdict against
main. Structural gate unchanged: 9 jobs, list identical, fast-linux and
rust-quality byte-identical, `changes` the only modified job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Owner Author

Independent Opus review returned REQUEST CHANGES and found a fail-open in the guard I had just written — in the direction that matters. Reproduced all three findings before fixing. e02deb57e.

MAJOR — a two-line embed was invisible, so #2077 stayed open

git grep is line-oriented. My regex required the macro, the ( and the opening quote to share a line. rustfmt wraps any call over 100 columns onto its own line, and this repo already has 15 of 163 embed sites in that form.

The two motivating .md embeds are 87 and 86 columns. An unrelated reformat pushing either past 100 would have silently reopened #2077 — while the comment directly above my scan claimed it "cannot go stale when a third embed is added". The guard's central claim was false.

Isolated with a three-way control on one commit pair whose diff is exactly [docs/genai/INFERENCE_METADATA_DECISIONS.md], with the embed wrapped on the base so it sits outside the diff:

origin/main   (no embed logic)   docs_only=true    <- the #2077 bug
79aa1ecae     (line-based scan)  docs_only=true    <- the fail-open
HEAD          (whole-file scan)  docs_only=false   <- fixed

The middle row is the finding. My original main control could not have caught this: main has no embed logic at all, so it says true for every embedded-doc arm and cannot distinguish "my fix works" from "my fix is invisible here". A control that differs from the subject in two ways measures neither — I hit the identical mistake on #2052 and wrote it down, then made it again.

Scan is now whole-file via perl -0777 over git ls-files -z. It also picks up raw-string forms (r"", br"", r#""#), and tab-separated output removes the file:line:match parsing that would have mis-split a path containing a colon (reviewer's NIT). 158 hits vs 148, 149 resolved vs 143, still exactly two docs-classified, so no docs PR starts running full CI unnecessarily.

MINOR — I guarded one assignment against pipefail and left the next one open

The step declares shell: bash, which GitHub runs as bash --noprofile --norc -eo pipefail {0} — pipefail is on. embedded="$(printf | while | sort)" was a bare assignment, so a failing iteration kills the step, and a dead changes job skips fast-linux and rust-quality, which satisfies the ruleset.

I had written a comment on the assignment immediately above explaining this exact hazard, then left the next assignment unguarded. Both are now if ! with embed_scan_ok=false.

Reviewer was straight that they could not make realpath -m exit non-zero on real input, so this is latent rather than live. Fixed anyway: the cost is one if, and the failure mode is the worst one available.

MINOR — my battery could not have observed either finding

It ran the script under plain bash -e, not -eo pipefail. Every new failure mode this patch introduces is a pipefail mode, so the harness was blind to exactly the class of defect the patch could add. Fixed, plus a new arm that puts the embed in rustfmt-wrapped form on the base.

== behaviour arms (want | new | main-control), bash --noprofile --norc -eo pipefail ==
  [OK] plain root doc            want=true  new=true  main=true
  [OK] plain nested doc          want=true  new=true  main=true
  [OK] EMBEDDED metadata doc     want=false new=false main=true    <-- changed
  [OK] EMBEDDED cuda doc         want=false new=false main=true    <-- changed
  [OK] EMBEDDED marker rename    want=false new=false main=true    <-- changed
  [OK] embedded + plain doc      want=false new=false main=true    <-- changed
  [OK] rust source               want=false new=false main=false
  [OK] doc + rust source         want=false new=false main=false
  [OK] LICENSE                   want=true  new=true  main=true
  [OK] EMBEDDED, rustfmt-wrapped want=false new=false main=true    <-- changed
  10/10, 5 arms change verdict

Structural gate unchanged: 9 jobs, list identical to main, fast-linux/rust-quality byte-identical, changes the only modified job, step ids [None, 'filter'].

Accepted as documented limitations

Raw strings now handled; computed paths (concat!/env!) still invisible and documented as staying docs; non-ASCII embedded paths would be C-quoted by git diff --name-only and miss — none exist, and it is the same core.quotePath boundary I documented rather than handled in #2036.

Thanks — the MAJOR is a genuine fail-open that my own controls were structurally incapable of detecting, which is the most useful kind to have found.

@justinchuby
justinchuby marked this pull request as ready for review August 25, 2026 06:24
… zero

The embed scan added in this PR closes #2077, but it had no control proving
it can fire. If the regex, the pathspec or perl ever stops matching, the
resolved set is empty, `is_docs_path` never blocks, and the guard is inert --
docs-only edits to embedded markdown would again skip both required checks,
behind a completely green run. A scan that found nothing and a scan that
cannot find anything were the same two lines of log.

So the step now echoes the number of resolved targets, and treats zero as a
broken instrument rather than as an answer: this repo has 149 such targets
and cannot legitimately reach zero while any `include_str!` remains.

Controls, 5/5, with the positive control run first so the zero is attributable:
  A embedded .md edited     -> docs_only=false, count=149, no alarm
  B non-embedded .md edited -> docs_only=true,  count=149, no alarm (not over-broad)
  D synthetic WITH include_str! -> count=1, no alarm  (proves the harness can
                                   produce a nonzero count)
  C synthetic with no .rs   -> count=0, alarm, fail closed
  E same tree under main's classifier -> docs_only=true (discriminator)

Original battery still 10/10, 5 arms changing verdict vs main; job list
identical to main and fast-linux/rust-quality byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Owner Author

Pushed c8269858f — a self-check on this PR's own guard, found by watching it run on itself.

The Detect change scope job on the previous head classified this PR correctly (docs_only=false, both required checks queued rather than skipped, no fail-closed message — so the perl scan and the realpath resolve both genuinely succeeded on the runner, and the shell really is -e -o pipefail as assumed). But reading that log I could not answer one question from it: did the scan find 149 targets, or zero?

Both produce docs_only=false on this PR, because ci.yml is a code path anyway. And zero would mean the guard is inert — embedded="", is_docs_path never blocks, docs-only edits to embedded markdown skip both required checks again, and #2077 is reopened behind a completely green run.

That is exactly the failure mode I merged in #2070 §9: a scan that found nothing and a scan that cannot find anything are the same two lines of log. I had shipped a detector with no control proving it can fire.

So the step now echoes the resolved count and treats zero as a broken instrument rather than as an answer — this repo has 149 such targets and cannot legitimately reach zero while any include_str! remains. Zero forces docs_only=false, matching the file's existing fail-closed contract.

Controls, 5/5 — the positive control runs first, so the zero in arm C is attributable to absence rather than to a dead harness:

arm result
A embedded .md edited docs_only=false, count=149, no alarm
B non-embedded .md edited docs_only=true, count=149, no alarm — not over-broad
D positive control: synthetic repo with include_str! count=1, no alarm
C synthetic repo with no .rs count=0, alarm, fail closed
E discriminator: same tree under main's classifier docs_only=true

Original battery still 10/10, 5 arms changing verdict vs main; job list identical to main (9 jobs) and fast-linux/rust-quality byte-identical.

Diff is +18 lines, additive only. Re-running full CI from scratch.

@codecov

codecov Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.60%. Comparing base (0893095) to head (c826985).
⚠️ Report is 12 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2081      +/-   ##
==========================================
+ Coverage   80.27%   80.60%   +0.33%     
==========================================
  Files         422      428       +6     
  Lines      198030   209200   +11170     
  Branches   198030   209200   +11170     
==========================================
+ Hits       158964   168627    +9663     
- Misses      33474    34879    +1405     
- Partials     5592     5694     +102     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (?)
cli-ort-windows 72.10% <ø> (+0.09%) ⬆️
mlas 85.90% <ø> (?)
offline 80.72% <ø> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 71 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@justinchuby
justinchuby merged commit bbd8677 into main Aug 25, 2026
17 of 18 checks passed
@justinchuby
justinchuby deleted the holden/docs-only-embedded branch August 25, 2026 08:14
justinchuby added a commit that referenced this pull request Aug 25, 2026
…rong test (#2103)

## What

§9 of `measurement-discipline` guards selector **cardinality** — `--list
| grep -c ': test$'` must resolve to exactly 1, and the run output must
then read `1 passed`. Both checks answer *how many tests ran*. Neither
answers *whether they were the tests that cover the mutated code*, and a
mutation battery only carries its claim if the answer to the second is
yes.

This adds the distinction, the falsifier, and a `**Check:**`.

## The gap, measured

Purpose-built two-test crate; filter `--exact` onto a test that does not
touch the mutated function:

```
precheck  n = 1                                    (the -eq 1 guard is SATISFIED)
mutate    covered(a) -> a + 1  becomes  a + 99
arm       expects FAIL, gets   1 passed; 0 failed  -> reads "survived"
control   unfiltered            1 passed; 1 failed -> the mutant IS caught
```

The guard never fires. The arm reports a clean PASS over a live mutant.

Worth being precise about what §9 already covers, since this was my
first reading and it was wrong: §9's guard is `-eq 1`, **not** `>= 1`,
so it *does* reject the 18-test case on cardinality alone. The residual
gap is narrower and nastier — **identity, not cardinality**. When the
selector resolves to exactly one test and that one is wrong, every
existing check in the section passes.

## Provenance

#2000 (closing #1995) hit the same shape at a larger count: a substring
filter selected 18 tests, none covering the arm under test, and reported
PASS. A non-zero selection *suppresses* the suspicion an empty one would
raise, which makes it strictly harder to catch than the vacuity case
already documented.

I verified the load-bearing half from the tree rather than relaying it:
the three tests that do cover that arm exist, and none of their names
contains the filter word. The count `18` is attributed to that battery's
own output and labelled as such — I did not rebuild the server crate to
re-derive it, and the text says so rather than implying I measured it.

## The snippet is executed, not asserted

The `**Check:**` ships a snippet, so it was run in both directions plus
a firing control:

| case | result |
|---|---|
| wrong filter, mutation live | `ARM-DRIFT`, exit 2 ✅ |
| right filter, mutation live | `arm OK`, exit 0 ✅ |
| right filter, **mutation reverted** (control) | `ARM-DRIFT` ✅ — the
check fires when there is nothing to catch |

Without the third row the first two prove only that the command runs.

## Scope

Docs only — one file, +38/-1. No code, workflow, or test behaviour
changes. The frontmatter `source:` gains `#1995/#2000 selector
identity`, matching the existing `#1619/#1982 selector vacuity` form.

Incidental confirmation while building the falsifier: `cargo test --lib
<bare_name> -- --exact --list` on a test inside `mod tests` printed `0
tests`, exit 0 — §9's own opening claim, reproduced independently.

## Expected CI

This should classify **docs-only** and skip both required checks, which
also makes it a live exercise of the classifier merged in #2081. I will
confirm the skip comes from correct classification rather than from the
fail-closed guard before merging.

Requesting independent Opus review.

---------

Co-authored-by: Holden <holden@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
A skipped required check satisfies a ruleset. Every job in ci.yml carries
`if: needs.changes.outputs.docs_only != 'true'`, so anything that makes the
`changes` classifier emit docs_only=true skips both required checks and the
PR is mergeable -- with the tests present, unrun, and reported as neither
pass nor fail. Holden falsified the reachable case in #2077: two .md files
are compiled into Rust via include_str!, so a pure-markdown edit can skip
both required lanes on a tree that does not compile (#2081 fixes the
classifier).

This gate reasoned about step-level `if:` and stopped there, one level below
where the guarantee actually lives: it proved the ORT step runs whenever the
job runs, and said nothing about whether the job runs. Pin the required
jobs' conditions to the known form instead, and refuse an unrecognised one.

Five self-test arms, positive control first so that a zero is attributable
to the condition being acceptable rather than to a harness that cannot
refuse. The last arm is the discriminator: a step-level `if:` is indented
deeper and must not be read as the job's -- without it, a regex matching any
`if:` would pass every other arm, because no fixture would tell them apart.
Mutation-proved end to end on the real workflow in both directions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

docs-only CI classifier skips both required checks for markdown that is compiled into Rust

1 participant