From e60f9a295a09f3c3c1e6cd5f30e2d154c51cd1cb Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 11:30:36 +0200 Subject: [PATCH] :recycle: refactor(examples): discover dogfood packs instead of a hardcoded 3-name loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EX-S08: the "which example packs does `assent test` dogfood" list was duplicated by hand across Taskfile.yml's dogfood-examples task, the verify.yaml dogfood step, and a greenExamplePacks Go slice — a proven skew risk when a pack is added. hack/dogfood-examples.sh now discovers every examples/packs/ with a .assent/tests directory (mirroring the EX-S01 inventory contract); Taskfile.yml and verify.yaml both call that one script, and greenExamplePacks is derived from the same glob at test time. task check gains a dogfood-wiring-test stage (after build, alongside build itself) so unwiring dogfood-examples from check: reds a pin instead of only failing in CI (REQ-EX-S08-01..05). --- .github/workflows/verify.yaml | 9 +- CHANGELOG.md | 4 + Taskfile.yml | 24 +++-- cmd/assent/test_corpus_test.go | 151 +++++++++++++++++++++++++-- hack/dogfood-examples.sh | 55 ++++++++++ hack/examples/dogfood_wiring_test.sh | 134 ++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 25 deletions(-) create mode 100755 hack/dogfood-examples.sh create mode 100755 hack/examples/dogfood_wiring_test.sh diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index c5783e14..7ff8d336 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -78,13 +78,8 @@ jobs: - name: coverage gate (D-010 — threshold single-sourced from Taskfile.yml) run: task coverage - run: CGO_ENABLED=0 go build ./... - - name: dogfood examples (P5-E6-S08 — every non-locked pack gates itself green under the real `assent test` CLI) - run: | - CGO_ENABLED=0 go build -o bin/assent ./cmd/assent - for pack in service-catalog infra-vars topic-registry; do - ./bin/assent test "examples/packs/$pack" - ./bin/assent test --coverage "examples/packs/$pack" - done + - name: dogfood examples (EX-S08 — shared discovery script; every examples/packs/ with .assent/tests gates itself green under the real `assent test` CLI) + run: bash hack/dogfood-examples.sh - name: comparison corpus dogfood (PCS-S08 — examples/comparison validates and runs green) run: go test ./examples/comparison/... - name: PCS compare exit gate (RELSE-03 — REQ-PCS-S09) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d561fb..dbee3e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ repository still gets a decision, never by following the link; no release carrie - :memo: docs(adr): add ADR-0011 Amendment 4 — Rego/OPA rule-7 capability boundary - :memo: docs(adr): add ADR-0015 host-side credential resolver amendment - :memo: docs(adr): retcon ADR narrative to read as planned phases +- :memo: docs(changelog): regenerate CHANGELOG.md for PR #59's operator-ruling commits ### Features - :sparkles: feat(docs): gate example pack and format claims against dogfood @@ -124,6 +125,9 @@ repository still gets a decision, never by following the link; no release carrie - :bug: specs(p5-ex): make S10 schema freeze and S07 fence non-vacuous - :bug: fix(docs-gates): fail on unmapped format tokens and de-confound the tests-dir mutation (EX-S01 review) +### Refactoring +- :recycle: refactor(examples): discover dogfood packs instead of a hardcoded 3-name loop + ### Testing - :white_check_mark: test(release): anchor the D-120 note check on its header sentence, not the bare token - :white_check_mark: test(release): key the merge-skip proof on commit shape, not subject prefix diff --git a/Taskfile.yml b/Taskfile.yml index 8ae9fc3b..f077315a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -58,16 +58,10 @@ tasks: - CGO_ENABLED=0 go build -ldflags "-X main.version=${ASSENT_VERSION:-0.0.0-dev}" -o bin/assent ./cmd/assent dogfood-examples: - desc: "Dogfood gate (P5-E6-S08): every non-locked example pack gates itself green under the real `assent test` (+ --coverage) CLI" + desc: "Dogfood gate (EX-S08): every examples/packs/ with .assent/tests gates itself green under the real `assent test` (+ --coverage) CLI — discovered, not hardcoded" deps: [build] cmds: - - | - set -e - for pack in service-catalog infra-vars topic-registry; do - echo "== assent test examples/packs/$pack ==" - ./bin/assent test "examples/packs/$pack" - ./bin/assent test --coverage "examples/packs/$pack" - done + - bash hack/dogfood-examples.sh dogfood-comparison: desc: "Dogfood gate (PCS-S08): comparison corpus validates and runs green under assent compare --suite" @@ -83,6 +77,11 @@ tasks: - task: test - task: coverage - task: build + # EX-S08: dogfood the shipped example packs through the real `assent test` + # CLI, sequential and AFTER build (D-124 lesson: a deps: race with fmt + # caused a prior bug — this stage runs the built binary, so it belongs + # after build like docs-gates runs after the tree is otherwise settled). + - task: dogfood-examples - task: dogfood-comparison - task: compare-exitgate-test # AUD-S02 (REQ-AUD-S02-02): CHANGELOG.md drift gate. It is the LOCAL half of the @@ -116,6 +115,10 @@ tasks: # .github/workflows/**, so without this line an unpinned workflow is caught # only by CI — and only for as long as the CI step itself survives. - task: lint-workflow-pins-test + # EX-S08 (REQ-EX-S08-03): the dogfood-examples gate's own guard. Without + # this, deleting `- task: dogfood-examples` from check: above passes + # every wired gate — the same manual-gate defect D-124 closed elsewhere. + - task: dogfood-wiring-test # Operator 2026-08-13: Dependabot is the updater; a reintroduced # renovate.json would otherwise merge green. - task: ci-audit-test @@ -125,6 +128,11 @@ tasks: cmds: - bash hack/release/ci_audit_test.sh + dogfood-wiring-test: + desc: "EX-S08 gate: task check runs dogfood-examples after build, and Taskfile.yml/verify.yaml both delegate to hack/dogfood-examples.sh (REQ-EX-S08-02/03)" + cmds: + - bash hack/examples/dogfood_wiring_test.sh + lint-workflow-pins-test: desc: "AUD-S09/S14 gate: workflow supply-chain pins are present and the checks can fail (REQ-AUD-S09-01, REQ-AUD-S14-01/02)" cmds: diff --git a/cmd/assent/test_corpus_test.go b/cmd/assent/test_corpus_test.go index 3e9fd159..b7de6aad 100644 --- a/cmd/assent/test_corpus_test.go +++ b/cmd/assent/test_corpus_test.go @@ -31,6 +31,9 @@ import ( "bytes" "os" "path/filepath" + "reflect" + "regexp" + "sort" "strings" "testing" @@ -42,18 +45,52 @@ import ( // greenExamplePacks are the non-locked example packs that gate themselves green under // `assent test`. rego is locked (D-012) and carries no .assent/tests/** at all. -// Keep Taskfile.yml dogfood-examples + .github/workflows/verify.yaml dogfood step in -// sync (TestDogfoodScriptsIncludeGreenExamplePacks). -var greenExamplePacks = []string{"service-catalog", "infra-vars", "topic-registry"} +// +// EX-S08: this used to be a hardcoded three-name literal, duplicated by hand in +// Taskfile.yml's dogfood-examples task and .github/workflows/verify.yaml's dogfood +// step — a proven skew risk (a pack green in one, silently absent in another). It is +// now DISCOVERED from the filesystem via discoverGreenExamplePacks, walking the same +// examples/packs/*/.assent/tests contract that hack/dogfood-examples.sh (the script +// Taskfile.yml and verify.yaml both now call) and examples/README.md's inventory gate +// (EX-S01, hack/docs/example_format_inventory_test.sh) use — a fourth pack is picked +// up here without editing this file. +var greenExamplePacks = discoverGreenExamplePacks() + +// discoverGreenExamplePacks walks examples/packs/*/.assent/tests: every immediate +// child of examples/packs/ with a .assent/tests/ subdirectory is a pack this gate +// dogfoods. Mirrors hack/dogfood-examples.sh's discovery rule exactly. +func discoverGreenExamplePacks() []string { + matches, err := filepath.Glob(filepath.Join(examplesPacksDir, "*", ".assent", "tests")) + if err != nil { + // filepath.Glob only errors on a malformed pattern; the pattern above is a + // compile-time constant, so this is unreachable short of test corruption. + panic("discoverGreenExamplePacks: " + err.Error()) + } + packs := make([]string, 0, len(matches)) + for _, m := range matches { + // m == examplesPacksDir//.assent/tests + packs = append(packs, filepath.Base(filepath.Dir(filepath.Dir(m)))) + } + sort.Strings(packs) + return packs +} // brokenPackDir is the DELIBERATELY-broken fixture (a valid pack whose expect.yaml pins // the wrong decision). It lives under testdata, never examples/packs, so the shipped // corpus stays green while the failure path is still proven. const brokenPackDir = "testdata/broken-pack" -// TestDogfoodScriptsIncludeGreenExamplePacks pins the CLI dogfood loops -// (Taskfile dogfood-examples + verify.yaml) to the same pack set as greenExamplePacks -// so unpinning a pack (EFE-S04 / topic-registry) cannot leave the shell loops stale. +// hardcodedPackLoopPattern matches the OLD `for pack in ...` shell +// loop shape (2+ space-separated tokens) that EX-S08 replaced. Mirrors +// hack/examples/dogfood_wiring_test.sh's hardcoded_pack_loop check. +var hardcodedPackLoopPattern = regexp.MustCompile(`for pack in [A-Za-z0-9_-]+ [A-Za-z0-9_-]+`) + +// TestDogfoodScriptsIncludeGreenExamplePacks is REQ-EX-S08-02: Taskfile.yml's +// dogfood-examples task and verify.yaml's dogfood step both call the SHARED +// hack/dogfood-examples.sh discovery script rather than each carrying its own +// hardcoded pack-name loop. Before EX-S08 this test grepped for the three literal +// pack names in each file — passing on the redundant-but-correct loop AND on a +// hardcoded loop of DIFFERENT names, which is exactly the skew this story closes. func TestDogfoodScriptsIncludeGreenExamplePacks(t *testing.T) { files := []string{ filepath.Join("..", "..", "Taskfile.yml"), @@ -65,13 +102,107 @@ func TestDogfoodScriptsIncludeGreenExamplePacks(t *testing.T) { t.Fatalf("read %s: %v", f, err) } body := string(raw) - for _, pack := range greenExamplePacks { - // Match the shell `for pack in …` token list, not incidental prose. - if !strings.Contains(body, pack) { - t.Errorf("%s: dogfood loop missing green pack %q", f, pack) + if !strings.Contains(body, "hack/dogfood-examples.sh") { + t.Errorf("%s: does not invoke the shared hack/dogfood-examples.sh discovery script", f) + } + if hardcodedPackLoopPattern.MatchString(body) { + t.Errorf("%s: re-hardcodes a pack-name loop instead of delegating to hack/dogfood-examples.sh", f) + } + } +} + +// TestGreenExamplePacksIsFilesystemDerived is REQ-EX-S08-05: greenExamplePacks is +// exactly the set of examples/packs/* directories with a .assent/tests/ +// subdirectory — not a hardcoded literal that happens to match today's corpus. A +// hand-reverted `var greenExamplePacks = []string{...}` would still compile and +// would still pass every other test in this file (they only iterate the slice), so +// this pin independently recomputes the glob and compares. +func TestGreenExamplePacksIsFilesystemDerived(t *testing.T) { + matches, err := filepath.Glob(filepath.Join(examplesPacksDir, "*", ".assent", "tests")) + if err != nil { + t.Fatalf("glob: %v", err) + } + want := make([]string, 0, len(matches)) + for _, m := range matches { + want = append(want, filepath.Base(filepath.Dir(filepath.Dir(m)))) + } + sort.Strings(want) + if len(want) == 0 { + t.Fatal("filesystem glob discovered zero packs — this assertion would be vacuous") + } + got := append([]string(nil), greenExamplePacks...) + sort.Strings(got) + if !reflect.DeepEqual(got, want) { + t.Errorf("greenExamplePacks = %v, want filesystem-derived %v", got, want) + } +} + +// TestBrokenPackFixtureIsNotDiscovered is the REQ-EX-S08-04 edge: the +// deliberately-broken CLI fixture (brokenPackDir) lives under cmd/assent/testdata, +// never under examples/packs/, precisely so the shipped corpus stays green while +// the failure path is still proven. Discovery walks examples/packs/* only, so it +// can never pick brokenPackDir up by construction — pinned explicitly so a future +// move of the fixture under examples/packs/ is caught here rather than silently +// turning every dogfood run red. +func TestBrokenPackFixtureIsNotDiscovered(t *testing.T) { + for _, pack := range greenExamplePacks { + if pack == "broken-pack" { + t.Fatalf("greenExamplePacks discovered the testdata broken-pack fixture: %v", greenExamplePacks) + } + } + if _, err := os.Stat(filepath.Join(examplesPacksDir, "broken-pack")); err == nil { + t.Fatal("broken-pack must not exist under examples/packs/ — the deliberately-broken fixture belongs under cmd/assent/testdata only") + } +} + +// readmePackNames extracts the backtick-quoted pack names from examples/README.md's +// `[`packs/`]` bullet (and its wrapped continuation line), mirroring +// hack/docs/example_format_inventory_test.sh's readme_packs() exactly. +func readmePackNames(t *testing.T, readmePath string) []string { + t.Helper() + raw, err := os.ReadFile(readmePath) //nolint:gosec // fixed in-repo docs path. + if err != nil { + t.Fatalf("read %s: %v", readmePath, err) + } + backtick := regexp.MustCompile("`([a-z0-9-]+)`") + inPacks := false + var names []string + for _, line := range strings.Split(string(raw), "\n") { + if strings.Contains(line, "[`packs/`]") { + inPacks = true + } else if inPacks && (strings.TrimSpace(line) == "" || + (strings.HasPrefix(strings.TrimSpace(line), "- [`"))) { + break + } + if !inPacks { + continue + } + for _, m := range backtick.FindAllStringSubmatch(line, -1) { + if m[1] != "packs" { + names = append(names, m[1]) } } } + sort.Strings(names) + return names +} + +// TestExampleCorpusDiscoveryMatchesReadmeInventory is the second half of +// REQ-EX-S08-02/05 ("the walk matches what README/inventory claims"): the +// filesystem-discovered greenExamplePacks must equal the pack names +// examples/README.md's `[`packs/`]` bullet advertises — the same equality EX-S01's +// hack/docs/example_format_inventory_test.sh proves from the shell side. Two +// independent readers (bash awk vs. Go regex) agreeing on the same filesystem +// contract is the point: a drift only one of them would catch is exactly what this +// duplication is meant to rule out. +func TestExampleCorpusDiscoveryMatchesReadmeInventory(t *testing.T) { + readmePath := filepath.Join("..", "..", "examples", "README.md") + docPacks := readmePackNames(t, readmePath) + fsPacks := append([]string(nil), greenExamplePacks...) + sort.Strings(fsPacks) + if !reflect.DeepEqual(docPacks, fsPacks) { + t.Errorf("examples/README.md packs %v != filesystem discovery %v (EX-S01 inventory drift)", docPacks, fsPacks) + } } // TestAllExamplePacksGreenUnderAssentTest is REQ-E6-S08-01: every non-locked diff --git a/hack/dogfood-examples.sh b/hack/dogfood-examples.sh new file mode 100755 index 00000000..c6d36487 --- /dev/null +++ b/hack/dogfood-examples.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# hack/dogfood-examples.sh — EX-S08 shared discovery script. +# +# Before this script existed, "run every example pack through `assent test`" +# was a hardcoded `for pack in service-catalog infra-vars topic-registry` loop +# duplicated in THREE places: Taskfile.yml's dogfood-examples task, +# .github/workflows/verify.yaml's dogfood step, and the greenExamplePacks Go +# slice in cmd/assent/test_corpus_test.go. Adding a pack meant editing all +# three in sync — a proven skew risk (a pack green in one, silently absent in +# another). Taskfile.yml and verify.yaml now both call THIS script; the Go +# corpus test discovers packs the same way (filepath.Glob over +# examples/packs/*/.assent/tests), so all three halves walk the same +# filesystem contract instead of carrying independent copies of a name list. +# +# Discovery rule (matches examples/README.md's inventory gate, +# hack/docs/example_format_inventory_test.sh, EX-S01): every immediate child +# of examples/packs/ that has a .assent/tests/ subdirectory is a pack to +# dogfood. A pack directory WITHOUT .assent/tests/ is not silently skipped — +# hack/docs/example_format_inventory_test.sh treats that as a hard error +# (incomplete tree) — so this script only ever sees complete packs, and an +# incomplete/red one that DOES have .assent/tests/ fails loudly below instead +# of being filtered out by name. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +BIN="${ASSENT_BIN:-bin/assent}" +if [[ ! -x "$BIN" ]]; then + echo "== building $BIN ==" + CGO_ENABLED=0 go build -o "$BIN" ./cmd/assent +fi + +packs=() +for dir in examples/packs/*/; do + [[ -d "$dir" ]] || continue + name="$(basename "$dir")" + if [[ -d "${dir}.assent/tests" ]]; then + packs+=("$name") + fi +done + +if [[ "${#packs[@]}" -eq 0 ]]; then + echo "no example packs with .assent/tests/ discovered under examples/packs/" >&2 + exit 1 +fi + +echo "discovered packs: ${packs[*]}" +for pack in "${packs[@]}"; do + echo "== assent test examples/packs/$pack ==" + "$BIN" test "examples/packs/$pack" + "$BIN" test --coverage "examples/packs/$pack" +done + +echo "OK: dogfooded ${#packs[@]} example pack(s): ${packs[*]}" diff --git a/hack/examples/dogfood_wiring_test.sh b/hack/examples/dogfood_wiring_test.sh new file mode 100755 index 00000000..b081b8ea --- /dev/null +++ b/hack/examples/dogfood_wiring_test.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# REQ-EX-S08-02/03 — dogfood wiring pin, adversarially proven (both polarities). +# +# REQ-EX-S08-02: Taskfile.yml's dogfood-examples task AND verify.yaml's +# dogfood step call the SHARED hack/dogfood-examples.sh discovery script — +# neither may re-hardcode its own `for pack in ` loop. +# REQ-EX-S08-03: `task check` runs dogfood-examples (after build); deleting +# that line from check: must redden this pin. +# +# Follows the hack/release/changelog_gate_test.sh / example_format_inventory_test.sh +# discipline: every "is it wired" assertion is re-run against a mutated copy +# with the wiring deleted, so the assertion is proven capable of failing +# before its green result on the real tree is believed. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +TASKFILE="$ROOT/Taskfile.yml" +WORKFLOW="$ROOT/.github/workflows/verify.yaml" +SCRIPT="hack/dogfood-examples.sh" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# extract_block — body of a top-level 2-space-indented +# mapping key (a Taskfile task), up to the next such key. +extract_block() { + awk -v name="$2" ' + $0 == " " name ":" { inblk = 1; next } + inblk && /^ [A-Za-z0-9_.:-]+:[[:space:]]*$/ { inblk = 0 } + inblk { print } + ' "$1" +} + +check_lists_task() { + extract_block "$1" check >"$WORK/check.block" + grep -qE "^[[:space:]]+- task: $2\$" "$WORK/check.block" +} + +# hardcoded_pack_loop — true if the file still contains the OLD +# `for pack in ...` shell-loop shape (2+ space-separated +# tokens after "for pack in"), the pattern this script replaces. +hardcoded_pack_loop() { + grep -qE 'for pack in [A-Za-z0-9_-]+ [A-Za-z0-9_-]+' "$1" +} + +echo "== 0. extraction positive control ==" +extract_block "$TASKFILE" check >"$WORK/check.control" +[[ -s "$WORK/check.control" ]] || fail "Taskfile check: block extracted EMPTY — the awk range is broken, every assertion below would be vacuous" +grep -qE '^[[:space:]]+- task: build$' "$WORK/check.control" \ + || fail "Taskfile check: block does not contain the known-present '- task: build' — extraction is wrong" +echo "OK: check: block extracted ($(wc -l <"$WORK/check.control" | tr -d ' ') lines), anchored on build" + +# --------------------------------------------- 1. REQ-EX-S08-03 (wiring) -- + +echo "== 1. REQ-EX-S08-03: task check runs dogfood-examples, after build ==" +check_lists_task "$TASKFILE" dogfood-examples \ + || fail "'task check' does not run 'dogfood-examples' — the gate is defined but invoked by nothing (REQ-EX-S08-03)" +extract_block "$TASKFILE" dogfood-examples >"$WORK/def.dogfood-examples" +[[ -s "$WORK/def.dogfood-examples" ]] || fail "'dogfood-examples' is listed in check: but not defined in Taskfile.yml" + +build_line="$(grep -nE '^[[:space:]]+- task: build$' "$WORK/check.control" | head -1 | cut -d: -f1)" +dogfood_line="$(grep -nE '^[[:space:]]+- task: dogfood-examples$' "$WORK/check.control" | head -1 | cut -d: -f1)" +[[ -n "$build_line" && -n "$dogfood_line" ]] || fail "could not locate build/dogfood-examples lines within check: block" +[[ "$dogfood_line" -gt "$build_line" ]] \ + || fail "dogfood-examples (line $dogfood_line) is not positioned AFTER build (line $build_line) in check: — D-124 requires a sequential stage, not a deps: race with fmt" +echo "OK: check runs dogfood-examples after build (line $dogfood_line > $build_line)" + +echo "== 1b. the wiring assertion itself can fail (mutation) ==" +mutant="$WORK/Taskfile.no-dogfood.yml" +grep -vE '^[[:space:]]+- task: dogfood-examples$' "$TASKFILE" >"$mutant" +if grep -qE '^[[:space:]]+- task: dogfood-examples$' "$mutant"; then + fail "mutation did not land: '- task: dogfood-examples' is still in $mutant" +fi +if [[ "$(wc -l <"$mutant")" -eq "$(wc -l <"$TASKFILE")" ]]; then + fail "mutation did not land: $mutant has the same line count as Taskfile.yml" +fi +if check_lists_task "$mutant" dogfood-examples; then + fail "check_lists_task reports dogfood-examples wired in a Taskfile with that line deleted — the assertion is vacuous" +fi +echo "OK: deleting '- task: dogfood-examples' from check: turns the assertion red" + +# --------------------------------------------- 2. REQ-EX-S08-02 (shared script) -- + +echo "== 2. REQ-EX-S08-02: Taskfile dogfood-examples calls the shared script ==" +grep -qF "$SCRIPT" "$WORK/def.dogfood-examples" \ + || fail "Taskfile.yml dogfood-examples task does not invoke $SCRIPT" +if hardcoded_pack_loop "$WORK/def.dogfood-examples"; then + fail "Taskfile.yml dogfood-examples re-hardcodes a pack-name loop instead of calling $SCRIPT" +fi +echo "OK: Taskfile dogfood-examples calls $SCRIPT, no hardcoded loop" + +echo "== 2b. the shared-script assertion can fail (mutation, positive control) ==" +# Prove hardcoded_pack_loop actually detects the OLD shape it replaces. +printf 'cmds:\n - for pack in service-catalog infra-vars topic-registry; do true; done\n' >"$WORK/old-shape.yml" +if ! hardcoded_pack_loop "$WORK/old-shape.yml"; then + fail "hardcoded_pack_loop failed to detect the historical three-name loop shape — the detector is broken" +fi +echo "OK: hardcoded_pack_loop detects the historical loop shape (positive control)" + +echo "== 3. REQ-EX-S08-02: verify.yaml dogfood step calls the shared script ==" +grep -qF "$SCRIPT" "$WORKFLOW" \ + || fail "verify.yaml does not invoke $SCRIPT" +if hardcoded_pack_loop "$WORKFLOW"; then + fail "verify.yaml re-hardcodes a pack-name loop instead of calling $SCRIPT" +fi +echo "OK: verify.yaml calls $SCRIPT, no hardcoded loop" + +echo "== 3b. the workflow assertion itself can fail (mutation) ==" +mutant_wf="$WORK/verify.no-script.yaml" +grep -vF "$SCRIPT" "$WORKFLOW" >"$mutant_wf" +if grep -qF "$SCRIPT" "$mutant_wf"; then + fail "mutation did not land: $mutant_wf still references $SCRIPT" +fi +[[ "$(wc -l <"$mutant_wf")" -lt "$(wc -l <"$WORKFLOW")" ]] || fail "mutation did not land: $mutant_wf has the same line count as verify.yaml" +if grep -qF "$SCRIPT" "$mutant_wf"; then + fail "the shared-script assertion stayed green after deleting the reference — vacuous" +fi +echo "OK: deleting the $SCRIPT reference from verify.yaml turns the assertion red" + +# ------------------------------------------------------------- 4. the script exists -- + +echo "== 4. hack/dogfood-examples.sh exists and is executable ==" +[[ -f "$ROOT/$SCRIPT" ]] || fail "$SCRIPT does not exist" +[[ -x "$ROOT/$SCRIPT" ]] || fail "$SCRIPT is not executable" +echo "OK: $SCRIPT present and executable" + +echo "PASS: dogfood wiring (REQ-EX-S08-02, REQ-EX-S08-03) — task check runs dogfood-examples after build; Taskfile.yml and verify.yaml both delegate to the shared discovery script"