|
| 1 | +# CSL manifest lockfile false-positive on Windows incremental builds |
| 2 | + |
| 3 | +Strand: bd-2w80 ("Investigate CSL manifest test failure after rebase") — **closed**. |
| 4 | + |
| 5 | +## Resolution (final) |
| 6 | + |
| 7 | +Neither research agent's mechanism theory panned out. Empirical repro (add |
| 8 | +then remove a fixture file, no `cargo clean`) showed directory-level |
| 9 | +`rerun-if-changed` correctly triggers a rebuild both ways on cargo |
| 10 | +1.97.0-nightly — the "directory watch misses changes" theories (both the |
| 11 | +pre-1.50-citation one and the mtime-granularity one) were never confirmed as |
| 12 | +the actual cause. Bigger finding: the baked `expected` string only ever |
| 13 | +depended on file names/counts, never content — so the per-file |
| 14 | +`rerun-if-changed` fix originally planned below **would not have prevented |
| 15 | +this failure even if applied**, since it only helps with content-edit |
| 16 | +detection. |
| 17 | + |
| 18 | +Actual fix shipped: stopped baking the validation state at build time |
| 19 | +entirely. `csl_validate_manifest` is now a hand-written runtime test in |
| 20 | +`tests/integration/csl_conformance.rs` that reads `test-data/csl-suite/` + |
| 21 | +`tests/enabled_tests.txt` live at test-run time (via |
| 22 | +`env!("CARGO_MANIFEST_DIR")`, a compile-time *path* constant, not a baked |
| 23 | +*computed value*) and compares against `tests/csl_conformance.lock` in the |
| 24 | +same instant. No time-of-check/time-of-use gap, no dependency on |
| 25 | +build-script rerun timing for this check at all. `build.rs` now only handles |
| 26 | +per-fixture test codegen (unaffected — verified byte-identical by an |
| 27 | +independent review pass). Full crate suite green: 863 passed, 0 failed. |
| 28 | + |
| 29 | +The per-file `rerun-if-changed` hardening for `quarto-citeproc` + |
| 30 | +`quarto-sass` (checklist items below) was **not applied** — no reproducible |
| 31 | +defect was ever found for that angle, so it would have been unproven |
| 32 | +defensive code. |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Original investigation (kept for history) |
| 37 | + |
| 38 | +## Overview |
| 39 | + |
| 40 | +`quarto-citeproc::csl_conformance::csl_validate_manifest` failed on a Windows |
| 41 | +incremental build with "Lockfile mismatch: tests/csl_conformance.lock", even |
| 42 | +though `git status` showed `tests/enabled_tests.txt` and `test-data/csl-suite/` |
| 43 | +clean, and regenerating the lockfile (`UPDATE_CSL_LOCKFILE=1`) produced a |
| 44 | +byte-identical file. `cargo clean -p quarto-citeproc` + fresh build made the |
| 45 | +test pass with zero lockfile changes. Conclusion: `build.rs` served a stale |
| 46 | +baked `expected` value from an old `generated_csl_tests.rs`, not a real data |
| 47 | +divergence. |
| 48 | + |
| 49 | +Two research passes (Opus, repo-specific; Sonnet, ecosystem-wide) looked at |
| 50 | +*why* `build.rs`'s `cargo:rerun-if-changed=test-data/csl-suite` (a directory, |
| 51 | +not per-file) failed to trigger a rebuild, and disagree on mechanism: |
| 52 | + |
| 53 | +- **Opus**: cited [rust-lang/cargo#2599](https://github.com/rust-lang/cargo/issues/2599) |
| 54 | + — directory `rerun-if-changed` only tracks the directory's own mtime, misses |
| 55 | + in-place file edits. |
| 56 | +- **Sonnet**: found #2599 was *fixed* by |
| 57 | + [rust-lang/cargo#8973](https://github.com/rust-lang/cargo/pull/8973), merged |
| 58 | + 2020-12-14, shipped in **Cargo 1.50**. Since then, directory |
| 59 | + `rerun-if-changed` recursively scans and takes the max mtime of every file |
| 60 | + inside. Current repo toolchain is `nightly-2026-04-28` → `cargo |
| 61 | + 1.97.0-nightly` — Opus's cited mechanism is stale for the cargo version we |
| 62 | + actually run. |
| 63 | + |
| 64 | +So the "directory watch is shallow" story is **not confirmed** for our cargo |
| 65 | +version. Sonnet's alternate candidates: mtime-granularity/timestamp-truncation |
| 66 | +bugs Cargo has open elsewhere (e.g. |
| 67 | +[#13119](https://github.com/rust-lang/cargo/issues/13119), WSL/9p nanosecond |
| 68 | +truncation; [#9445](https://github.com/rust-lang/cargo/issues/9445), relative |
| 69 | +path normalization in dep-info) are more plausible, but neither is a confirmed |
| 70 | +match either — they're precedent for "Cargo's rerun-if-changed freshness check |
| 71 | +is fragile on Windows-adjacent filesystems in general," not a proven cause |
| 72 | +here. |
| 73 | + |
| 74 | +**Before writing any public rationale (tracker comment, commit message), we |
| 75 | +need to settle the actual mechanism empirically on this machine** — not ship a |
| 76 | +fix with a plausible-sounding but unverified "why." |
| 77 | + |
| 78 | +The concrete fix both agents converge on regardless of mechanism — enumerate |
| 79 | +every fixture file explicitly for `rerun-if-changed` instead of relying solely |
| 80 | +on the directory line — is correct either way: it's strictly more precise than |
| 81 | +Cargo's own directory scan, and matches an existing in-repo pattern |
| 82 | +(`watch_recursive()` in `quarto-trace-server`, `quarto-preview`, |
| 83 | +`quarto-mcp-launcher` build.rs files). `quarto-sass/build.rs` has the same |
| 84 | +directory-only gap (`resources/scss`, `src`) and has not yet manifested a |
| 85 | +failure — worth fixing in the same pass since it's the same category of bug. |
| 86 | + |
| 87 | +## Checklist |
| 88 | + |
| 89 | +- [ ] **Reproduce mechanism empirically.** With current `generated_csl_tests.rs` |
| 90 | + freshly built (no dirty cache), touch *only the mtime and content* of one |
| 91 | + existing file in `test-data/csl-suite/` (no add/remove), then run |
| 92 | + `cargo nextest run -p quarto-citeproc csl_validate_manifest` without |
| 93 | + `cargo clean` first. If build.rs re-runs and the test still passes (no |
| 94 | + new fixture added, so `expected` shouldn't change anyway) — need a |
| 95 | + variant that actually changes what's baked, e.g. add a fixture to |
| 96 | + `test-data/csl-suite/` *and* to `enabled_tests.txt`, then only touch |
| 97 | + mtime-adjacent files to see if a plain incremental build (no clean) |
| 98 | + picks up the addition. Confirm whether Cargo actually re-invokes |
| 99 | + build.rs (check via `cargo build -vv -p quarto-citeproc | grep |
| 100 | + "Running.*build-script"` or by adding a temporary `eprintln!` in |
| 101 | + build.rs) or serves stale output. |
| 102 | +- [ ] **Identify actual trigger** for the original failure if possible — was it |
| 103 | + a `git rebase`/checkout that touched files at/before Cargo's cached |
| 104 | + build-script timestamp? Check `git reflog` / recent branch history if |
| 105 | + still reconstructable; otherwise document as "not reproduced from a |
| 106 | + clean baseline, only observed once" and rely on the mechanism test above. |
| 107 | +- [ ] **Apply per-file `rerun-if-changed` fix** to `crates/quarto-citeproc/build.rs`: |
| 108 | + after `test_files` is collected and sorted (existing code, ~line 36), |
| 109 | + loop over it and emit `cargo:rerun-if-changed=<path>` per fixture. Keep |
| 110 | + the existing directory line (still needed/harmless for catching |
| 111 | + additions/removals). No new `walkdir` dependency needed — `csl-suite/` |
| 112 | + is flat, and the file list is already collected. |
| 113 | +- [ ] **Apply the same fix to `crates/quarto-sass/build.rs`** (`resources/scss` |
| 114 | + and `src` directory watches) — same latent gap, not yet triggered. |
| 115 | + Confirm whether that build.rs already collects a file list to reuse, or |
| 116 | + needs its own walk (check if `walkdir` is already a build-dependency |
| 117 | + there for the sibling crates' pattern). |
| 118 | +- [ ] **Verify per project TDD/bug-fix rules**: this is build-script infra, not |
| 119 | + app logic, so the "test" is the reproduction procedure itself (already |
| 120 | + have one: `cargo clean -p quarto-citeproc` fixes it; need the |
| 121 | + no-clean-incremental repro from step 1 to prove the *fix*, not just the |
| 122 | + symptom, is addressed). Confirm fixed build.rs reruns correctly on a |
| 123 | + fixture add without requiring `cargo clean`. |
| 124 | +- [ ] **Full workspace verify**: `cargo build --workspace`, `cargo nextest run |
| 125 | + --workspace`, `cargo xtask verify --skip-hub-build` (Rust-only change). |
| 126 | +- [ ] **Correct the existing bd-2w80 comment.** The comment already posted |
| 127 | + claims "cargo's directory-mtime dependency tracking is known to be |
| 128 | + unreliable on Windows (NTFS mtime semantics differ from Linux ext4)" — |
| 129 | + that framing is Opus's pre-1.50 citation, not confirmed against our |
| 130 | + actual cargo version. Post a follow-up comment with the corrected |
| 131 | + mechanism once step 1 settles it, before/alongside closing. |
| 132 | +- [ ] **Decide bd-2w80 disposition**: close with the confirmed root cause + |
| 133 | + fix, or split the `quarto-sass` sibling fix into its own strand |
| 134 | + (`discovered-from` link) if it's not bundled into the same PR. |
| 135 | +- [ ] **Commit.** Stage `crates/quarto-citeproc/build.rs`, |
| 136 | + `crates/quarto-sass/build.rs` (if touched). Do not push without explicit |
| 137 | + approval per project git policy. |
| 138 | + |
| 139 | +## Notes / open questions |
| 140 | + |
| 141 | +- Both research agents agree on the *fix* even while disagreeing on the |
| 142 | + *mechanism* — low risk either way, but the tracker comment and commit |
| 143 | + message should state only what we've actually verified on this cargo |
| 144 | + version, not repeat either agent's unverified citation. |
| 145 | +- Dispatch-prompt gap noticed mid-investigation: non-fork subagents (Explore, |
| 146 | + general-purpose) inherit zero context from `~/.claude/rules/*.md` or project |
| 147 | + `CLAUDE.md` — the sonnet research agent used `curl` for GitHub source |
| 148 | + reads instead of `gh repo read-file`/`read-dir` because the dispatch prompt |
| 149 | + never mentioned that preference. Not fixed as part of this plan; noted for |
| 150 | + future dispatch-prompt hygiene. |
0 commit comments