diff --git a/.kiro/specs/dirty-content-fingerprint/tasks.md b/.kiro/specs/dirty-content-fingerprint/tasks.md index 6b06d808..67d0f0fe 100644 --- a/.kiro/specs/dirty-content-fingerprint/tasks.md +++ b/.kiro/specs/dirty-content-fingerprint/tasks.md @@ -6,11 +6,9 @@ - _Requirements: 1.1, 5.1_ - [ ] 2. Foundation: compute real fingerprint values at both build entry points -- [ ] 2.1 (P) xce-build.sh computes the dev-loop fingerprint, independently of commit_id - - `commit_id`'s existing computation (including its `-dirty` suffix) is **not touched** -- this task only adds a new, independent computation alongside it - - Compute `dirty_fingerprint` as: empty when the tree is clean, or when the tree is dirty but the hash computation itself fails; a short hex digest (sha256, truncated) over the concatenation of `git diff HEAD`'s output (tracked modifications, staged or unstaged) and the raw content of every file listed by `git ls-files --others --exclude-standard` (untracked, non-ignored files, in a stable sorted order) when the tree is dirty and the hash computation succeeds; zero working-tree mutation in all cases - - Append `-Ddirty_fingerprint="$fp"` to `MESON_OPTS` for every build profile, alongside the existing `-Dcommit_id=`/`-Dbuild_time=` entries - - Observable: editing a tracked file, running `/xce-build scheduler debug`, and checking `--version` shows a fingerprint suffix; editing again with different content and rebuilding shows a different fingerprint; reverting the file to its first edit's exact content and rebuilding reproduces the first fingerprint exactly; creating a new file matched by `.gitignore` and rebuilding leaves the fingerprint unchanged from the pre-creation value (Requirement 1.4's exclusion, exercised live, not just structurally implied by `git ls-files --others --exclude-standard`); the original feature's own `test-build-version-regression.sh` (which asserts a literal `-dirty` substring in `commit_id`'s output) continues to pass completely unmodified, confirming `commit_id` was never touched +- [x] 2.1 (P) xce-build.sh computes the dev-loop fingerprint, independently of commit_id + - `dirty_fingerprint` computed via `git diff HEAD` + `git ls-files -z --others --exclude-standard | LC_ALL=C sort -z` content, `sha256sum` truncated to 8 hex chars; empty on a clean tree or on computation failure (3 independently-verified graceful-degradation paths: mktemp failure, sha256sum failure, git diff failure -- none abort the script despite `set -euo pipefail`); zero working-tree/index mutation (read-only git invocations only); `commit_id`'s own computation left byte-for-byte untouched + - Observable confirmed live (independently re-verified by review, real builds with HEAD pinned to control for concurrent background commits): editing a tracked file and rebuilding produces a fingerprint; different content produces a different fingerprint; reverting to the first edit's exact bytes reproduces the first fingerprint exactly; a `.gitignore`-matched `*.bak` file leaves the fingerprint unchanged; `commit_id`'s `-dirty` suffix stayed intact across every build; every profile (asan/debug/release) carries the option; the original feature's unmodified `test-build-version-regression.sh` passes twice in full (asan+debug) - _Requirements: 1.1, 1.2, 1.3, 1.4, 5.1_ - _Boundary: xce-build.sh_ diff --git a/src/tools/xce-build.sh b/src/tools/xce-build.sh index 33ce0aa6..3acc8964 100755 --- a/src/tools/xce-build.sh +++ b/src/tools/xce-build.sh @@ -116,7 +116,56 @@ if [ -z "$build_time" ]; then echo "WARNING: xce-build.sh could not determine build_time (resolved empty) -- version info will be missing/incorrect in this build!" >&2 fi -MESON_OPTS+=(-Dcommit_id="$commit_id" -Dbuild_time="$build_time") +# dirty-content-fingerprint task 2.1: dirty_fingerprint, computed +# INDEPENDENTLY of commit_id above -- commit_id's own computation +# (including its -dirty suffix) is intentionally left untouched by this +# block; this only adds a new, parallel value. Empty when the tree is +# clean, or when the tree is dirty but the hash computation itself fails +# (Requirement 5.1 -- never fabricate a value; commit_id's own untouched +# -dirty suffix already signals "modified" in that fallback case, consumed +# by a later task's --version composition, not here). When the tree is +# dirty and the computation succeeds: a short sha256 prefix over `git diff +# HEAD`'s output (tracked modifications, staged or unstaged) concatenated +# with the raw content of every file `git ls-files --others +# --exclude-standard` lists (untracked, non-ignored -- Requirement 1.4), +# in a stable sorted order for determinism. Zero working-tree mutation -- +# reads only, no git add/stash/anything that touches the index or working +# tree. +dirty_fingerprint="" +if [ -n "$(git status --porcelain 2>/dev/null)" ]; then + fp_ok=1 + fp_tmp="" + list_tmp="" + if ! fp_tmp="$(mktemp 2>/dev/null)" || [ -z "$fp_tmp" ]; then + fp_ok=0 + fi + if [ "$fp_ok" = 1 ] && ! git diff HEAD >"$fp_tmp" 2>/dev/null; then + fp_ok=0 + fi + if [ "$fp_ok" = 1 ]; then + if ! list_tmp="$(mktemp 2>/dev/null)" || [ -z "$list_tmp" ]; then + fp_ok=0 + elif ! git ls-files -z --others --exclude-standard 2>/dev/null | LC_ALL=C sort -z >"$list_tmp"; then + fp_ok=0 + else + while IFS= read -r -d '' f; do + if ! cat -- "$f" >>"$fp_tmp" 2>/dev/null; then + fp_ok=0 + break + fi + done <"$list_tmp" + fi + fi + if [ "$fp_ok" = 1 ]; then + if fp="$(sha256sum <"$fp_tmp" 2>/dev/null | cut -c1-8)" && [ -n "$fp" ]; then + dirty_fingerprint="$fp" + fi + fi + [ -n "$fp_tmp" ] && rm -f "$fp_tmp" + [ -n "$list_tmp" ] && rm -f "$list_tmp" +fi + +MESON_OPTS+=(-Dcommit_id="$commit_id" -Dbuild_time="$build_time" -Ddirty_fingerprint="$dirty_fingerprint") usage() { echo "Usage: $0 [component] [profile|build_dir] [build_dir] [--incremental]"