coverage: measure the workspace instead of nothing
grcov reported 0% and passed: LLVM_PROFILE_FILE was relative, so cargo
wrote .profraw into each package root, and --keep-only 'src/**' matched
no file in a workspace where sources live under frork-*/src.

Assisted-by: Claude Opus 5 via Claude Code
change kmkqkkzmkmsouxqmnlnlknlvpzvvqvzl
commit 1351832744e2687fdce9d96b441f21ab4c833157
author Alpha Chen <alpha@kejadlen.dev>
date
parent tokzmzms
diff --git a/.gitignore b/.gitignore
index 75295c1..214f4d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
-/target
+target/
 
 /out
diff --git a/bin/coverage b/bin/coverage
new file mode 100755
index 0000000..089dfe6
--- /dev/null
+++ b/bin/coverage
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+# Measures library line coverage with grcov and fails below the
+# threshold. Uses a separate CARGO_TARGET_DIR so instrumented and
+# normal build artifacts never mix — mixing causes phantom uncovered
+# lines. Override the gate with COVERAGE_THRESHOLD (defaults to 100).
+#
+# See the rust skill's coverage.md and scaffolding.md.
+
+set -euo pipefail
+if [[ "${TRACE-0}" == "1" ]]; then
+    set -o xtrace
+fi
+
+readonly THRESHOLD="${COVERAGE_THRESHOLD:-100}"
+readonly TARGET_DIR="$PWD/target/coverage"
+
+main() {
+    export RUSTFLAGS="-Cinstrument-coverage"
+    export CARGO_TARGET_DIR="$TARGET_DIR"
+    # Absolute: cargo runs test binaries with cwd set to the package root,
+    # so a relative path scatters profraw into each crate's own directory.
+    export LLVM_PROFILE_FILE="$TARGET_DIR/profraw/%p-%m.profraw"
+
+    rm -rf "$TARGET_DIR" frork-*/target/coverage
+    # Not --workspace: frork-cli and frork-lua use incompatible mlua features.
+    cargo test -p frork-cli --quiet
+
+    local report
+    report=$(grcov "$TARGET_DIR/profraw" \
+        --binary-path "$TARGET_DIR/debug/" \
+        --source-dir . \
+        --output-types covdir \
+        --ignore-not-existing \
+        --keep-only 'frork-*/src/**' \
+        --ignore 'frork-*/src/main.rs' \
+        --excl-line 'cov-excl-line|unreachable!' \
+        --excl-start 'cov-excl-start' \
+        --excl-stop 'cov-excl-stop')
+
+    # Per-file breakdown: walk the covdir tree, printing leaf files only.
+    jq --raw-output '
+        def files:
+            to_entries[] | .value |
+            if .children then .children | files
+            else "\(.name): \(.coveragePercent)% (\(.linesCovered)/\(.linesTotal))"
+            end;
+        .children | files
+    ' <<<"$report"
+
+    # A crate with no coverable library lines yet (fresh scaffold)
+    # reports 0.0% for an empty set; that's a vacuous pass, not a failure.
+    local lines_total
+    lines_total=$(jq '.linesTotal' <<<"$report")
+    # Guard the arithmetic below: [[ -eq ]] evaluates a non-numeric string
+    # as 0, so a missing field would silently skip the gate.
+    if ! [[ "$lines_total" =~ ^[0-9]+$ ]]; then
+        echo "ERROR: expected a numeric linesTotal in the grcov report, got '${lines_total}'" >&2
+        echo "The report may not be covdir format — check grcov's --output-types flag." >&2
+        exit 1
+    fi
+    if [[ "$lines_total" -eq 0 ]]; then
+        echo "ERROR: no coverable lines found; --keep-only likely does not match the source layout" >&2
+        exit 1
+    fi
+
+    local coverage
+    coverage=$(jq '.coveragePercent' <<<"$report")
+    echo ""
+    echo "Total: ${coverage}%"
+
+    if [[ "$(echo "$coverage < $THRESHOLD" | bc -l)" -eq 1 ]]; then
+        echo "ERROR: coverage ${coverage}% is below ${THRESHOLD}%" >&2
+        exit 1
+    fi
+}
+
+main "$@"
diff --git a/justfile b/justfile
index f7b8039..f0eb8d6 100644
--- a/justfile
+++ b/justfile
@@ -3,53 +3,23 @@ default: all
 fmt:
     cargo fmt --all
 
+# Not --workspace: frork-cli and frork-lua use incompatible mlua features.
 check:
     cargo check -p frork-cli
 
 clippy:
-    cargo clippy -p frork-cli -- -D warnings
+    cargo clippy -p frork-cli --all-targets -- -D warnings
 
+# Report-only until assertions.rs has tests; raise the threshold as it climbs.
 coverage:
-    #!/usr/bin/env bash
-    set -euo pipefail
-    export RUSTFLAGS="-Cinstrument-coverage"
-    export CARGO_TARGET_DIR="target/coverage"
-    export LLVM_PROFILE_FILE="target/coverage/profraw/%p-%m.profraw"
-    rm -rf target/coverage
-    cargo test -p frork-cli -q
-    REPORT=$(grcov target/coverage/profraw \
-        --binary-path ./target/coverage/debug/ \
-        -s . \
-        -t covdir \
-        --ignore-not-existing \
-        --keep-only 'src/**' \
-        --ignore 'src/bin/**' \
-        --excl-line 'cov-excl-line|unreachable!' \
-        --excl-start 'cov-excl-start' \
-        --excl-stop 'cov-excl-stop')
-    echo "$REPORT" | jq -r '
-        def files:
-            to_entries[] | .value |
-            if .children then .children | files
-            else "\(.name): \(.coveragePercent)% (\(.linesCovered)/\(.linesTotal))"
-            end;
-        .children | files
-    '
-    COVERAGE=$(echo "$REPORT" | jq '.coveragePercent')
-    echo ""
-    echo "Total: ${COVERAGE}%"
-    # TODO: enforce 100% coverage
-    # if [ "$(echo "$COVERAGE < 100" | bc -l)" -eq 1 ]; then
-    #     echo "ERROR: Coverage is below 100%"
-    #     exit 1
-    # fi
+    COVERAGE_THRESHOLD=0 ./bin/coverage
 
 mutants:
     #!/usr/bin/env bash
     set -uo pipefail
     cargo mutants -p frork-cli --timeout-multiplier 3 -j4
     rc=$?
-    # 0 = all caught, 3 = timeouts (infinite loops from mutants, still caught)
+    # 0 = all caught, 3 = timeouts (infinite loops from mutants, still caught).
     if [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; then
         exit 0
     fi