diff --git a/.github/workflows/guards-nightly.yml b/.github/workflows/guards-nightly.yml index af5fe6b1d5..6201af784c 100644 --- a/.github/workflows/guards-nightly.yml +++ b/.github/workflows/guards-nightly.yml @@ -349,6 +349,27 @@ jobs: exec > >(tee -a "$RUNNER_TEMP/census.log") 2>&1 setsid --wait bash scripts/census_ratchet.sh --self-test CENSUS_BASE_REF="${RATCHET_24H_BASE:?no 24 h comparand (#5182)}" setsid --wait bash scripts/census_ratchet.sh + # #5809 (FLOW-010 E5): the affected-set tool's case tables. LAB, never on a PR path (C324 rules 5/7): + # the tool is not consumed by any gate yet; a red opens or updates the one LAB ticket below, never a block. + - name: Affected-set tool and readers guards (FLOW-010 E5, #5809) + id: affected_set + if: ${{ !cancelled() }} + run: | + set -euo pipefail + exec > >(tee -a "$RUNNER_TEMP/affected.log") 2>&1 + setsid --wait bash scripts/check_affected_set.sh + setsid --wait bash scripts/check_affected_readers.sh + setsid --wait bash scripts/check_affected_readers.sh --self-test + - name: A red affected-set night opens or updates the one LAB ticket (#5809) + if: failure() && steps.affected_set.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + bash scripts/lab_ticket.sh --label lab-affected-set --title "LAB: nightly affected-set guards red (#5809)" \ + --owner aprender-w5018 --log "$RUNNER_TEMP/affected.log" --event "$GITHUB_EVENT_NAME" --run-url "$RUN_URL" - name: A red census night opens or updates the one LAB ticket (#4709) if: failure() && steps.ratchet_census.outcome == 'failure' env: diff --git a/scripts/affected_set.sh b/scripts/affected_set.sh new file mode 100755 index 0000000000..f9a8adb719 --- /dev/null +++ b/scripts/affected_set.sh @@ -0,0 +1,93 @@ +#!/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) +# Cargo.toml of a crate ALL (a manifest edit changes features/deps, which no path can bound) +# 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 +# docs/, contracts/, root *.md READERS only (FLOW-010 b): their run-time readers are all in READERS. Exceptions, ALL: +# contracts binding.yaml, contracts model-families and the BUILD_CONTRACTS files (compiled into a crate) +# .claude/, ci/shard-step-costs.tsv +# NO-READER list (#5809): the only run-time readers of these paths (grep of crates/**/*.rs: contracts-cli, +# orchestrate, core, mcp, apr-cli) are all in READERS, so the set is READERS alone. Alone or +# beside crate .rs files; any other path in the diff still decides ALL. +# 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 +# out of a crate would otherwise show only the destination and the source crate would be skipped). +# --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 = 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 (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:-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-contracts-macros aprender-contrastive-data aprender-core aprender-data aprender-db aprender-decide aprender-distribute aprender-explain aprender-gpu aprender-graph aprender-mcp aprender-orchestrate aprender-present-cli aprender-present-core aprender-present-layout aprender-present-lib aprender-present-terminal aprender-present-widgets aprender-present-yaml aprender-profile aprender-ptx-debug aprender-qa-certify aprender-qa-cli aprender-qa-gen aprender-qa-report aprender-qa-runner aprender-quant aprender-rag aprender-rag-cli aprender-registry aprender-review-experiment aprender-rollup aprender-serve aprender-shell aprender-simulate aprender-test-cli aprender-test-lib aprender-train aprender-train-bench aprender-train-distill aprender-train-inspect aprender-train-lora aprender-train-shell aprender-tsp aprender-update aprender-verify-ml aprender-viz aprender-viz-ttop aprender-zram aprender-zram-cli aprender-zram-generator facades}" +filter=0 from="" bad=0 +while [ $# -gt 0 ]; do + case "$1" in + --filter) filter=1; shift ;; + --diff-from) [ $# -ge 2 ] || { bad=1; break; }; from="$2"; shift 2 ;; + *) break ;; + esac +done +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 +} +[ "$bad" = 0 ] || { emit ALL; exit 0; } # --diff-from with no FILE +case "$from" in *..*) echo "affected_set: refusing a --diff-from with \"..\": $from" >&2; emit ALL; exit 0 ;; esac # bashrs SEC010: refuse the path (never read it), fail closed with ALL +if [ -n "$from" ]; then files="$(cat "$from")" || files="" +else files="$(git diff --name-only --no-renames "${1:-origin/main}...${2:-HEAD}")" || files=""; fi +[ -n "$files" ] || { emit ALL; exit 0; } +if [ -n "${AFFECTED_META:-}" ]; then meta="$(cat "$AFFECTED_META")" || meta="" +else meta="$(cargo metadata --no-deps --format-version 1 --offline 2>/dev/null || cargo metadata --no-deps --format-version 1)" || meta=""; fi +root="${AFFECTED_ROOT:-$(git rev-parse --show-toplevel)}" || root="" +[ -n "$meta" ] && [ -n "$root" ] || { emit ALL; exit 0; } # no metadata, no checkout root: nothing measured +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; } +# Root contracts/ files that build.rs or include_str!/include! turn into compiled code or constants (aprender-serve build.rs generates +# arch/tensor-name code from three of them; apr-cli and mcp embed others). A change reaches dependents of the compiling crate, so these are ALL. +# check_affected_readers.sh derives this list from the tree and is RED on a file it omits. +BUILD_CONTRACTS="${AFFECTED_BUILD_CONTRACTS:-apr-mcp-tool-schemas-v1.yaml apr-model-capability-v1.yaml arch-constraints-v1.yaml architecture-requirements-v1.yaml kernel-fusion-v1.yaml layer-parity-v1.yaml softmax-kernel-v1.yaml tensor-names-v1.yaml thinking-budgets-v1.yaml}" +touched="" skipped=0 +while IFS= read -r f; do + case "$f" in */*) ;; *.md) skipped=1; continue ;; esac # root *.md: its run-time readers are in READERS + case "$f" in contracts/*) case " $BUILD_CONTRACTS " in *" ${f#contracts/} "*) emit ALL; exit 0 ;; esac ;; esac + case "$f" in + '') continue ;; + contracts/binding.yaml | contracts/*/binding.yaml | contracts/model-families/* | contracts/*/model-families/*) emit ALL; exit 0 ;; # build inputs of core/compute/contracts/simulate/train: their generated code reaches dependents + contracts/* | docs/*) skipped=1; continue ;; # FLOW-010(b): run-time readers are in READERS + .claude/* | ci/shard-step-costs.tsv) skipped=1; continue ;; + crates/*/build.rs) emit ALL; exit 0 ;; + 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")" + [ -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" ] || [ "$skipped" = 1 ] || { emit ALL; exit 0; } # nothing placed in a package and nothing on the no-reader list: 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 (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(.))) + ($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..f65291ab32 --- /dev/null +++ b/scripts/check_affected_readers.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# 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 +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/)' +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 +} +build_listed() { sed -n 's/^BUILD_CONTRACTS="\${AFFECTED_BUILD_CONTRACTS:-\(.*\)}"$/\1/p' "$SET" | tr ' ' '\n' | grep -v '^$' | sort -u; } +# Root contracts/ that a build.rs names (any quoted *-vN.yaml) or an include*! macro embeds: compiled into a crate, so ALL. +build_inputs() { # $1 = crates dir; root contracts dir = ${CONTRACTS_DIR:-contracts} + { grep -rhE --include=build.rs '[A-Za-z0-9_-]+\.yaml"' "$1" 2>/dev/null | grep -vE '^\s*//' | grep -oE '[A-Za-z0-9_-]+\.yaml"' | tr -d '"' + grep -rhE --include='*.rs' 'include(_str|_bytes)?!' "$1" 2>/dev/null | grep -vE '^\s*//' | grep -oE 'contracts/[A-Za-z0-9_-]+\.yaml' | sed 's#contracts/##' + } | sort -u | while read -r f; do [ -f "${CONTRACTS_DIR:-contracts}/$f" ] && echo "$f"; done | grep -v 'binding\.yaml$' || true +} +check() { + local bad="" l c; l="$(listed)" + for c in $(hits "$1"); do grep -qx "$c" <<<"$l" || bad="$bad $c"; done + local bl; bl="$(build_listed)" + for c in $(build_inputs "$1"); do grep -qx "$c" <<<"$bl" || bad="$bad contracts/$c(BUILD_CONTRACTS)"; 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 + 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 "${1:-crates}" diff --git a/scripts/check_affected_set.sh b/scripts/check_affected_set.sh new file mode 100755 index 0000000000..2a2b3afe24 --- /dev/null +++ b/scripts/check_affected_set.sh @@ -0,0 +1,193 @@ +#!/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 +export LC_ALL=C # sort order must match jq's codepoint order (apr-format vs aprender-*) +# 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 +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 aprender-quant " +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="$(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 + 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 +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.*//'; 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)" +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 and fails closed to ALL (SEC010)" "ALL:0" "$(o=$($A --diff-from ../x 2>/dev/null); echo "$o:$?")" +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/findings ledger alone -> READERS only, not ALL (#5809)" "$(printf '%s\n' $readers | sort -u)" "$(run docs/findings/s1.jsonl | sort -u)" +row ".claude agent-memory alone -> READERS only" "$(printf '%s\n' $readers | sort -u)" "$(run .claude/agent-memory/x/MEMORY.md | sort -u)" +row "shard cost table alone -> READERS only" "$(printf '%s\n' $readers | sort -u)" "$(run ci/shard-step-costs.tsv | sort -u)" +row "no-reader path next to a leaf .rs -> leaf + READERS" "$lc" "$(run docs/findings/s1.jsonl "$ld/src/lib.rs" | sort -u)" +row "no-reader path next to docs/x.md -> READERS only (FLOW-010 b)" "$(printf '%s\n' $readers | sort -u)" "$(run docs/findings/s1.jsonl docs/x.md | sort -u)" +row "no-reader path next to a CORE crate -> ALL" ALL "$(run .claude/x docs/findings/s1.jsonl crates/aprender-core/src/lib.rs)" +row "docs/findings lookalike (docs/findings-x.md) -> READERS only" "$(printf '%s\n' $readers | sort -u)" "$(run docs/findings-x.md | sort -u)" +row "docs file -> READERS only (their run-time readers are in READERS)" "$(printf '%s\n' $readers | sort -u)" "$(run docs/x.md | sort -u)" +row "contracts yaml -> READERS only" "$(printf '%s\n' $readers | sort -u)" "$(run contracts/foo-v1.yaml | sort -u)" +row "contracts//binding.yaml -> ALL (build input of core/compute/contracts/simulate/train)" ALL "$(run contracts/aprender/binding.yaml)" +row "serve build.rs input contracts/tensor-names-v1.yaml -> ALL" ALL "$(run contracts/tensor-names-v1.yaml)" +row "serve build.rs input contracts/architecture-requirements-v1.yaml -> ALL" ALL "$(run contracts/architecture-requirements-v1.yaml)" +row "serve build.rs input contracts/arch-constraints-v1.yaml -> ALL" ALL "$(run contracts/arch-constraints-v1.yaml)" +row "BUILD_CONTRACTS file next to a leaf .rs -> ALL" ALL "$(run contracts/tensor-names-v1.yaml "$ld/src/lib.rs")" +row "contracts/binding.yaml -> ALL" ALL "$(run contracts/binding.yaml)" +row "contracts/model-families -> ALL (aprender-core build.rs generates code from it)" ALL "$(run contracts/model-families/q.yaml)" +row "contracts yaml next to a leaf .rs -> leaf + READERS" "$lc" "$(run contracts/foo-v1.yaml "$ld/src/lib.rs" | sort -u)" +row "root README.md -> READERS only" "$(printf '%s\n' $readers | sort -u)" "$(run README.md | sort -u)" +row "root CLAUDE.md -> READERS only" "$(printf '%s\n' $readers | sort -u)" "$(run CLAUDE.md | sort -u)" +row "ci/sections.yml -> ALL (it holds the CI job bodies: editing it changes what CI runs, not only what tests read)" 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 -> 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 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 +# --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 +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" "$lc" "$(bash "$t/mut-build.sh" --diff-from "$t/db" | sort -u || true)"; fi +# mutant: dropping the contracts build-input arm must be caught by the binding/model-families ALL rows +sed '/^ contracts\/binding.yaml /d' "$A" >"$t/mut-bind.sh" +if cmp -s "$A" "$t/mut-bind.sh"; then echo "FAIL binding mutant did not apply"; fail=1 +else printf '%s\n' contracts/aprender/binding.yaml >"$t/db2"; row "MUTANT: dropping the binding/model-families ALL arm is caught" "$(printf '%s\n' $readers | sort -u)" "$(bash "$t/mut-bind.sh" --diff-from "$t/db2" | sort -u)"; fi +# mutant: the checker must go RED when a build.rs-read root contract is missing from BUILD_CONTRACTS +sed 's/ tensor-names-v1.yaml//' "$A" >"$t/mut-bc.sh" +if cmp -s "$A" "$t/mut-bc.sh"; then echo "FAIL build-contracts mutant did not apply"; fail=1 +elif AFFECTED_SET_SH="$t/mut-bc.sh" bash scripts/check_affected_readers.sh >/dev/null 2>&1; then echo "FAIL MUTANT: a build.rs-read contract missing from BUILD_CONTRACTS is not caught"; fail=1 +else echo "ok MUTANT: a build.rs-read contract missing from BUILD_CONTRACTS is caught by check_affected_readers.sh"; fi +# mutant: root-md arm that also skips crate-level .md must be caught by the leaf README row +sed 's#case "\$f" in \*/\*) ;; \*.md)#case "\$f" in *.md)#' "$A" >"$t/mut-md.sh" +if cmp -s "$A" "$t/mut-md.sh"; then echo "FAIL md mutant did not apply"; fail=1 +else printf '%s\n%s\n' "$ld/README.md" "$ld/src/lib.rs" >"$t/dmd"; row "MUTANT: skipping crate-level .md is caught by the leaf README row" "$lc" "$(bash "$t/mut-md.sh" --diff-from "$t/dmd" | sort -u)"; 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%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 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 +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 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 AFFECTED_READERS=aprender-core bash "$t/mut-fmt.sh" --filter --diff-from <(echo crates/a/src/lib.rs))"; fi +# fail-closed: every error path prints ALL (never nothing) -- a silent exit would read as "no tests affected" (#5815 quorum) +abs_a="$(cd "$(dirname "$A")" && pwd)/$(basename "$A")" +row "missing --diff-from file -> ALL" ALL "$($A --diff-from "$t/no-such-file" 2>/dev/null || true)" +row "--diff-from with no FILE -> ALL" ALL "$($A --diff-from 2>/dev/null || true)" +row "--filter, missing --diff-from file -> all()" "all()" "$($A --filter --diff-from "$t/no-such-file" 2>/dev/null || true)" +mkdir -p "$t/nocargo"; for b in cat git jq sed awk paste grep sort head dirname basename mktemp; do ln -sf "$(command -v $b)" "$t/nocargo/$b"; done +printf 'crates/a/src/lib.rs\n' >"$t/one" +row "no cargo on PATH (rc 127) -> ALL" ALL "$(PATH="$t/nocargo" "$(command -v bash)" "$abs_a" --diff-from "$t/one" 2>/dev/null || true)" +mkdir -p "$t/notgit" +row "git rev-parse fails (not a checkout, rc 128) -> ALL" ALL "$(cd "$t/notgit" && AFFECTED_META="$t/meta.json" bash "$abs_a" --diff-from "$t/one" 2>/dev/null || true)" +sed 's/ || root=""$//' "$A" >"$t/mut-root.sh" +if cmp -s "$A" "$t/mut-root.sh"; then echo "FAIL root-fallback mutant did not apply"; fail=1 +else row "MUTANT: dropping the rev-parse fallback is caught (prints nothing)" "" "$(cd "$t/notgit" && AFFECTED_META="$t/meta.json" bash "$t/mut-root.sh" --diff-from "$t/one" 2>/dev/null || true)"; fi +sed 's/ || files=""$//' "$A" >"$t/mut-cat.sh" +if cmp -s "$A" "$t/mut-cat.sh"; then echo "FAIL cat-fallback mutant did not apply"; fail=1 +else row "MUTANT: dropping the missing-file fallback is caught (prints nothing)" "" "$(bash "$t/mut-cat.sh" --diff-from "$t/no-such-file" 2>/dev/null || true)"; 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" diff --git a/scripts/guards_nightly_manifest.txt b/scripts/guards_nightly_manifest.txt index 423ddca459..8f15c9fb35 100644 --- a/scripts/guards_nightly_manifest.txt +++ b/scripts/guards_nightly_manifest.txt @@ -49,3 +49,5 @@ # #4709 (C303): the pv census ratchet, LAB from its first night (never on a PR path, C324). # Seconds are a local wall-clock sample (2026-10-14, pv prebuilt); the first nightly replaces them. 6 The pv census bound share may only rise (#4709) +# #5809 (FLOW-010 E5): affected-set tool case tables, LAB from the first night (never on a PR path, C324). Seconds are a local sample. +40 Affected-set tool and readers guards (FLOW-010 E5, #5809)