From 59a35c560e5da9db5e7ab37ffbcc39c7f37430eb Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 17:39:15 +0200 Subject: [PATCH 01/16] feat(flow-010): affected_set.sh - diff to crates + reverse-dep closure + nextest filter (E5) Rows checked against a cargo-tree oracle. Core crate, Cargo.lock, Cargo.toml and any unknown path give ALL. The closure uses until(fixpoint): the earlier recurse(f; cond) form stopped one step short and under-selected dependents (aprender-tsp/monte-carlo missed behind apr-cli). Co-Authored-By: Claude Sonnet 5.5 --- .../815-affected-set-rows.cmd | 2 + scripts/affected_set.sh | 54 +++++++++++++++++++ scripts/check_affected_set.sh | 37 +++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 ci/explicit-test-commands.d/815-affected-set-rows.cmd create mode 100755 scripts/affected_set.sh create mode 100755 scripts/check_affected_set.sh diff --git a/ci/explicit-test-commands.d/815-affected-set-rows.cmd b/ci/explicit-test-commands.d/815-affected-set-rows.cmd new file mode 100644 index 0000000000..639899fb2a --- /dev/null +++ b/ci/explicit-test-commands.d/815-affected-set-rows.cmd @@ -0,0 +1,2 @@ +# FLOW-010 E5: affected-set tool rows (leaf+dependents vs cargo tree oracle, core/Cargo.lock/unknown -> ALL). +bash scripts/check_affected_set.sh diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh new file mode 100755 index 0000000000..5582dd1618 --- /dev/null +++ b/scripts/affected_set.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# FLOW-010 E5: the diff -> the set of workspace packages whose tests it can change. +# +# scripts/affected_set.sh [--filter] [--diff-from FILE] [BASE [HEAD]] +# +# Prints one package per line, or the single word ALL. With --filter it prints the +# nextest filterset instead (`package(a) | package(b)`, or `all()` for ALL). +# Fail-safe: anything this tool cannot place in a package is ALL. +# crates//... the package + its reverse-dependency closure (cargo metadata) +# a CORE crate ALL (its closure is the workspace; naming it would only hide that) +# docs/, *.md, ci/sections.yml, ci/shard-step-costs.tsv, ci/explicit-test-commands.d/ +# contribute nothing (no crate code) +# Cargo.lock, Cargo.toml, rust-toolchain.toml, .cargo/, .github/, Makefile, any other path +# ALL +# An empty diff prints nothing. --diff-from reads the changed paths from FILE (tests). +set -euo pipefail +CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts}" +filter=0 from="" +while [ $# -gt 0 ]; do + case "$1" in + --filter) filter=1; shift ;; + --diff-from) from="$2"; shift 2 ;; + *) break ;; + esac +done +if [ -n "$from" ]; then files="$(cat "$from")"; else files="$(git diff --name-only "${1:-origin/main}...${2:-HEAD}")"; fi +emit() { # $@ = package names, or ALL + if [ "${1:-}" = ALL ]; then [ "$filter" = 1 ] && echo 'all()' || echo ALL; return; fi + if [ "$filter" = 1 ]; then printf '%s\n' "$@" | sed 's/.*/package(&)/' | paste -sd'|' | sed 's/|/ | /g'; else printf '%s\n' "$@"; fi +} +[ -n "$files" ] || exit 0 +meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)" +root="$(git rev-parse --show-toplevel)" +dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" +touched="" +while IFS= read -r f; do + case "$f" in + '') continue ;; + docs/* | *.md | ci/sections.yml | ci/shard-step-costs.tsv | ci/explicit-test-commands.d/*) continue ;; + esac + # longest manifest dir that prefixes the path + pkg="$(awk -F'\t' -v f="$f" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" + [ -n "$pkg" ] || { emit ALL; exit 0; } + case " $CORE " in *" $pkg "*) emit ALL; exit 0 ;; esac + touched="$touched"$'\n'"$pkg" +done <<<"$files" +touched="$(printf '%s\n' "$touched" | grep -v '^$' | sort -u || true)" +[ -n "$touched" ] || exit 0 +mapfile -t out < <(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" ' + (.packages | map({name, deps: [.dependencies[].name]})) as $p + | def grow($s): ($s + [$p[] | select(any(.deps[]; . as $d | $s | index($d))) | .name] | unique); + (.packages | map(.name)) as $m + | ($t | until(grow(.) == .; grow(.))) | .[] | select(. as $n | $m | index($n))' <<<"$meta") +emit "${out[@]}" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh new file mode 100755 index 0000000000..47c6d561a2 --- /dev/null +++ b/scripts/check_affected_set.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# FLOW-010 E5 RED rows for scripts/affected_set.sh. Oracle for the closure is +# `cargo tree -i` (a different code path from the tool's cargo-metadata jq). +set -euo pipefail +A=scripts/affected_set.sh +t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +fail=0 +row() { # name want got + if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1"; echo " want: $2" | head -c 300; echo; echo " got: $3" | head -c 300; echo; fail=1; fi +} +run() { printf '%s\n' "$@" >"$t/d"; $A --diff-from "$t/d"; } +core_list=" aprender-core aprender-compute aprender-contracts " +meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) +members=$(jq -r '.packages[].name' <<<"$meta" | sort) +# a non-core crate that has dependents, and a leaf with none +withdep="" leaf="" +for p in $members; do + case "$core_list" in *" $p "*) continue ;; esac + n=$(cargo tree --offline -i "$p" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//' | sort -u | grep -vc "^$p$" || true) + dir=$(jq -r --arg p "$p" '.packages[]|select(.name==$p)|.manifest_path' <<<"$meta" | sed "s|$(git rev-parse --show-toplevel)/||; s|/Cargo.toml||") + [ -z "$withdep" ] && [ "$n" -gt 0 ] && [ "$n" -lt 10 ] && withdep="$p:$dir" + [ -z "$leaf" ] && [ "$n" -eq 0 ] && leaf="$p:$dir" + [ -n "$withdep" ] && [ -n "$leaf" ] && break +done +[ -n "$withdep" ] && [ -n "$leaf" ] || { echo "FAIL could not find a leaf and a crate with dependents"; exit 1; } +wp=${withdep%%:*}; wd=${withdep#*:}; lp=${leaf%%:*}; ld=${leaf#*:} +want=$(cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//' | sort -u) +row "crate with dependents ($wp) -> itself + dependents" "$want" "$(run "$wd/src/lib.rs" | sort -u)" +row "leaf crate ($lp) -> itself only" "$lp" "$(run "$ld/src/lib.rs")" +row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" +row "Cargo.lock -> ALL" ALL "$(run Cargo.lock)" +row "unknown path -> ALL" ALL "$(run some/unknown/path.txt)" +row "unknown path next to a leaf -> ALL" ALL "$(run "$ld/src/lib.rs" scripts/whatever.sh)" +row "docs only -> nothing" "" "$(run docs/x.md)" +row "--filter names the package" "package($lp)" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" +row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" +exit "$fail" From ad30656062253c0a49f052291476500b73e1eee8 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 18:19:20 +0200 Subject: [PATCH 02/16] fix(flow-010): empty diff and bad base are ALL; pin dev-dep counting (Refs #5802) Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 15 ++++++++++----- scripts/check_affected_set.sh | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 5582dd1618..22b2028a76 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -12,7 +12,10 @@ # contribute nothing (no crate code) # Cargo.lock, Cargo.toml, rust-toolchain.toml, .cargo/, .github/, Makefile, any other path # ALL -# An empty diff prints nothing. --diff-from reads the changed paths from FILE (tests). +# An empty diff, or a diff git cannot compute (bad base: exit 128), is ALL: nothing was +# measured, so nothing may be skipped. A diff of only no-crate-code paths prints nothing. +# --diff-from reads the changed paths from FILE (tests). AFFECTED_META / AFFECTED_ROOT +# replace `cargo metadata` and the checkout root (tests; synthetic graphs). set -euo pipefail CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts}" filter=0 from="" @@ -23,14 +26,16 @@ while [ $# -gt 0 ]; do *) break ;; esac done -if [ -n "$from" ]; then files="$(cat "$from")"; else files="$(git diff --name-only "${1:-origin/main}...${2:-HEAD}")"; fi +if [ -n "$from" ]; then files="$(cat "$from")" +else files="$(git diff --name-only "${1:-origin/main}...${2:-HEAD}")" || files=""; fi emit() { # $@ = package names, or ALL if [ "${1:-}" = ALL ]; then [ "$filter" = 1 ] && echo 'all()' || echo ALL; return; fi if [ "$filter" = 1 ]; then printf '%s\n' "$@" | sed 's/.*/package(&)/' | paste -sd'|' | sed 's/|/ | /g'; else printf '%s\n' "$@"; fi } -[ -n "$files" ] || exit 0 -meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)" -root="$(git rev-parse --show-toplevel)" +[ -n "$files" ] || { emit ALL; exit 0; } +if [ -n "${AFFECTED_META:-}" ]; then meta="$(cat "$AFFECTED_META")" +else meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)"; fi +root="${AFFECTED_ROOT:-$(git rev-parse --show-toplevel)}" dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" touched="" while IFS= read -r f; do diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 47c6d561a2..a0919ae580 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -34,4 +34,25 @@ row "unknown path next to a leaf -> ALL" ALL "$(run "$ld/src/lib.rs" scripts/wha row "docs only -> nothing" "" "$(run docs/x.md)" row "--filter names the package" "package($lp)" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" +# empty diff and a bad base are ALL (nothing measured, nothing skipped) +: >"$t/empty" +row "empty diff -> ALL" ALL "$($A --diff-from "$t/empty")" +row "bad base (git exit 128) -> ALL" ALL "$($A no-such-base-ref-5802 HEAD 2>/dev/null)" +# dev-dep counting: b depends on a ONLY as a dev-dependency; touching a must reach b. +cat >"$t/meta.json" <<'JSON' +{"packages":[ + {"name":"a","manifest_path":"/r/crates/a/Cargo.toml","dependencies":[]}, + {"name":"b","manifest_path":"/r/crates/b/Cargo.toml","dependencies":[{"name":"a","kind":"dev"}]}, + {"name":"c","manifest_path":"/r/crates/c/Cargo.toml","dependencies":[]}]} +JSON +row "dev-only edge is counted: touching a reaches b" "a +b" "$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r run crates/a/src/lib.rs | sort -u)" +row "an unrelated crate is not reached" "c" "$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r run crates/c/src/lib.rs)" +# mutant: a tool that drops dev edges misses b, and the dev-dep row must see it +sed 's/\[\.dependencies\[\]\.name\]/[.dependencies[]|select(.kind!="dev")|.name]/' "$A" >"$t/mut.sh" +if cmp -s "$A" "$t/mut.sh"; then echo "FAIL mutant did not apply"; fail=1 +else + got=$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r bash "$t/mut.sh" --diff-from <(echo crates/a/src/lib.rs) | sort -u) + row "MUTANT: ignoring dev edges is caught by the dev-dep row" "a" "$got" +fi exit "$fail" From dea7901cccfa1c5eb82b0d2efa2538cfd8cd292e Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 18:23:49 +0200 Subject: [PATCH 03/16] fix(flow-010): affected_set --no-renames and include_str!/include_bytes! mapping (Refs #5802) A rename out of a crate showed only its destination (git rename detection), so the source crate was skipped; --no-renames reports both paths. A docs/*.md file a crate embeds with include_str!/include_bytes!(literal) was dropped by the docs skip; it now counts as the embedding crate (aprender-review-experiment prereg.rs). Rows plus a RED mutant for each. Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 23 +++++++++++++++++++++-- scripts/check_affected_set.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 22b2028a76..ec0c162635 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -14,6 +14,9 @@ # ALL # An empty diff, or a diff git cannot compute (bad base: exit 128), is ALL: nothing was # measured, so nothing may be skipped. A diff of only no-crate-code paths prints nothing. +# git diff runs with --no-renames: a moved file is its old path AND its new path (a rename +# out of a crate would otherwise show only the destination and the source crate would be skipped). +# A docs/*.md file that a crate embeds with include_str!/include_bytes!("literal") counts as that crate. # --diff-from reads the changed paths from FILE (tests). AFFECTED_META / AFFECTED_ROOT # replace `cargo metadata` and the checkout root (tests; synthetic graphs). set -euo pipefail @@ -27,7 +30,7 @@ while [ $# -gt 0 ]; do esac done if [ -n "$from" ]; then files="$(cat "$from")" -else files="$(git diff --name-only "${1:-origin/main}...${2:-HEAD}")" || files=""; fi +else files="$(git diff --name-only --no-renames "${1:-origin/main}...${2:-HEAD}")" || files=""; fi emit() { # $@ = package names, or ALL if [ "${1:-}" = ALL ]; then [ "$filter" = 1 ] && echo 'all()' || echo ALL; return; fi if [ "$filter" = 1 ]; then printf '%s\n' "$@" | sed 's/.*/package(&)/' | paste -sd'|' | sed 's/|/ | /g'; else printf '%s\n' "$@"; fi @@ -37,11 +40,27 @@ if [ -n "${AFFECTED_META:-}" ]; then meta="$(cat "$AFFECTED_META")" else meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)"; fi root="${AFFECTED_ROOT:-$(git rev-parse --show-toplevel)}" dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" +# "\t" for every include_str!/include_bytes!("literal") under crates/, +# the literal resolved against the embedding file's directory. Non-literal (concat!/macro) args are not resolved. +INCL_PL='use File::Basename; for $f (@ARGV) { open F, "<", $f or next; local $/; $s = ; close F; + while ($s =~ /include_(?:str|bytes)!\s*\(\s*"([^"]*)"/g) { @o = (); + for (split m{/}, dirname($f) . "/" . $1) { next if $_ eq "" || $_ eq "."; if ($_ eq "..") { pop @o } else { push @o, $_ } } + print join("/", @o) . "\t" . $f . "\n" } }' +incl="$( (cd "$root" 2>/dev/null && find crates -name "*.rs" -type f -print0 2>/dev/null | xargs -0 -r perl -0777 -e "$INCL_PL") || true )" touched="" while IFS= read -r f; do case "$f" in '') continue ;; - docs/* | *.md | ci/sections.yml | ci/shard-step-costs.tsv | ci/explicit-test-commands.d/*) continue ;; + docs/* | *.md | ci/sections.yml | ci/shard-step-costs.tsv | ci/explicit-test-commands.d/*) + # skipped as "no crate code" unless a crate embeds it: then it is that crate's input + while IFS=$'\t' read -r _ src; do + [ -n "$src" ] || continue + pkg="$(awk -F'\t' -v f="$src" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" + [ -n "$pkg" ] || { emit ALL; exit 0; } + case " $CORE " in *" $pkg "*) emit ALL; exit 0 ;; esac + touched="$touched"$'\n'"$pkg" + done < <(awk -F'\t' -v f="$f" '$1 == f' <<<"$incl") + continue ;; esac # longest manifest dir that prefixes the path pkg="$(awk -F'\t' -v f="$f" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index a0919ae580..ad06571a26 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -55,4 +55,34 @@ else got=$(AFFECTED_META="$t/meta.json" AFFECTED_ROOT=/r bash "$t/mut.sh" --diff-from <(echo crates/a/src/lib.rs) | sort -u) row "MUTANT: ignoring dev edges is caught by the dev-dep row" "a" "$got" fi +# --no-renames: moving a file out of crate a into crate c touches BOTH (git's rename detection would show only c). +rt="$t/repo"; mkdir -p "$rt/crates/a/src" "$rt/crates/b" "$rt/crates/c/src"; abs="$(cd "$(dirname "$A")" && pwd)/$(basename "$A")" +sed "s|/r/|$rt/|" "$t/meta.json" >"$t/meta-rt.json" +printf 'pub fn moved() -> u32 { 1 }\n// padding so git sees a rename, not a delete+add\n// padding\n' >"$rt/crates/a/src/x.rs" +# plumbing (write-tree/commit-tree) so the scratch repo runs no commit hooks +(cd "$rt" && git init -q && git add -A && b=$(GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t git commit-tree "$(git write-tree)" -m base) && git mv crates/a/src/x.rs crates/c/src/x.rs && git update-ref HEAD "$b" && git add -A && GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t git update-ref HEAD "$(GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t git commit-tree "$(git write-tree)" -p "$b" -m move)") +mv_run() { (cd "$rt" && AFFECTED_META="$t/meta-rt.json" AFFECTED_ROOT="$rt" bash "$1" HEAD~1 HEAD | sort -u); } +row "rename a->c touches both ends (a, its dependent b, and c)" "a +b +c" "$(mv_run "$abs")" +sed 's/--name-only --no-renames/--name-only/' "$A" >"$t/mut-rn.sh" +if cmp -s "$A" "$t/mut-rn.sh"; then echo "FAIL mutant did not apply"; fail=1 +else row "MUTANT: letting git detect renames is caught by the rename row" "c" "$(mv_run "$t/mut-rn.sh")"; fi +# include_str!: a docs file a crate embeds is that crate's input (and reaches its dependents); an unembedded one is not. +it="$t/inc"; mkdir -p "$it/crates/a/src" "$it/docs" +printf 'pub const D: &str = include_str!("../../../docs/embedded.md");\n' >"$it/crates/a/src/lib.rs" +sed "s|/r/|$it/|" "$t/meta.json" >"$t/meta-inc.json" +inc_run() { printf '%s\n' "$2" >"$t/dinc"; AFFECTED_META="$t/meta-inc.json" AFFECTED_ROOT="$it" bash "$1" --diff-from "$t/dinc" | sort -u; } +row "docs file embedded by crate a -> a and its dependent b" "a +b" "$(inc_run "$A" docs/embedded.md)" +row "docs file nobody embeds -> nothing" "" "$(inc_run "$A" docs/other.md)" +sed "s/'\\\$1 == f'/'0'/" "$A" >"$t/mut-inc.sh" +if cmp -s "$A" "$t/mut-inc.sh"; then echo "FAIL include mutant did not apply"; fail=1 +else row "MUTANT: ignoring include_str! is caught by the embedded-doc row" "" "$(inc_run "$t/mut-inc.sh" docs/embedded.md)"; fi +real="$($A --diff-from <(echo docs/audits/prm-001/prereg.lock) | sort -u)" +case " +$real +" in *" +aprender-review-experiment +"*) echo "ok real tree: prereg.lock reaches aprender-review-experiment" ;; *) echo "FAIL real tree: prereg.lock -> '$real'"; fail=1 ;; esac exit "$fail" From 5f272c850a455dea6ceccb737931dd91a05e3ec1 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 18:47:29 +0200 Subject: [PATCH 04/16] fix(e5): wire check_affected_set.sh, SC1087 and SEC011 in the affected-set scripts Co-Authored-By: Claude Sonnet 5.5 --- ci/sections.yml | 2 ++ scripts/affected_set.sh | 2 +- scripts/check_affected_set.sh | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ci/sections.yml b/ci/sections.yml index cdb4bea410..f85871681f 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -1698,6 +1698,8 @@ jobs: # a secrets. read reaches it only through ci.yml's FAT_SECRET_. Case table first. - name: "Fat-job secret pass-through case table (6 rows)" run: setsid --wait bash scripts/check_fat_secrets_passed.sh --self-test + - name: "Affected-set tool case table (FLOW-010 E5, RED rows and mutants)" + run: setsid --wait bash scripts/check_affected_set.sh - name: Every secret a section reads is passed to the fat job under its own name run: setsid --wait bash scripts/check_fat_secrets_passed.sh # PRQ-013 (PP-066 C0-5, #2982): the job that judges a PR's OWN receipt must be diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index ec0c162635..33704ad6df 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -72,7 +72,7 @@ touched="$(printf '%s\n' "$touched" | grep -v '^$' | sort -u || true)" [ -n "$touched" ] || exit 0 mapfile -t out < <(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" ' (.packages | map({name, deps: [.dependencies[].name]})) as $p - | def grow($s): ($s + [$p[] | select(any(.deps[]; . as $d | $s | index($d))) | .name] | unique); + | def grow($s): ($s + [$p|.[] | select(any(.deps[]; . as $d | $s | index($d))) | .name] | unique); (.packages | map(.name)) as $m | ($t | until(grow(.) == .; grow(.))) | .[] | select(. as $n | $m | index($n))' <<<"$meta") emit "${out[@]}" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index ad06571a26..c540aec3ef 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -3,7 +3,7 @@ # `cargo tree -i` (a different code path from the tool's cargo-metadata jq). set -euo pipefail A=scripts/affected_set.sh -t=$(mktemp -d); trap 'rm -rf "$t"' EXIT +t=$(mktemp -d); trap 'rm -rf -- "${t:?}"' EXIT fail=0 row() { # name want got if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1"; echo " want: $2" | head -c 300; echo; echo " got: $3" | head -c 300; echo; fail=1; fi From 1e73c59a2838cb646f8ccb4cfd496a8b323fd5dc Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 18:59:37 +0200 Subject: [PATCH 05/16] fix(e5): docs, root *.md and ci/ select ALL; a crate Cargo.toml selects ALL; check runs with safe.directory Tests read README.md, CLAUDE.md, ci/sections.yml and docs/ at run time, so skipping them emptied the set. The include_str! scan is gone: docs are ALL. Rows and mutants for each. Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 30 +++++++----------------------- scripts/check_affected_set.sh | 34 ++++++++++++++++------------------ 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 33704ad6df..4afee9fdcb 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -8,15 +8,15 @@ # Fail-safe: anything this tool cannot place in a package is ALL. # crates//... the package + its reverse-dependency closure (cargo metadata) # a CORE crate ALL (its closure is the workspace; naming it would only hide that) -# docs/, *.md, ci/sections.yml, ci/shard-step-costs.tsv, ci/explicit-test-commands.d/ -# contribute nothing (no crate code) -# Cargo.lock, Cargo.toml, rust-toolchain.toml, .cargo/, .github/, Makefile, any other path -# ALL +# Cargo.toml of a crate ALL (a manifest edit changes features/deps, which no path can bound) +# docs/, root *.md, ci/ (sections.yml, explicit-test-commands.d/, ...), Cargo.lock, root Cargo.toml, +# rust-toolchain.toml, .cargo/, .github/, Makefile, any other path outside a crate +# ALL (tests read README.md, CLAUDE.md, ci/sections.yml and docs/ at run time) +# A *.md under crates// is that crate's input like any file there. # An empty diff, or a diff git cannot compute (bad base: exit 128), is ALL: nothing was -# measured, so nothing may be skipped. A diff of only no-crate-code paths prints nothing. +# measured, so nothing may be skipped. # git diff runs with --no-renames: a moved file is its old path AND its new path (a rename # out of a crate would otherwise show only the destination and the source crate would be skipped). -# A docs/*.md file that a crate embeds with include_str!/include_bytes!("literal") counts as that crate. # --diff-from reads the changed paths from FILE (tests). AFFECTED_META / AFFECTED_ROOT # replace `cargo metadata` and the checkout root (tests; synthetic graphs). set -euo pipefail @@ -40,27 +40,11 @@ if [ -n "${AFFECTED_META:-}" ]; then meta="$(cat "$AFFECTED_META")" else meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)"; fi root="${AFFECTED_ROOT:-$(git rev-parse --show-toplevel)}" dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" -# "\t" for every include_str!/include_bytes!("literal") under crates/, -# the literal resolved against the embedding file's directory. Non-literal (concat!/macro) args are not resolved. -INCL_PL='use File::Basename; for $f (@ARGV) { open F, "<", $f or next; local $/; $s = ; close F; - while ($s =~ /include_(?:str|bytes)!\s*\(\s*"([^"]*)"/g) { @o = (); - for (split m{/}, dirname($f) . "/" . $1) { next if $_ eq "" || $_ eq "."; if ($_ eq "..") { pop @o } else { push @o, $_ } } - print join("/", @o) . "\t" . $f . "\n" } }' -incl="$( (cd "$root" 2>/dev/null && find crates -name "*.rs" -type f -print0 2>/dev/null | xargs -0 -r perl -0777 -e "$INCL_PL") || true )" touched="" while IFS= read -r f; do case "$f" in '') continue ;; - docs/* | *.md | ci/sections.yml | ci/shard-step-costs.tsv | ci/explicit-test-commands.d/*) - # skipped as "no crate code" unless a crate embeds it: then it is that crate's input - while IFS=$'\t' read -r _ src; do - [ -n "$src" ] || continue - pkg="$(awk -F'\t' -v f="$src" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" - [ -n "$pkg" ] || { emit ALL; exit 0; } - case " $CORE " in *" $pkg "*) emit ALL; exit 0 ;; esac - touched="$touched"$'\n'"$pkg" - done < <(awk -F'\t' -v f="$f" '$1 == f' <<<"$incl") - continue ;; + */Cargo.toml) case "$f" in crates/*) emit ALL; exit 0 ;; esac ;; esac # longest manifest dir that prefixes the path pkg="$(awk -F'\t' -v f="$f" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index c540aec3ef..e7f433be03 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -2,6 +2,8 @@ # FLOW-010 E5 RED rows for scripts/affected_set.sh. Oracle for the closure is # `cargo tree -i` (a different code path from the tool's cargo-metadata jq). set -euo pipefail +# the CI container runs as root over a host-owned checkout: git would refuse it (dubious ownership) +export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' A=scripts/affected_set.sh t=$(mktemp -d); trap 'rm -rf -- "${t:?}"' EXIT fail=0 @@ -31,7 +33,13 @@ row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" row "Cargo.lock -> ALL" ALL "$(run Cargo.lock)" row "unknown path -> ALL" ALL "$(run some/unknown/path.txt)" row "unknown path next to a leaf -> ALL" ALL "$(run "$ld/src/lib.rs" scripts/whatever.sh)" -row "docs only -> nothing" "" "$(run docs/x.md)" +row "docs file -> ALL (tests read docs/ at run time)" ALL "$(run docs/x.md)" +row "root README.md -> ALL" ALL "$(run README.md)" +row "root CLAUDE.md -> ALL" ALL "$(run CLAUDE.md)" +row "ci/sections.yml -> ALL" ALL "$(run ci/sections.yml)" +row "ci/explicit-test-commands.d fragment -> ALL" ALL "$(run ci/explicit-test-commands.d/815-affected-set-rows.cmd)" +row "a crate's own Cargo.toml -> ALL" ALL "$(run "$ld/Cargo.toml")" +row "a .md inside a leaf crate -> that crate (not skipped by the *.md glob)" "$lp" "$(run "$ld/playbooks/x.md")" row "--filter names the package" "package($lp)" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" # empty diff and a bad base are ALL (nothing measured, nothing skipped) @@ -68,21 +76,11 @@ c" "$(mv_run "$abs")" sed 's/--name-only --no-renames/--name-only/' "$A" >"$t/mut-rn.sh" if cmp -s "$A" "$t/mut-rn.sh"; then echo "FAIL mutant did not apply"; fail=1 else row "MUTANT: letting git detect renames is caught by the rename row" "c" "$(mv_run "$t/mut-rn.sh")"; fi -# include_str!: a docs file a crate embeds is that crate's input (and reaches its dependents); an unembedded one is not. -it="$t/inc"; mkdir -p "$it/crates/a/src" "$it/docs" -printf 'pub const D: &str = include_str!("../../../docs/embedded.md");\n' >"$it/crates/a/src/lib.rs" -sed "s|/r/|$it/|" "$t/meta.json" >"$t/meta-inc.json" -inc_run() { printf '%s\n' "$2" >"$t/dinc"; AFFECTED_META="$t/meta-inc.json" AFFECTED_ROOT="$it" bash "$1" --diff-from "$t/dinc" | sort -u; } -row "docs file embedded by crate a -> a and its dependent b" "a -b" "$(inc_run "$A" docs/embedded.md)" -row "docs file nobody embeds -> nothing" "" "$(inc_run "$A" docs/other.md)" -sed "s/'\\\$1 == f'/'0'/" "$A" >"$t/mut-inc.sh" -if cmp -s "$A" "$t/mut-inc.sh"; then echo "FAIL include mutant did not apply"; fail=1 -else row "MUTANT: ignoring include_str! is caught by the embedded-doc row" "" "$(inc_run "$t/mut-inc.sh" docs/embedded.md)"; fi -real="$($A --diff-from <(echo docs/audits/prm-001/prereg.lock) | sort -u)" -case " -$real -" in *" -aprender-review-experiment -"*) echo "ok real tree: prereg.lock reaches aprender-review-experiment" ;; *) echo "FAIL real tree: prereg.lock -> '$real'"; fail=1 ;; esac +# mutants: the skip list that used to exist (docs/*, *.md) must be caught by the ALL rows above. +sed 's#^ \*/Cargo.toml)# docs/* | *.md) continue ;;\n */Cargo.toml)#' "$A" >"$t/mut-docs.sh" +if cmp -s "$A" "$t/mut-docs.sh"; then echo "FAIL docs-skip mutant did not apply"; fail=1 +else printf '%s\n' docs/x.md >"$t/dd"; row "MUTANT: skipping docs/*.md is caught by the docs-ALL row" "" "$(bash "$t/mut-docs.sh" --diff-from "$t/dd" | head -n1 || true)"; fi +sed 's#^ \*/Cargo.toml) case.*# */Cargo.toml) continue ;;#' "$A" >"$t/mut-toml.sh" +if cmp -s "$A" "$t/mut-toml.sh"; then echo "FAIL manifest mutant did not apply"; fail=1 +else printf '%s\n' "$ld/Cargo.toml" >"$t/dt"; row "MUTANT: skipping crate Cargo.toml is caught by the manifest row" "" "$(bash "$t/mut-toml.sh" --diff-from "$t/dt" | head -n1 || true)"; fi exit "$fail" From 2ae2e4ebcc2a4ffc143d7743ae9fda0f97b56933 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 19:21:11 +0200 Subject: [PATCH 06/16] fix(e5): any non-.rs file under crates/ selects ALL; move the check to guard-cargo-steps Other crates' tests read crates/*/README.md and aprender-serve/kernel-registry.json. check_affected_set.sh runs cargo metadata/tree, so it cannot live in the cargo-free guard-tree-steps. Co-Authored-By: Claude Sonnet 5.5 --- ci/sections.yml | 5 +++-- scripts/affected_set.sh | 6 ++++-- scripts/check_affected_set.sh | 10 ++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ci/sections.yml b/ci/sections.yml index f85871681f..cefb8919a2 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -1698,8 +1698,6 @@ jobs: # a secrets. read reaches it only through ci.yml's FAT_SECRET_. Case table first. - name: "Fat-job secret pass-through case table (6 rows)" run: setsid --wait bash scripts/check_fat_secrets_passed.sh --self-test - - name: "Affected-set tool case table (FLOW-010 E5, RED rows and mutants)" - run: setsid --wait bash scripts/check_affected_set.sh - name: Every secret a section reads is passed to the fat job under its own name run: setsid --wait bash scripts/check_fat_secrets_passed.sh # PRQ-013 (PP-066 C0-5, #2982): the job that judges a PR's OWN receipt must be @@ -1938,6 +1936,9 @@ jobs: run: setsid --wait bash scripts/check_lockfile_current.sh - name: "lockfile guard must still turn RED (case table)" run: setsid --wait bash scripts/check_lockfile_current.sh --self-test + # FLOW-010 E5: runs cargo metadata/tree, so it lives in the cargo job, not guard-tree-steps. + - name: "Affected-set tool case table (FLOW-010 E5, RED rows and mutants)" + run: setsid --wait bash scripts/check_affected_set.sh # #2640: the SAME defect class as the two steps above, one level up. The # dogfood release runner existed as two 1170-line copies whose divergence # was, in every hunk, a pin for a release verifier that one copy had and diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 4afee9fdcb..aab7fc1aef 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -12,7 +12,8 @@ # docs/, root *.md, ci/ (sections.yml, explicit-test-commands.d/, ...), Cargo.lock, root Cargo.toml, # rust-toolchain.toml, .cargo/, .github/, Makefile, any other path outside a crate # ALL (tests read README.md, CLAUDE.md, ci/sections.yml and docs/ at run time) -# A *.md under crates// is that crate's input like any file there. +# Any non-.rs file under crates// (README.md, *.json, *.toml, ...) is ALL: other crates' tests read them +# (aprender-core readme_contract.rs reads every crates/*/README.md; aprender-contracts reads aprender-serve/kernel-registry.json). # An empty diff, or a diff git cannot compute (bad base: exit 128), is ALL: nothing was # measured, so nothing may be skipped. # git diff runs with --no-renames: a moved file is its old path AND its new path (a rename @@ -44,7 +45,8 @@ touched="" while IFS= read -r f; do case "$f" in '') continue ;; - */Cargo.toml) case "$f" in crates/*) emit ALL; exit 0 ;; esac ;; + crates/*.rs) ;; + crates/*) emit ALL; exit 0 ;; esac # longest manifest dir that prefixes the path pkg="$(awk -F'\t' -v f="$f" 'index(f, $2 "/") == 1 && length($2) > best { best = length($2); n = $1 } END { print n }' <<<"$dirs")" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index e7f433be03..8b40594ae2 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -39,7 +39,9 @@ row "root CLAUDE.md -> ALL" ALL "$(run CLAUDE.md)" row "ci/sections.yml -> ALL" ALL "$(run ci/sections.yml)" row "ci/explicit-test-commands.d fragment -> ALL" ALL "$(run ci/explicit-test-commands.d/815-affected-set-rows.cmd)" row "a crate's own Cargo.toml -> ALL" ALL "$(run "$ld/Cargo.toml")" -row "a .md inside a leaf crate -> that crate (not skipped by the *.md glob)" "$lp" "$(run "$ld/playbooks/x.md")" +row "a .md inside a leaf crate -> ALL (aprender-core reads crates/*/README.md)" ALL "$(run "$ld/README.md")" +row "a .json inside a leaf crate -> ALL (kernel-registry.json is read by aprender-contracts)" ALL "$(run "$ld/kernel-registry.json")" +row "a .rs inside a leaf crate stays scoped" "$lp" "$(run "$ld/src/other.rs")" row "--filter names the package" "package($lp)" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" # empty diff and a bad base are ALL (nothing measured, nothing skipped) @@ -77,10 +79,10 @@ sed 's/--name-only --no-renames/--name-only/' "$A" >"$t/mut-rn.sh" if cmp -s "$A" "$t/mut-rn.sh"; then echo "FAIL mutant did not apply"; fail=1 else row "MUTANT: letting git detect renames is caught by the rename row" "c" "$(mv_run "$t/mut-rn.sh")"; fi # mutants: the skip list that used to exist (docs/*, *.md) must be caught by the ALL rows above. -sed 's#^ \*/Cargo.toml)# docs/* | *.md) continue ;;\n */Cargo.toml)#' "$A" >"$t/mut-docs.sh" +sed 's#^ crates/\*.rs) ;;# docs/* | *.md) continue ;;\n crates/*.rs) ;;#' "$A" >"$t/mut-docs.sh" if cmp -s "$A" "$t/mut-docs.sh"; then echo "FAIL docs-skip mutant did not apply"; fail=1 else printf '%s\n' docs/x.md >"$t/dd"; row "MUTANT: skipping docs/*.md is caught by the docs-ALL row" "" "$(bash "$t/mut-docs.sh" --diff-from "$t/dd" | head -n1 || true)"; fi -sed 's#^ \*/Cargo.toml) case.*# */Cargo.toml) continue ;;#' "$A" >"$t/mut-toml.sh" +sed 's#^ crates/\*) emit ALL.*# crates/*) continue ;;#' "$A" >"$t/mut-toml.sh" if cmp -s "$A" "$t/mut-toml.sh"; then echo "FAIL manifest mutant did not apply"; fail=1 -else printf '%s\n' "$ld/Cargo.toml" >"$t/dt"; row "MUTANT: skipping crate Cargo.toml is caught by the manifest row" "" "$(bash "$t/mut-toml.sh" --diff-from "$t/dt" | head -n1 || true)"; fi +else printf '%s\n' "$ld/Cargo.toml" >"$t/dt"; row "MUTANT: skipping non-.rs crate files is caught by the manifest row" "" "$(bash "$t/mut-toml.sh" --diff-from "$t/dt" | head -n1 || true)"; fi exit "$fail" From cf174b863d8a07ef01f1a4c16fdf96dc18c0e010 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 19:44:25 +0200 Subject: [PATCH 07/16] fix(e5): refuse '..' in --diff-from (bashrs SEC010); apr-cli is CORE (other crates' tests read its sources) Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 6 +++++- scripts/check_affected_set.sh | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index aab7fc1aef..7bc1e77836 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -21,7 +21,10 @@ # --diff-from reads the changed paths from FILE (tests). AFFECTED_META / AFFECTED_ROOT # replace `cargo metadata` and the checkout root (tests; synthetic graphs). set -euo pipefail -CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts}" +# CORE = ALL on touch. apr-cli is here because other crates' tests read its sources (aprender-core readme_contract.rs reads +# apr-cli/src/commands_enum.rs; spec_checklist_*, toyota_principles_tests). Reader edges are not in cargo metadata, so a +# crate whose source another crate reads at run time must be listed. +CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli}" filter=0 from="" while [ $# -gt 0 ]; do case "$1" in @@ -30,6 +33,7 @@ while [ $# -gt 0 ]; do *) break ;; esac done +case "$from" in *..*) echo "affected_set: refusing a --diff-from with \"..\": $from" >&2; exit 2 ;; esac # bashrs SEC010 if [ -n "$from" ]; then files="$(cat "$from")" else files="$(git diff --name-only --no-renames "${1:-origin/main}...${2:-HEAD}")" || files=""; fi emit() { # $@ = package names, or ALL diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 8b40594ae2..fe72595799 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -11,7 +11,7 @@ row() { # name want got if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1"; echo " want: $2" | head -c 300; echo; echo " got: $3" | head -c 300; echo; fail=1; fi } run() { printf '%s\n' "$@" >"$t/d"; $A --diff-from "$t/d"; } -core_list=" aprender-core aprender-compute aprender-contracts " +core_list=" aprender-core aprender-compute aprender-contracts apr-cli " meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) members=$(jq -r '.packages[].name' <<<"$meta" | sort) # a non-core crate that has dependents, and a leaf with none @@ -20,6 +20,7 @@ for p in $members; do case "$core_list" in *" $p "*) continue ;; esac n=$(cargo tree --offline -i "$p" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//' | sort -u | grep -vc "^$p$" || true) dir=$(jq -r --arg p "$p" '.packages[]|select(.name==$p)|.manifest_path' <<<"$meta" | sed "s|$(git rev-parse --show-toplevel)/||; s|/Cargo.toml||") + case "$dir" in crates/*) ;; *) continue ;; esac [ -z "$withdep" ] && [ "$n" -gt 0 ] && [ "$n" -lt 10 ] && withdep="$p:$dir" [ -z "$leaf" ] && [ "$n" -eq 0 ] && leaf="$p:$dir" [ -n "$withdep" ] && [ -n "$leaf" ] && break @@ -30,6 +31,8 @@ want=$(cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix no row "crate with dependents ($wp) -> itself + dependents" "$want" "$(run "$wd/src/lib.rs" | sort -u)" row "leaf crate ($lp) -> itself only" "$lp" "$(run "$ld/src/lib.rs")" row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" +row "apr-cli source -> ALL (aprender-core tests read apr-cli/src/commands_enum.rs)" ALL "$(run crates/apr-cli/src/commands_enum.rs)" +row "--diff-from with .. is refused (SEC010)" "2" "$($A --diff-from ../x >/dev/null 2>&1; echo $?)" row "Cargo.lock -> ALL" ALL "$(run Cargo.lock)" row "unknown path -> ALL" ALL "$(run some/unknown/path.txt)" row "unknown path next to a leaf -> ALL" ALL "$(run "$ld/src/lib.rs" scripts/whatever.sh)" From 132b80d037f7bf56e873450cb0647a92295b6853 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 20:28:06 +0200 Subject: [PATCH 08/16] fix(e5): aprender-quant is CORE; any crates/*/build.rs selects ALL (reader edges), with rows and a mutant Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 6 ++++-- scripts/check_affected_set.sh | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 7bc1e77836..8a6b5d72fa 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -23,8 +23,9 @@ set -euo pipefail # CORE = ALL on touch. apr-cli is here because other crates' tests read its sources (aprender-core readme_contract.rs reads # apr-cli/src/commands_enum.rs; spec_checklist_*, toyota_principles_tests). Reader edges are not in cargo metadata, so a -# crate whose source another crate reads at run time must be listed. -CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli}" +# crate whose source another crate reads at run time must be listed (aprender-quant: aprender-serve/capability.rs reads +# aprender-quant/src/ggml_type.rs). Any crates/*/build.rs is ALL: contracts/build_helper.rs reads crates/*/build.rs. +CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli aprender-quant}" filter=0 from="" while [ $# -gt 0 ]; do case "$1" in @@ -49,6 +50,7 @@ touched="" while IFS= read -r f; do case "$f" in '') continue ;; + crates/*/build.rs) emit ALL; exit 0 ;; crates/*.rs) ;; crates/*) emit ALL; exit 0 ;; esac diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index fe72595799..8c29b21200 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -11,7 +11,7 @@ row() { # name want got if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1"; echo " want: $2" | head -c 300; echo; echo " got: $3" | head -c 300; echo; fail=1; fi } run() { printf '%s\n' "$@" >"$t/d"; $A --diff-from "$t/d"; } -core_list=" aprender-core aprender-compute aprender-contracts apr-cli " +core_list=" aprender-core aprender-compute aprender-contracts apr-cli aprender-quant " meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) members=$(jq -r '.packages[].name' <<<"$meta" | sort) # a non-core crate that has dependents, and a leaf with none @@ -31,6 +31,8 @@ want=$(cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix no row "crate with dependents ($wp) -> itself + dependents" "$want" "$(run "$wd/src/lib.rs" | sort -u)" row "leaf crate ($lp) -> itself only" "$lp" "$(run "$ld/src/lib.rs")" row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" +row "aprender-quant source -> ALL (aprender-serve capability.rs reads aprender-quant/src/ggml_type.rs)" ALL "$(run crates/aprender-quant/src/ggml_type.rs)" +row "a crate's build.rs -> ALL (contracts build_helper.rs reads crates/*/build.rs)" ALL "$(run "$ld/build.rs")" row "apr-cli source -> ALL (aprender-core tests read apr-cli/src/commands_enum.rs)" ALL "$(run crates/apr-cli/src/commands_enum.rs)" row "--diff-from with .. is refused (SEC010)" "2" "$($A --diff-from ../x >/dev/null 2>&1; echo $?)" row "Cargo.lock -> ALL" ALL "$(run Cargo.lock)" @@ -81,6 +83,9 @@ c" "$(mv_run "$abs")" sed 's/--name-only --no-renames/--name-only/' "$A" >"$t/mut-rn.sh" if cmp -s "$A" "$t/mut-rn.sh"; then echo "FAIL mutant did not apply"; fail=1 else row "MUTANT: letting git detect renames is caught by the rename row" "c" "$(mv_run "$t/mut-rn.sh")"; fi +sed '/^ crates\/\*\/build.rs)/d' "$A" >"$t/mut-build.sh" +if cmp -s "$A" "$t/mut-build.sh"; then echo "FAIL build.rs mutant did not apply"; fail=1 +else printf '%s\n' "$ld/build.rs" >"$t/db"; row "MUTANT: dropping the build.rs arm is caught by the build.rs row" "$lp" "$(bash "$t/mut-build.sh" --diff-from "$t/db" | head -n1 || true)"; fi # mutants: the skip list that used to exist (docs/*, *.md) must be caught by the ALL rows above. sed 's#^ crates/\*.rs) ;;# docs/* | *.md) continue ;;\n crates/*.rs) ;;#' "$A" >"$t/mut-docs.sh" if cmp -s "$A" "$t/mut-docs.sh"; then echo "FAIL docs-skip mutant did not apply"; fail=1 From 2308a7c219e4026dc8d9905491fe5c0869c81fde Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 20:38:56 +0200 Subject: [PATCH 09/16] fix(affected-set): every non-ALL set carries aprender-core; empty touched or jq failure prints ALL; rows + RED mutants Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 12 +++++--- scripts/check_affected_set.sh | 55 ++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 8a6b5d72fa..e61424cba6 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -45,7 +45,8 @@ emit() { # $@ = package names, or ALL if [ -n "${AFFECTED_META:-}" ]; then meta="$(cat "$AFFECTED_META")" else meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)"; fi root="${AFFECTED_ROOT:-$(git rev-parse --show-toplevel)}" -dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" +dirs="$(jq -r --arg root "$root/" '.packages[] | [.name, (.manifest_path | sub($root;"") | sub("/?Cargo.toml$";""))] | @tsv' <<<"$meta")" || { emit ALL; exit 0; } +[ -n "$dirs" ] || { emit ALL; exit 0; } touched="" while IFS= read -r f; do case "$f" in @@ -61,10 +62,13 @@ while IFS= read -r f; do touched="$touched"$'\n'"$pkg" done <<<"$files" touched="$(printf '%s\n' "$touched" | grep -v '^$' | sort -u || true)" -[ -n "$touched" ] || exit 0 -mapfile -t out < <(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" ' +[ -n "$touched" ] || { emit ALL; exit 0; } # nothing placed in a package: nothing may be skipped +# The closure, then aprender-core: its falsification_spec_v10 test scans every crate's src, so ANY non-ALL set includes it. +res="$(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" ' (.packages | map({name, deps: [.dependencies[].name]})) as $p | def grow($s): ($s + [$p|.[] | select(any(.deps[]; . as $d | $s | index($d))) | .name] | unique); (.packages | map(.name)) as $m - | ($t | until(grow(.) == .; grow(.))) | .[] | select(. as $n | $m | index($n))' <<<"$meta") + | (($t | until(grow(.) == .; grow(.))) + ["aprender-core"] | unique) | .[] | select(. as $n | $m | index($n))' <<<"$meta")" || { emit ALL; exit 0; } +[ -n "$res" ] || { emit ALL; exit 0; } +mapfile -t out <<<"$res" emit "${out[@]}" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 8c29b21200..63e60d19d3 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -27,9 +27,11 @@ for p in $members; do done [ -n "$withdep" ] && [ -n "$leaf" ] || { echo "FAIL could not find a leaf and a crate with dependents"; exit 1; } wp=${withdep%%:*}; wd=${withdep#*:}; lp=${leaf%%:*}; ld=${leaf#*:} -want=$(cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//' | sort -u) -row "crate with dependents ($wp) -> itself + dependents" "$want" "$(run "$wd/src/lib.rs" | sort -u)" -row "leaf crate ($lp) -> itself only" "$lp" "$(run "$ld/src/lib.rs")" +# every non-ALL set also carries aprender-core (its falsification_spec_v10 test scans every crate's src) +want=$( { cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//'; echo aprender-core; } | sort -u) +lc="$(printf '%s\n' "$lp" aprender-core | sort -u)" +row "crate with dependents ($wp) -> itself + dependents + aprender-core" "$want" "$(run "$wd/src/lib.rs" | sort -u)" +row "leaf crate ($lp) -> itself + aprender-core" "$lc" "$(run "$ld/src/lib.rs" | sort -u)" row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" row "aprender-quant source -> ALL (aprender-serve capability.rs reads aprender-quant/src/ggml_type.rs)" ALL "$(run crates/aprender-quant/src/ggml_type.rs)" row "a crate's build.rs -> ALL (contracts build_helper.rs reads crates/*/build.rs)" ALL "$(run "$ld/build.rs")" @@ -46,8 +48,8 @@ row "ci/explicit-test-commands.d fragment -> ALL" ALL "$(run ci/explicit-test-co row "a crate's own Cargo.toml -> ALL" ALL "$(run "$ld/Cargo.toml")" row "a .md inside a leaf crate -> ALL (aprender-core reads crates/*/README.md)" ALL "$(run "$ld/README.md")" row "a .json inside a leaf crate -> ALL (kernel-registry.json is read by aprender-contracts)" ALL "$(run "$ld/kernel-registry.json")" -row "a .rs inside a leaf crate stays scoped" "$lp" "$(run "$ld/src/other.rs")" -row "--filter names the package" "package($lp)" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" +row "a .rs inside a leaf crate stays scoped (itself + aprender-core)" "$lc" "$(run "$ld/src/other.rs" | sort -u)" +row "--filter names the package and aprender-core" "$(printf 'package(%s)\n' $lc | paste -sd'|' | sed 's/|/ | /g')" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" # empty diff and a bad base are ALL (nothing measured, nothing skipped) : >"$t/empty" @@ -85,12 +87,49 @@ if cmp -s "$A" "$t/mut-rn.sh"; then echo "FAIL mutant did not apply"; fail=1 else row "MUTANT: letting git detect renames is caught by the rename row" "c" "$(mv_run "$t/mut-rn.sh")"; fi sed '/^ crates\/\*\/build.rs)/d' "$A" >"$t/mut-build.sh" if cmp -s "$A" "$t/mut-build.sh"; then echo "FAIL build.rs mutant did not apply"; fail=1 -else printf '%s\n' "$ld/build.rs" >"$t/db"; row "MUTANT: dropping the build.rs arm is caught by the build.rs row" "$lp" "$(bash "$t/mut-build.sh" --diff-from "$t/db" | head -n1 || true)"; fi +else printf '%s\n' "$ld/build.rs" >"$t/db"; row "MUTANT: dropping the build.rs arm is caught by the build.rs row" "$lc" "$(bash "$t/mut-build.sh" --diff-from "$t/db" | sort -u || true)"; fi # mutants: the skip list that used to exist (docs/*, *.md) must be caught by the ALL rows above. sed 's#^ crates/\*.rs) ;;# docs/* | *.md) continue ;;\n crates/*.rs) ;;#' "$A" >"$t/mut-docs.sh" if cmp -s "$A" "$t/mut-docs.sh"; then echo "FAIL docs-skip mutant did not apply"; fail=1 -else printf '%s\n' docs/x.md >"$t/dd"; row "MUTANT: skipping docs/*.md is caught by the docs-ALL row" "" "$(bash "$t/mut-docs.sh" --diff-from "$t/dd" | head -n1 || true)"; fi +else printf '%s\n%s\n' docs/x.md "$ld/src/lib.rs" >"$t/dd"; row "MUTANT: skipping docs/*.md is caught by the docs-ALL row" "$lc" "$(bash "$t/mut-docs.sh" --diff-from "$t/dd" | sort -u || true)"; fi sed 's#^ crates/\*) emit ALL.*# crates/*) continue ;;#' "$A" >"$t/mut-toml.sh" if cmp -s "$A" "$t/mut-toml.sh"; then echo "FAIL manifest mutant did not apply"; fail=1 -else printf '%s\n' "$ld/Cargo.toml" >"$t/dt"; row "MUTANT: skipping non-.rs crate files is caught by the manifest row" "" "$(bash "$t/mut-toml.sh" --diff-from "$t/dt" | head -n1 || true)"; fi +else printf '%s\n%s\n' "$ld/Cargo.toml" "$ld/src/lib.rs" >"$t/dt"; row "MUTANT: skipping non-.rs crate files is caught by the manifest row" "$lc" "$(bash "$t/mut-toml.sh" --diff-from "$t/dt" | sort -u || true)"; fi +# CORE crates: each must give ALL, and dropping each from CORE must be seen (its dependents alone are not the workspace) +for c in aprender-compute aprender-contracts aprender-quant; do + cd_=$(jq -r --arg p "$c" '.packages[]|select(.name==$p)|.manifest_path' <<<"$meta" | sed "s|$(git rev-parse --show-toplevel)/||; s|/Cargo.toml||") + row "CORE crate $c -> ALL" ALL "$(run "$cd_/src/lib.rs")" + sed "s/ $c//" "$A" >"$t/mut-core.sh" + if cmp -s "$A" "$t/mut-core.sh"; then echo "FAIL core mutant ($c) did not apply"; fail=1 + else printf '%s\n' "$cd_/src/lib.rs" >"$t/dcore"; row "MUTANT: dropping $c from CORE is caught" "not-ALL" "$([ "$(bash "$t/mut-core.sh" --diff-from "$t/dcore" | head -n1)" != ALL ] && echo not-ALL || echo ALL)"; fi +done +# a 2-hop chain (c -> b -> a) and the multi-package --filter format, on a synthetic graph +cat >"$t/chain.json" <<'JSON' +{"packages":[ + {"name":"a","manifest_path":"/r/crates/a/Cargo.toml","dependencies":[]}, + {"name":"b","manifest_path":"/r/crates/b/Cargo.toml","dependencies":[{"name":"a","kind":null}]}, + {"name":"c","manifest_path":"/r/crates/c/Cargo.toml","dependencies":[{"name":"b","kind":null}]}, + {"name":"aprender-core","manifest_path":"/r/crates/core/Cargo.toml","dependencies":[]}]} +JSON +chain() { AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none bash "$1" --diff-from <(echo crates/a/src/lib.rs) | sort -u; } +row "2-hop chain a<-b<-c: touching a reaches b and c (+ aprender-core)" "a +aprender-core +b +c" "$(chain "$A")" +sed 's/until(grow(.) == .; grow(.))/grow(.)/' "$A" >"$t/mut-hop.sh" +if cmp -s "$A" "$t/mut-hop.sh"; then echo "FAIL 1-hop mutant did not apply"; fail=1 +else row "MUTANT: a 1-hop closure is caught by the 2-hop row" "a +aprender-core +b" "$(chain "$t/mut-hop.sh")"; fi +want_f="package(a) | package(aprender-core) | package(b) | package(c)" +row "multi-package --filter is ' | ' joined" "$want_f" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none bash "$A" --filter --diff-from <(echo crates/a/src/lib.rs))" +sed "s/ | sed 's\/|\/ | \/g'//" "$A" >"$t/mut-fmt.sh" +if cmp -s "$A" "$t/mut-fmt.sh"; then echo "FAIL filter-format mutant did not apply"; fail=1 +else row "MUTANT: an unspaced multi-package filter is caught" "package(a)|package(aprender-core)|package(b)|package(c)" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none bash "$t/mut-fmt.sh" --filter --diff-from <(echo crates/a/src/lib.rs))"; fi +# an unreadable cargo metadata is ALL, not an empty answer +echo 'not json' >"$t/bad.json" +row "unreadable metadata -> ALL" ALL "$(AFFECTED_META="$t/bad.json" bash "$A" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null)" +sed 's/ || { emit ALL; exit 0; }$//' "$A" >"$t/mut-jq.sh" +if cmp -s "$A" "$t/mut-jq.sh"; then echo "FAIL jq-failure mutant did not apply"; fail=1 +else row "MUTANT: dropping the jq-failure fallback is caught" "" "$(AFFECTED_META="$t/bad.json" bash "$t/mut-jq.sh" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null || true)"; fi exit "$fail" From 29b1f3e70addaf9f7995c905d971a02fe33278cf Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 20:54:27 +0200 Subject: [PATCH 10/16] fix(affected-set): READERS list (crates whose tests read other crates) in every non-ALL set; readers guard; res= fallback row Co-Authored-By: Claude Sonnet 5.5 --- ci/sections.yml | 2 ++ scripts/affected_set.sh | 9 +++++--- scripts/check_affected_readers.sh | 37 +++++++++++++++++++++++++++++++ scripts/check_affected_set.sh | 24 +++++++++++++++----- 4 files changed, 64 insertions(+), 8 deletions(-) create mode 100755 scripts/check_affected_readers.sh diff --git a/ci/sections.yml b/ci/sections.yml index cefb8919a2..ded7307c99 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -1939,6 +1939,8 @@ jobs: # FLOW-010 E5: runs cargo metadata/tree, so it lives in the cargo job, not guard-tree-steps. - name: "Affected-set tool case table (FLOW-010 E5, RED rows and mutants)" run: setsid --wait bash scripts/check_affected_set.sh + - name: "Affected-set readers guard (every crate that reads other crates' files is listed; RED on a new one)" + run: setsid --wait bash scripts/check_affected_readers.sh && bash scripts/check_affected_readers.sh --self-test # #2640: the SAME defect class as the two steps above, one level up. The # dogfood release runner existed as two 1170-line copies whose divergence # was, in every hunk, a pin for a release verifier that one copy had and diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index e61424cba6..f9cdf4f9ba 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -26,6 +26,9 @@ set -euo pipefail # crate whose source another crate reads at run time must be listed (aprender-quant: aprender-serve/capability.rs reads # aprender-quant/src/ggml_type.rs). Any crates/*/build.rs is ALL: contracts/build_helper.rs reads crates/*/build.rs. CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli aprender-quant}" +# READERS = crates whose tests read OTHER crates' files at run time (CARGO_MANIFEST_DIR/.., crates/, ../). CORE ones are ALL on touch, but that does not put them in a non-ALL set; every non-ALL set includes them, because the touched crate may be what they read. scripts/check_affected_readers.sh +# goes RED on a reader this list (or CORE) lacks. +READERS="${AFFECTED_READERS:-aprender-core aprender-contracts apr-cli aprender-serve aprender-test-lib aprender-orchestrate aprender-present-yaml aprender-decide aprender-cgp aprender-review-experiment aprender-qa-certify}" filter=0 from="" while [ $# -gt 0 ]; do case "$1" in @@ -63,12 +66,12 @@ while IFS= read -r f; do done <<<"$files" touched="$(printf '%s\n' "$touched" | grep -v '^$' | sort -u || true)" [ -n "$touched" ] || { emit ALL; exit 0; } # nothing placed in a package: nothing may be skipped -# The closure, then aprender-core: its falsification_spec_v10 test scans every crate's src, so ANY non-ALL set includes it. -res="$(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" ' +# The closure, then aprender-core: its falsification_spec_v10 test scans every crate's src, so ANY non-ALL set includes it (and the READERS). +res="$(jq -r --argjson t "$(printf '%s\n' "$touched" | jq -R . | jq -s .)" --arg rd "$READERS" ' (.packages | map({name, deps: [.dependencies[].name]})) as $p | def grow($s): ($s + [$p|.[] | select(any(.deps[]; . as $d | $s | index($d))) | .name] | unique); (.packages | map(.name)) as $m - | (($t | until(grow(.) == .; grow(.))) + ["aprender-core"] | unique) | .[] | select(. as $n | $m | index($n))' <<<"$meta")" || { emit ALL; exit 0; } + | (($t | until(grow(.) == .; grow(.))) + ($rd | split(" ")) | unique) | .[] | select(. as $n | $m | index($n))' <<<"$meta")" || { emit ALL; exit 0; } [ -n "$res" ] || { emit ALL; exit 0; } mapfile -t out <<<"$res" emit "${out[@]}" diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh new file mode 100755 index 0000000000..5950af023f --- /dev/null +++ b/scripts/check_affected_readers.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# FLOW-010 E5: a crate whose Rust reads OTHER crates' files at run time (join("crates"), ..//.., +# CARGO_MANIFEST_DIR/../..) is a reader edge that cargo metadata cannot see. scripts/affected_set.sh must +# list it in CORE or READERS, or a change to the crate it reads would skip its tests. RED on a reader +# neither list names. Usage: check_affected_readers.sh [crates-dir] (self-test: --self-test) +set -euo pipefail +dir="${1:-crates}" +SET="${AFFECTED_SET_SH:-scripts/affected_set.sh}" +listed() { sed -n 's/^CORE="${AFFECTED_CORE:-\(.*\)}"$/\1/p; s/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } +# reader patterns; lines about target/, .config, contracts/, docs/ read non-crate paths (an unknown root path is ALL anyway) +readers() { + grep -rEn --include='*.rs' 'join\("crates"\)|\.\./aprender-|MANIFEST_DIR.*\.\.|join\("\.\./\.\."\)' "$1" 2>/dev/null \ + | grep -vE 'target|\.config|contracts|docs/|^\S+:[0-9]+:\s*//' | awk -F/ '{print $2}' | sort -u || true +} +# aprender-bench-tokenizer joins ../.. only to find tokenizer.json / models/ at the repo root, never another crate's file +ALLOW=" aprender-bench-tokenizer " +check() { + local bad="" l; l="$(listed)" + for c in $(readers "$1"); do + case "$ALLOW" in *" $c "*) continue ;; esac + printf '%s\n' "$l" | grep -qx "$c" || bad="$bad $c" + done + if [ -n "$bad" ]; then echo "RED: crates read other crates' files but are in neither CORE nor READERS (scripts/affected_set.sh):$bad"; return 1; fi + echo "ok: every cross-crate reader is listed" +} +if [ "${1:-}" = "--self-test" ]; then + t=$(mktemp -d); trap 'rm -rf -- "${t:?}"' EXIT + mkdir -p "$t/crates/zz-new/tests" "$t/crates/aprender-serve/src" + echo 'fn f() { let _ = root.join("crates"); }' >"$t/crates/zz-new/tests/x.rs" + echo 'fn f() { let _ = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); }' >"$t/crates/aprender-serve/src/y.rs" + cd "$t"; SET="${OLDPWD}/${SET}" + if (check crates >/dev/null 2>&1); then echo "FAIL self-test: an unlisted reader was not caught"; exit 1; fi + rm -r crates/zz-new + (check crates >/dev/null 2>&1) || { echo "FAIL self-test: a listed reader was flagged"; exit 1; } + echo "ok: self-test"; exit 0 +fi +check "$dir" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 63e60d19d3..132832da63 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -12,6 +12,7 @@ row() { # name want got } run() { printf '%s\n' "$@" >"$t/d"; $A --diff-from "$t/d"; } core_list=" aprender-core aprender-compute aprender-contracts apr-cli aprender-quant " +readers="aprender-core aprender-contracts apr-cli aprender-serve aprender-test-lib aprender-orchestrate aprender-present-yaml aprender-decide aprender-cgp aprender-review-experiment aprender-qa-certify" meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) members=$(jq -r '.packages[].name' <<<"$meta" | sort) # a non-core crate that has dependents, and a leaf with none @@ -28,8 +29,8 @@ done [ -n "$withdep" ] && [ -n "$leaf" ] || { echo "FAIL could not find a leaf and a crate with dependents"; exit 1; } wp=${withdep%%:*}; wd=${withdep#*:}; lp=${leaf%%:*}; ld=${leaf#*:} # every non-ALL set also carries aprender-core (its falsification_spec_v10 test scans every crate's src) -want=$( { cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//'; echo aprender-core; } | sort -u) -lc="$(printf '%s\n' "$lp" aprender-core | sort -u)" +want=$( { cargo tree --offline -i "$wp" --workspace -e normal,dev,build --prefix none 2>/dev/null | sed 's/ v.*//'; printf '%s\n' $readers; } | sort -u) +lc="$(printf '%s\n' "$lp" $readers | sort -u)" row "crate with dependents ($wp) -> itself + dependents + aprender-core" "$want" "$(run "$wd/src/lib.rs" | sort -u)" row "leaf crate ($lp) -> itself + aprender-core" "$lc" "$(run "$ld/src/lib.rs" | sort -u)" row "core crate -> ALL" ALL "$(run crates/aprender-core/src/lib.rs)" @@ -111,7 +112,7 @@ cat >"$t/chain.json" <<'JSON' {"name":"c","manifest_path":"/r/crates/c/Cargo.toml","dependencies":[{"name":"b","kind":null}]}, {"name":"aprender-core","manifest_path":"/r/crates/core/Cargo.toml","dependencies":[]}]} JSON -chain() { AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none bash "$1" --diff-from <(echo crates/a/src/lib.rs) | sort -u; } +chain() { AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none AFFECTED_READERS=aprender-core bash "$1" --diff-from <(echo crates/a/src/lib.rs) | sort -u; } row "2-hop chain a<-b<-c: touching a reaches b and c (+ aprender-core)" "a aprender-core b @@ -122,14 +123,27 @@ else row "MUTANT: a 1-hop closure is caught by the 2-hop row" "a aprender-core b" "$(chain "$t/mut-hop.sh")"; fi want_f="package(a) | package(aprender-core) | package(b) | package(c)" -row "multi-package --filter is ' | ' joined" "$want_f" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none bash "$A" --filter --diff-from <(echo crates/a/src/lib.rs))" +row "multi-package --filter is ' | ' joined" "$want_f" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none AFFECTED_READERS=aprender-core bash "$A" --filter --diff-from <(echo crates/a/src/lib.rs))" sed "s/ | sed 's\/|\/ | \/g'//" "$A" >"$t/mut-fmt.sh" if cmp -s "$A" "$t/mut-fmt.sh"; then echo "FAIL filter-format mutant did not apply"; fail=1 -else row "MUTANT: an unspaced multi-package filter is caught" "package(a)|package(aprender-core)|package(b)|package(c)" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none bash "$t/mut-fmt.sh" --filter --diff-from <(echo crates/a/src/lib.rs))"; fi +else row "MUTANT: an unspaced multi-package filter is caught" "package(a)|package(aprender-core)|package(b)|package(c)" "$(AFFECTED_META="$t/chain.json" AFFECTED_ROOT=/r AFFECTED_CORE=none AFFECTED_READERS=aprender-core bash "$t/mut-fmt.sh" --filter --diff-from <(echo crates/a/src/lib.rs))"; fi # an unreadable cargo metadata is ALL, not an empty answer echo 'not json' >"$t/bad.json" row "unreadable metadata -> ALL" ALL "$(AFFECTED_META="$t/bad.json" bash "$A" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null)" sed 's/ || { emit ALL; exit 0; }$//' "$A" >"$t/mut-jq.sh" if cmp -s "$A" "$t/mut-jq.sh"; then echo "FAIL jq-failure mutant did not apply"; fail=1 else row "MUTANT: dropping the jq-failure fallback is caught" "" "$(AFFECTED_META="$t/bad.json" bash "$t/mut-jq.sh" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null || true)"; fi +# READERS: each reader is in every non-ALL set (real tree), and dropping the READERS list is caught +for r in $readers; do + case " $(run "$ld/src/lib.rs" | tr '\n' ' ')" in *" $r "*) echo "ok reader $r is in a non-ALL set" ;; *) echo "FAIL reader $r missing from a non-ALL set"; fail=1 ;; esac +done +sed 's#^READERS=.*#READERS="aprender-core"#' "$A" >"$t/mut-readers.sh" +if cmp -s "$A" "$t/mut-readers.sh"; then echo "FAIL readers mutant did not apply"; fail=1 +else row "MUTANT: an emptied READERS list is caught" "$(printf '%s\n' aprender-core "$lp" | sort -u)" "$(printf '%s\n' "$ld/src/lib.rs" >"$t/drd"; bash "$t/mut-readers.sh" --diff-from "$t/drd" | sort -u)"; fi +# the res= jq fallback ALONE: metadata with no `dependencies` key makes that jq fail -> ALL +echo '{"packages":[{"name":"a","manifest_path":"/r/crates/a/Cargo.toml"}]}' >"$t/nodeps.json" +row "no dependencies key -> ALL (res= fallback)" ALL "$(AFFECTED_META="$t/nodeps.json" AFFECTED_ROOT=/r bash "$A" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null)" +sed '/<<<"$meta")" || { emit ALL; exit 0; }$/s/ || { emit ALL; exit 0; }$//' "$A" >"$t/mut-res.sh" +if cmp -s "$A" "$t/mut-res.sh"; then echo "FAIL res-fallback mutant did not apply"; fail=1 +else row "MUTANT: dropping only the res= fallback is caught" "" "$(AFFECTED_META="$t/nodeps.json" AFFECTED_ROOT=/r bash "$t/mut-res.sh" --diff-from <(echo crates/a/src/lib.rs) 2>/dev/null || true)"; fi exit "$fail" From 99fc513412241508a98dff31e6af9460c8ec981a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 21:02:23 +0200 Subject: [PATCH 11/16] fix(affected-set): readers guard over-approximates path escapes; READERS is the only list; file:line allow; one case per w5404 probe Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 2 +- scripts/check_affected_readers.sh | 82 ++++++++++++++++++++----------- scripts/check_affected_set.sh | 4 +- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index f9cdf4f9ba..8f069414c3 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -28,7 +28,7 @@ set -euo pipefail CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli aprender-quant}" # READERS = crates whose tests read OTHER crates' files at run time (CARGO_MANIFEST_DIR/.., crates/, ../). CORE ones are ALL on touch, but that does not put them in a non-ALL set; every non-ALL set includes them, because the touched crate may be what they read. scripts/check_affected_readers.sh # goes RED on a reader this list (or CORE) lacks. -READERS="${AFFECTED_READERS:-aprender-core aprender-contracts apr-cli aprender-serve aprender-test-lib aprender-orchestrate aprender-present-yaml aprender-decide aprender-cgp aprender-review-experiment aprender-qa-certify}" +READERS="${AFFECTED_READERS:-apr-cli aprender-build-sha aprender-cbtop aprender-cgp aprender-ci-tools aprender-compute aprender-compute-xtask aprender-contracts aprender-contracts-cli aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-lib aprender-present-terminal aprender-present-yaml aprender-profile aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-distill aprender-train-inspect aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop facades}" filter=0 from="" while [ $# -gt 0 ]; do case "$1" in diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh index 5950af023f..4ae41274d5 100755 --- a/scripts/check_affected_readers.sh +++ b/scripts/check_affected_readers.sh @@ -1,37 +1,63 @@ #!/usr/bin/env bash -# FLOW-010 E5: a crate whose Rust reads OTHER crates' files at run time (join("crates"), ..//.., -# CARGO_MANIFEST_DIR/../..) is a reader edge that cargo metadata cannot see. scripts/affected_set.sh must -# list it in CORE or READERS, or a change to the crate it reads would skip its tests. RED on a reader -# neither list names. Usage: check_affected_readers.sh [crates-dir] (self-test: --self-test) +# FLOW-010 E5: a crate whose Rust can reach OTHER crates' files at run time is a reader edge that cargo metadata +# cannot see. scripts/affected_set.sh must list it in READERS (the only list that puts a crate in a non-ALL set), +# or a change to the crate it reads would skip its tests. RED on a crate with any path-escape form and no entry. +# Over-approximates on purpose: a false positive only adds a crate to READERS (more tests run); a miss skips tests. +# Flagged in any .rs: a string holding `..`, parent(), ancestors(), current_dir, a "crates/" string or "crates", +# CARGO_WORKSPACE_DIR, workspace_path_or_skip, read_dir(...) naming MANIFEST/crates/root/workspace, format!(...) +# naming MANIFEST_DIR or crates/. Comment-only lines and files under a target/ path component are skipped. +# Usage: check_affected_readers.sh [crates-dir] | --self-test | --list set -euo pipefail -dir="${1:-crates}" SET="${AFFECTED_SET_SH:-scripts/affected_set.sh}" -listed() { sed -n 's/^CORE="${AFFECTED_CORE:-\(.*\)}"$/\1/p; s/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } -# reader patterns; lines about target/, .config, contracts/, docs/ read non-crate paths (an unknown root path is ALL anyway) -readers() { - grep -rEn --include='*.rs' 'join\("crates"\)|\.\./aprender-|MANIFEST_DIR.*\.\.|join\("\.\./\.\."\)' "$1" 2>/dev/null \ - | grep -vE 'target|\.config|contracts|docs/|^\S+:[0-9]+:\s*//' | awk -F/ '{print $2}' | sort -u || true +# the exact file:line exemptions (a crate-wide exemption hides a later reader in the same crate) +ALLOW="${AFFECTED_READERS_ALLOW:-crates/aprender-bench-tokenizer/src/lib.rs:50}" # joins ../.. only for /tokenizer.json and /models/ +PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent\(\)|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' +listed() { sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } +hits() { # $1 = crates dir; prints crate names + { grep -rEn --include='*.rs' "$PAT" "$1" 2>/dev/null || true; } \ + | grep -vE '^[^:]*/target/' | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//' \ + | awk -F: -v allow=" $ALLOW " '{ k = $1 ":" $2; if (index(allow, " " k " ")) next; n = split($1, a, "/"); print a[2] }' | sort -u } -# aprender-bench-tokenizer joins ../.. only to find tokenizer.json / models/ at the repo root, never another crate's file -ALLOW=" aprender-bench-tokenizer " check() { - local bad="" l; l="$(listed)" - for c in $(readers "$1"); do - case "$ALLOW" in *" $c "*) continue ;; esac - printf '%s\n' "$l" | grep -qx "$c" || bad="$bad $c" - done - if [ -n "$bad" ]; then echo "RED: crates read other crates' files but are in neither CORE nor READERS (scripts/affected_set.sh):$bad"; return 1; fi - echo "ok: every cross-crate reader is listed" + local bad="" l c; l="$(listed)" + for c in $(hits "$1"); do printf '%s\n' "$l" | grep -qx "$c" || bad="$bad $c"; done + if [ -n "$bad" ]; then echo "RED: crates can reach other crates' files but are not in READERS (scripts/affected_set.sh):$bad"; return 1; fi + echo "ok: every crate with a path-escape form is in READERS" } +if [ "${1:-}" = "--list" ]; then hits crates; exit 0; fi if [ "${1:-}" = "--self-test" ]; then t=$(mktemp -d); trap 'rm -rf -- "${t:?}"' EXIT - mkdir -p "$t/crates/zz-new/tests" "$t/crates/aprender-serve/src" - echo 'fn f() { let _ = root.join("crates"); }' >"$t/crates/zz-new/tests/x.rs" - echo 'fn f() { let _ = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); }' >"$t/crates/aprender-serve/src/y.rs" - cd "$t"; SET="${OLDPWD}/${SET}" - if (check crates >/dev/null 2>&1); then echo "FAIL self-test: an unlisted reader was not caught"; exit 1; fi - rm -r crates/zz-new - (check crates >/dev/null 2>&1) || { echo "FAIL self-test: a listed reader was flagged"; exit 1; } - echo "ok: self-test"; exit 0 + SET="$(pwd)/$SET"; cd "$t" + fail=0 + fresh() { rm -rf -- "${t:?}/crates"; mkdir -p "$t/crates/$1/src"; } # one unlisted crate holding $2 + expect_red() { # name crate body + fresh "$2"; printf '%s\n' "fn f() { $3 }" >"$t/crates/$2/src/x.rs" + if (check crates >/dev/null 2>&1); then echo "FAIL self-test: not caught: $1"; fail=1; fi + } + expect_red 'join("crates")' zz-a 'let _ = root.join("crates");' + expect_red 'MANIFEST_DIR.parent().parent()' zz-b 'let _ = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent();' + expect_red 'root.join("crates/aprender-core")' zz-c 'let _ = root.join("crates/aprender-core");' + expect_red 'format! with ../../crates' zz-d 'let _ = format!("{}/../../crates/x", env!("CARGO_MANIFEST_DIR"));' + expect_red 'ancestors().nth(2)' zz-e 'let _ = here.ancestors().nth(2);' + expect_red 'read_dir("../")' zz-f 'let _ = std::fs::read_dir("../");' + expect_red 'current_dir().join("..")' zz-g 'let _ = std::env::current_dir().unwrap().join("..");' + expect_red 'relative read_to_string' zz-h 'let _ = std::fs::read_to_string("../../crates/x/src/lib.rs");' + expect_red 'MANIFEST_DIR.parent().join(apr-cli)' zz-i 'let _ = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("apr-cli");' + expect_red 'workspace_path_or_skip!' zz-j 'let _ = workspace_path_or_skip!("crates/x/..");' + expect_red 'read_dir(workspace_root)' zz-k 'let _ = std::fs::read_dir(workspace_root);' + expect_red 'CARGO_WORKSPACE_DIR' zz-l 'let _ = env!("CARGO_WORKSPACE_DIR");' + expect_red 'format! with MANIFEST_DIR' zz-m 'let _ = format!("{}/x", env!("CARGO_MANIFEST_DIR"));' + expect_red 'a *contracts*/*target* crate name' zz-contracts-target-new 'let _ = root.join("crates");' + expect_red 'a line that mentions target/contracts' zz-n 'let _ = root.join("crates"); /* target contracts docs/ */' + # the bench-tokenizer exemption is the one line, not the crate + fresh aprender-bench-tokenizer; printf 'fn f() {}\nfn g() { let _ = root.join("crates"); }\n' >"$t/crates/aprender-bench-tokenizer/src/lib.rs" + if (AFFECTED_READERS_ALLOW="crates/aprender-bench-tokenizer/src/lib.rs:1" check crates >/dev/null 2>&1); then echo "FAIL self-test: an allow entry exempted a different line"; fail=1; fi + # a listed reader, a comment-only mention and a target/ file are green + fresh aprender-serve; echo 'fn f() { let _ = root.join("crates"); }' >"$t/crates/aprender-serve/src/y.rs" + (check crates >/dev/null 2>&1) || { echo "FAIL self-test: a listed reader was flagged"; fail=1; } + fresh zz-o; echo '// root.join("crates")' >"$t/crates/zz-o/src/y.rs" + (check crates >/dev/null 2>&1) || { echo "FAIL self-test: a comment-only line was flagged"; fail=1; } + [ "$fail" -eq 0 ] && echo "ok: self-test" + exit "$fail" fi -check "$dir" +check "${1:-crates}" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 132832da63..8909af7794 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -12,9 +12,11 @@ row() { # name want got } run() { printf '%s\n' "$@" >"$t/d"; $A --diff-from "$t/d"; } core_list=" aprender-core aprender-compute aprender-contracts apr-cli aprender-quant " -readers="aprender-core aprender-contracts apr-cli aprender-serve aprender-test-lib aprender-orchestrate aprender-present-yaml aprender-decide aprender-cgp aprender-review-experiment aprender-qa-certify" +readers="$(sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$A")" meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) members=$(jq -r '.packages[].name' <<<"$meta" | sort) +# a READERS entry that is a directory and not a package (e.g. facades) is dropped by the tool; drop it from the oracle too +readers="$(for r in $readers; do if grep -qx "$r" <<<"$members"; then echo "$r"; fi; done)" # a non-core crate that has dependents, and a leaf with none withdep="" leaf="" for p in $members; do From 64602c82c77e56104ac46f23ca73fa63fd926d01 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Tue, 20 Oct 2026 21:03:53 +0200 Subject: [PATCH 12/16] fix(affected-set): oracle readers filter without a bashrs SC2135 false positive Co-Authored-By: Claude Sonnet 5.5 --- scripts/check_affected_set.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 8909af7794..a547491d75 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -16,7 +16,7 @@ readers="$(sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$A")" meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) members=$(jq -r '.packages[].name' <<<"$meta" | sort) # a READERS entry that is a directory and not a package (e.g. facades) is dropped by the tool; drop it from the oracle too -readers="$(for r in $readers; do if grep -qx "$r" <<<"$members"; then echo "$r"; fi; done)" +readers="$(printf '%s\n' $readers | grep -Fxf <(printf '%s\n' "$members") || true)" # a non-core crate that has dependents, and a leaf with none withdep="" leaf="" for p in $members; do From e823787feda680857fc239367d42604c5cb59761 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 10:27:23 +0200 Subject: [PATCH 13/16] fix(ci): affected-readers guard catches pop/push, ParentDir, show-toplevel, Path::parent; drop facades from READERS; prefix row (#5802) Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 2 +- scripts/check_affected_readers.sh | 12 ++++++++++-- scripts/check_affected_set.sh | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index 8f069414c3..e727242c1c 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -28,7 +28,7 @@ set -euo pipefail CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli aprender-quant}" # READERS = crates whose tests read OTHER crates' files at run time (CARGO_MANIFEST_DIR/.., crates/, ../). CORE ones are ALL on touch, but that does not put them in a non-ALL set; every non-ALL set includes them, because the touched crate may be what they read. scripts/check_affected_readers.sh # goes RED on a reader this list (or CORE) lacks. -READERS="${AFFECTED_READERS:-apr-cli aprender-build-sha aprender-cbtop aprender-cgp aprender-ci-tools aprender-compute aprender-compute-xtask aprender-contracts aprender-contracts-cli aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-lib aprender-present-terminal aprender-present-yaml aprender-profile aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-distill aprender-train-inspect aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop facades}" +READERS="${AFFECTED_READERS:-apr-cli aprender-build-sha aprender-cbtop aprender-cgp aprender-ci-tools aprender-compute aprender-compute-xtask aprender-contracts aprender-contracts-cli aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-lib aprender-present-terminal aprender-present-yaml aprender-profile aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-distill aprender-train-inspect aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop}" filter=0 from="" while [ $# -gt 0 ]; do case "$1" in diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh index 4ae41274d5..4c04ab3172 100755 --- a/scripts/check_affected_readers.sh +++ b/scripts/check_affected_readers.sh @@ -11,12 +11,14 @@ set -euo pipefail SET="${AFFECTED_SET_SH:-scripts/affected_set.sh}" # the exact file:line exemptions (a crate-wide exemption hides a later reader in the same crate) ALLOW="${AFFECTED_READERS_ALLOW:-crates/aprender-bench-tokenizer/src/lib.rs:50}" # joins ../.. only for /tokenizer.json and /models/ -PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent\(\)|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' +# crates/ dirs outside the workspace (excluded): a change there is unplaceable -> ALL, so they need no READERS entry +NONMEMBER="${AFFECTED_NONMEMBER:-facades}" +PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent[[:space:]]*\(|Path::parent\(|Component::ParentDir|show-toplevel|\.pop\(\);[^;]*\.push\(|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' listed() { sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } hits() { # $1 = crates dir; prints crate names { grep -rEn --include='*.rs' "$PAT" "$1" 2>/dev/null || true; } \ | grep -vE '^[^:]*/target/' | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//' \ - | awk -F: -v allow=" $ALLOW " '{ k = $1 ":" $2; if (index(allow, " " k " ")) next; n = split($1, a, "/"); print a[2] }' | sort -u + | awk -F: -v allow=" $ALLOW " -v nm=" $NONMEMBER " '{ k = $1 ":" $2; if (index(allow, " " k " ")) next; n = split($1, a, "/"); if (index(nm, " " a[2] " ")) next; print a[2] }' | sort -u } check() { local bad="" l c; l="$(listed)" @@ -49,6 +51,12 @@ if [ "${1:-}" = "--self-test" ]; then expect_red 'format! with MANIFEST_DIR' zz-m 'let _ = format!("{}/x", env!("CARGO_MANIFEST_DIR"));' expect_red 'a *contracts*/*target* crate name' zz-contracts-target-new 'let _ = root.join("crates");' expect_red 'a line that mentions target/contracts' zz-n 'let _ = root.join("crates"); /* target contracts docs/ */' + expect_red 'PathBuf pop then push' zz-p 'let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.pop(); p.push("aprender-core");' + expect_red 'Component::ParentDir' zz-q 'let _ = Component::ParentDir;' + expect_red 'git rev-parse --show-toplevel' zz-r 'let _ = Command::new("git").args(["rev-parse", "--show-toplevel"]);' + expect_red 'Path::parent(x)' zz-s 'let _ = Path::parent(&here);' + expect_red '.parent ()' zz-t 'let _ = here.parent ();' + expect_red 'a non-member dir is only exempt when named' facades-x 'let _ = here.ancestors();' # the bench-tokenizer exemption is the one line, not the crate fresh aprender-bench-tokenizer; printf 'fn f() {}\nfn g() { let _ = root.join("crates"); }\n' >"$t/crates/aprender-bench-tokenizer/src/lib.rs" if (AFFECTED_READERS_ALLOW="crates/aprender-bench-tokenizer/src/lib.rs:1" check crates >/dev/null 2>&1); then echo "FAIL self-test: an allow entry exempted a different line"; fail=1; fi diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index a547491d75..946f8afe12 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -15,7 +15,7 @@ core_list=" aprender-core aprender-compute aprender-contracts apr-cli aprender-q readers="$(sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$A")" meta=$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null) members=$(jq -r '.packages[].name' <<<"$meta" | sort) -# a READERS entry that is a directory and not a package (e.g. facades) is dropped by the tool; drop it from the oracle too +# a READERS entry that is not a workspace package is dropped by the tool; drop it from the oracle too readers="$(printf '%s\n' $readers | grep -Fxf <(printf '%s\n' "$members") || true)" # a non-core crate that has dependents, and a leaf with none withdep="" leaf="" @@ -42,6 +42,7 @@ row "apr-cli source -> ALL (aprender-core tests read apr-cli/src/commands_enum.r row "--diff-from with .. is refused (SEC010)" "2" "$($A --diff-from ../x >/dev/null 2>&1; echo $?)" row "Cargo.lock -> ALL" ALL "$(run Cargo.lock)" row "unknown path -> ALL" ALL "$(run some/unknown/path.txt)" +row "a path whose dir only prefixes a crate dir (crates/aprender-train-old/) -> ALL (awk prefix needs the trailing /)" ALL "$(run crates/aprender-train-old/src/lib.rs)" row "unknown path next to a leaf -> ALL" ALL "$(run "$ld/src/lib.rs" scripts/whatever.sh)" row "docs file -> ALL (tests read docs/ at run time)" ALL "$(run docs/x.md)" row "root README.md -> ALL" ALL "$(run README.md)" From 8dcadf514f1925d5d3973e329792acb441705f39 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 10:41:05 +0200 Subject: [PATCH 14/16] fix(ci): readers guard catches rustfmt-split PathBuf pop/push; fails if a NONMEMBER dir is a workspace member (#5802) Co-Authored-By: Claude Sonnet 5.5 --- scripts/check_affected_readers.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh index 4c04ab3172..4a0b13b7ae 100755 --- a/scripts/check_affected_readers.sh +++ b/scripts/check_affected_readers.sh @@ -13,14 +13,23 @@ SET="${AFFECTED_SET_SH:-scripts/affected_set.sh}" ALLOW="${AFFECTED_READERS_ALLOW:-crates/aprender-bench-tokenizer/src/lib.rs:50}" # joins ../.. only for /tokenizer.json and /models/ # crates/ dirs outside the workspace (excluded): a change there is unplaceable -> ALL, so they need no READERS entry NONMEMBER="${AFFECTED_NONMEMBER:-facades}" -PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent[[:space:]]*\(|Path::parent\(|Component::ParentDir|show-toplevel|\.pop\(\);[^;]*\.push\(|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' +PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent[[:space:]]*\(|Path::parent\(|Component::ParentDir|show-toplevel|\.pop\(\);[^;]*\.push\(|let mut [A-Za-z_0-9]+[^=]*= *(std::path::)?PathBuf::from\(env!\("CARGO_MANIFEST_DIR"\)\)|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' listed() { sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } hits() { # $1 = crates dir; prints crate names { grep -rEn --include='*.rs' "$PAT" "$1" 2>/dev/null || true; } \ | grep -vE '^[^:]*/target/' | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//' \ | awk -F: -v allow=" $ALLOW " -v nm=" $NONMEMBER " '{ k = $1 ":" $2; if (index(allow, " " k " ")) next; n = split($1, a, "/"); if (index(nm, " " a[2] " ")) next; print a[2] }' | sort -u } +nonmember_in_workspace() { # a NONMEMBER dir that cargo metadata lists is a member: its exemption would hide a reader + local m d + m="$({ if [ -n "${AFFECTED_META_JSON:-}" ]; then cat "$AFFECTED_META_JSON"; else cargo metadata --no-deps --format-version 1 --offline 2>/dev/null; fi; } || true)" + [ -n "$m" ] || return 0 + for d in $NONMEMBER; do + if printf '%s' "$m" | grep -qF "/crates/$d/"; then echo "RED: NONMEMBER dir crates/$d is a workspace member (cargo metadata lists it): drop it from NONMEMBER"; return 1; fi + done +} check() { + nonmember_in_workspace || return 1 local bad="" l c; l="$(listed)" for c in $(hits "$1"); do printf '%s\n' "$l" | grep -qx "$c" || bad="$bad $c"; done if [ -n "$bad" ]; then echo "RED: crates can reach other crates' files but are not in READERS (scripts/affected_set.sh):$bad"; return 1; fi @@ -57,6 +66,9 @@ if [ "${1:-}" = "--self-test" ]; then expect_red 'Path::parent(x)' zz-s 'let _ = Path::parent(&here);' expect_red '.parent ()' zz-t 'let _ = here.parent ();' expect_red 'a non-member dir is only exempt when named' facades-x 'let _ = here.ancestors();' + expect_red 'rustfmt-split PathBuf pop/push' zz-u $'let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n p.pop();\n p.push("aprender-core");' + printf '{"packages":[{"manifest_path":"/x/crates/facades/zz-evil/Cargo.toml"}]}' >"$t/meta.json"; fresh zz-v + if (AFFECTED_META_JSON="$t/meta.json" check crates >/dev/null 2>&1); then echo "FAIL self-test: a NONMEMBER dir listed by cargo metadata passed"; fail=1; fi # the bench-tokenizer exemption is the one line, not the crate fresh aprender-bench-tokenizer; printf 'fn f() {}\nfn g() { let _ = root.join("crates"); }\n' >"$t/crates/aprender-bench-tokenizer/src/lib.rs" if (AFFECTED_READERS_ALLOW="crates/aprender-bench-tokenizer/src/lib.rs:1" check crates >/dev/null 2>&1); then echo "FAIL self-test: an allow entry exempted a different line"; fail=1; fi From 29d1072c568cd282a21292dd49086d0e2e56fbd2 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 11:12:35 +0200 Subject: [PATCH 15/16] fix(ci): readers guard: file-level CARGO_MANIFEST_DIR + .pop( rule replaces the let-mut pattern; apr-format listed (false positive adds tests); filter row sorts in C (#5802) Co-Authored-By: Claude Sonnet 5.5 --- scripts/affected_set.sh | 2 +- scripts/check_affected_readers.sh | 10 +++++++--- scripts/check_affected_set.sh | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh index e727242c1c..4604dbec14 100755 --- a/scripts/affected_set.sh +++ b/scripts/affected_set.sh @@ -28,7 +28,7 @@ set -euo pipefail CORE="${AFFECTED_CORE:-aprender-core aprender-compute aprender-contracts apr-cli aprender-quant}" # READERS = crates whose tests read OTHER crates' files at run time (CARGO_MANIFEST_DIR/.., crates/, ../). CORE ones are ALL on touch, but that does not put them in a non-ALL set; every non-ALL set includes them, because the touched crate may be what they read. scripts/check_affected_readers.sh # goes RED on a reader this list (or CORE) lacks. -READERS="${AFFECTED_READERS:-apr-cli aprender-build-sha aprender-cbtop aprender-cgp aprender-ci-tools aprender-compute aprender-compute-xtask aprender-contracts aprender-contracts-cli aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-lib aprender-present-terminal aprender-present-yaml aprender-profile aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-distill aprender-train-inspect aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop}" +READERS="${AFFECTED_READERS:-apr-cli apr-format aprender-build-sha aprender-cbtop aprender-cgp aprender-ci-tools aprender-compute aprender-compute-xtask aprender-contracts aprender-contracts-cli aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-lib aprender-present-terminal aprender-present-yaml aprender-profile aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-distill aprender-train-inspect aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop}" filter=0 from="" while [ $# -gt 0 ]; do case "$1" in diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh index 4a0b13b7ae..5cd3d9ae66 100755 --- a/scripts/check_affected_readers.sh +++ b/scripts/check_affected_readers.sh @@ -13,10 +13,14 @@ SET="${AFFECTED_SET_SH:-scripts/affected_set.sh}" ALLOW="${AFFECTED_READERS_ALLOW:-crates/aprender-bench-tokenizer/src/lib.rs:50}" # joins ../.. only for /tokenizer.json and /models/ # crates/ dirs outside the workspace (excluded): a change there is unplaceable -> ALL, so they need no READERS entry NONMEMBER="${AFFECTED_NONMEMBER:-facades}" -PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent[[:space:]]*\(|Path::parent\(|Component::ParentDir|show-toplevel|\.pop\(\);[^;]*\.push\(|let mut [A-Za-z_0-9]+[^=]*= *(std::path::)?PathBuf::from\(env!\("CARGO_MANIFEST_DIR"\)\)|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' +PAT='"\.\."|"[^"]*(\.\./|/\.\.)[^"]*"|\bparent[[:space:]]*\(|Path::parent\(|Component::ParentDir|show-toplevel|\.pop\(\);[^;]*\.push\(|ancestors\(\)|current_dir|"[^"]*crates/|"crates"|CARGO_WORKSPACE_DIR|workspace_path_or_skip|read_dir\([^)]*(MANIFEST|crates|root|workspace)|format!\([^)]*(MANIFEST_DIR|crates/)' listed() { sed -n 's/^READERS="${AFFECTED_READERS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } hits() { # $1 = crates dir; prints crate names - { grep -rEn --include='*.rs' "$PAT" "$1" 2>/dev/null || true; } \ + { grep -rEn --include='*.rs' "$PAT" "$1" 2>/dev/null || true + # file-level: CARGO_MANIFEST_DIR and .pop( anywhere in one file (rustfmt may wrap the let or split pop/push across lines) + for f in $(grep -rlE --include='*.rs' 'CARGO_MANIFEST_DIR' "$1" 2>/dev/null || true); do + grep -qE '\.pop[[:space:]]*\(' "$f" && echo "$f:0:pop-with-MANIFEST_DIR" + done; } \ | grep -vE '^[^:]*/target/' | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//' \ | awk -F: -v allow=" $ALLOW " -v nm=" $NONMEMBER " '{ k = $1 ":" $2; if (index(allow, " " k " ")) next; n = split($1, a, "/"); if (index(nm, " " a[2] " ")) next; print a[2] }' | sort -u } @@ -66,7 +70,7 @@ if [ "${1:-}" = "--self-test" ]; then expect_red 'Path::parent(x)' zz-s 'let _ = Path::parent(&here);' expect_red '.parent ()' zz-t 'let _ = here.parent ();' expect_red 'a non-member dir is only exempt when named' facades-x 'let _ = here.ancestors();' - expect_red 'rustfmt-split PathBuf pop/push' zz-u $'let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n p.pop();\n p.push("aprender-core");' + expect_red 'rustfmt-wrapped let + split pop/push' zz-u $'let mut m =\n PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n m.pop();\n m.push("aprender-core");' printf '{"packages":[{"manifest_path":"/x/crates/facades/zz-evil/Cargo.toml"}]}' >"$t/meta.json"; fresh zz-v if (AFFECTED_META_JSON="$t/meta.json" check crates >/dev/null 2>&1); then echo "FAIL self-test: a NONMEMBER dir listed by cargo metadata passed"; fail=1; fi # the bench-tokenizer exemption is the one line, not the crate diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh index 946f8afe12..acb4d9c799 100755 --- a/scripts/check_affected_set.sh +++ b/scripts/check_affected_set.sh @@ -53,7 +53,7 @@ row "a crate's own Cargo.toml -> ALL" ALL "$(run "$ld/Cargo.toml")" row "a .md inside a leaf crate -> ALL (aprender-core reads crates/*/README.md)" ALL "$(run "$ld/README.md")" row "a .json inside a leaf crate -> ALL (kernel-registry.json is read by aprender-contracts)" ALL "$(run "$ld/kernel-registry.json")" row "a .rs inside a leaf crate stays scoped (itself + aprender-core)" "$lc" "$(run "$ld/src/other.rs" | sort -u)" -row "--filter names the package and aprender-core" "$(printf 'package(%s)\n' $lc | paste -sd'|' | sed 's/|/ | /g')" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" +row "--filter names the package and aprender-core" "$(printf 'package(%s)\n' $lc | LC_ALL=C sort | paste -sd'|' | sed 's/|/ | /g')" "$($A --filter --diff-from <(echo "$ld/src/lib.rs"))" row "--filter ALL -> all()" "all()" "$($A --filter --diff-from <(echo Cargo.lock))" # empty diff and a bad base are ALL (nothing measured, nothing skipped) : >"$t/empty" From 28a93128f9737f364fc1e3a838a7f06c0503b96b Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Wed, 21 Oct 2026 11:30:56 +0200 Subject: [PATCH 16/16] fix(ci): setsid --wait on the readers self-test step (#4133); spaced .pop ( row (#5802) Co-Authored-By: Claude Sonnet 5.5 --- ci/sections.yml | 2 +- scripts/check_affected_readers.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/sections.yml b/ci/sections.yml index 1678a615af..edc32bb85a 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -1955,7 +1955,7 @@ jobs: - name: "Affected-set tool case table (FLOW-010 E5, RED rows and mutants)" run: setsid --wait bash scripts/check_affected_set.sh - name: "Affected-set readers guard (every crate that reads other crates' files is listed; RED on a new one)" - run: setsid --wait bash scripts/check_affected_readers.sh && bash scripts/check_affected_readers.sh --self-test + run: setsid --wait bash scripts/check_affected_readers.sh && setsid --wait bash scripts/check_affected_readers.sh --self-test # #2640: the SAME defect class as the two steps above, one level up. The # dogfood release runner existed as two 1170-line copies whose divergence # was, in every hunk, a pin for a release verifier that one copy had and diff --git a/scripts/check_affected_readers.sh b/scripts/check_affected_readers.sh index 5cd3d9ae66..2da2e462e4 100755 --- a/scripts/check_affected_readers.sh +++ b/scripts/check_affected_readers.sh @@ -71,6 +71,7 @@ if [ "${1:-}" = "--self-test" ]; then expect_red '.parent ()' zz-t 'let _ = here.parent ();' expect_red 'a non-member dir is only exempt when named' facades-x 'let _ = here.ancestors();' expect_red 'rustfmt-wrapped let + split pop/push' zz-u $'let mut m =\n PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n m.pop();\n m.push("aprender-core");' + expect_red 'spaced .pop (' zz-w $'let mut m = PathBuf::from(env!("CARGO_MANIFEST_DIR"));\n m.pop ();' printf '{"packages":[{"manifest_path":"/x/crates/facades/zz-evil/Cargo.toml"}]}' >"$t/meta.json"; fresh zz-v if (AFFECTED_META_JSON="$t/meta.json" check crates >/dev/null 2>&1); then echo "FAIL self-test: a NONMEMBER dir listed by cargo metadata passed"; fail=1; fi # the bench-tokenizer exemption is the one line, not the crate