From 4c75114a9228e64fef293e9bbb62403cdb6cfe03 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 13:04:25 +0000 Subject: [PATCH 1/3] Switch EXE fast-path freshness check from mtime to content-hash (Item 39) The freshness check was mtime-only over *.py files, so a timestamp-preserving delivery method (a ZIP, xcopy, robocopy) could carry a genuinely changed file whose mtime still predated the built EXE, silently reusing stale logic with no signal to the user -- and requirements.txt/pyproject.toml/runtime.txt changes were invisible to the scan entirely, mtime or not. HP_FAST_CHECK (now with a canonical tools/fast_check.ps1 source) hashes the same *.py file set already scanned, extended to those three dependency files, and compares against a stored ~fast_check.hash.txt via a new :write_fast_hash subroutine called from :success (gated on HP_FASTPATH_USED so the fast-reuse case never pays a redundant second hash pass). Uses [System.Security.Cryptography.SHA256] directly, not Get-FileHash, per this repo's own Utility-module-cmdlet-autoload lesson. Adds tests/selfapps_fastpath_hash.ps1 (uv lane, non-gating for first landing) -- the exact backdated-mtime coverage gap the item asked for -- plus tests/test_fast_check.py unit-testing the isolated script via real pwsh, and a static wiring guard in harness.ps1. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017SQ1rvJxDbE71pTXJ4QvLV --- .github/workflows/batch-check.yml | 15 +++ CLAUDE.md | 33 +++++++ docs/agent-interconnect.md | 20 ++++ docs/agent-ndjson.md | 32 +++++++ run_setup.bat | 42 +++++++-- tests/harness.ps1 | 12 +++ tests/selfapps_fastpath_hash.ps1 | 152 ++++++++++++++++++++++++++++++ tests/test_fast_check.py | 111 ++++++++++++++++++++++ tools/fast_check.ps1 | 73 ++++++++++++++ 9 files changed, 480 insertions(+), 10 deletions(-) create mode 100644 tests/selfapps_fastpath_hash.ps1 create mode 100644 tests/test_fast_check.py create mode 100644 tools/fast_check.ps1 diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index 17548259..82364118 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -627,6 +627,21 @@ jobs: run: | & tests\selfapps_warnfix_venv_repair.ps1 + # CLAUDE.md Active Backlog Item 39: the EXE fast path's freshness check switched from + # mtime-only to a content-hash comparison, so a genuinely changed source file whose + # mtime is backdated (the ZIP/xcopy/robocopy delivery scenario) is no longer silently + # treated as fresh. uv lane only (non-gating for first landing -- the isolated + # tools/fast_check.ps1 script is already verified locally via real pwsh, but this is + # the first time the full :try_fast_exe -> :run_entry_smoke -> :success -> + # :write_fast_hash cycle runs end-to-end on real Windows CI). See + # selfapps_fastpath_hash.ps1's own header comment for the full mechanism. + - name: "Self-test: EXE fast path detects a backdated-mtime content change (uv lane only)" + if: ${{ matrix.mode == 'uv' }} + continue-on-error: true + shell: pwsh + run: | + & tests\selfapps_fastpath_hash.ps1 + # REQ-005.11: PEP 723 header write-back (uv add --script). v1-scoped to HP_ENV_MODE=uv, # so wired to the dedicated uv lane only (non-gating) for this first CI pass rather than # the gating real lane -- lets these brand-new scenarios prove out in real Windows CI diff --git a/CLAUDE.md b/CLAUDE.md index 0783f4b5..7214206d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -865,6 +865,39 @@ but several represent real gaps worth closing before calling the path fully rele **Coverage gap to close in the same slice**: no scenario backdates a source file's mtime below the EXE's to test this. Add one. + **Implemented: freshness is now a content-hash comparison, closing both exposures at once.** + `HP_FAST_CHECK` (canonical source `tools/fast_check.ps1`, previously inline-only with no + `tools/` file) now hashes the SAME `*.py` file set already scanned (unchanged filter), extended + to include `requirements.txt`/`pyproject.toml`/`runtime.txt` when present -- a combined SHA256 + over each file's own `relpath|filehash` line, sorted by path for determinism. This structurally + cannot be fooled by a preserved/backdated mtime (exposure a) and now sees dependency-file + changes regardless of mtime (exposure b), closing both suggested fix directions at once rather + than picking one. Two modes via a second positional arg: `check` (default, called from + `:try_fast_exe`, unchanged call site) prints `fresh` iff a stored `~fast_check.hash.txt` exists + and matches; a missing stored hash (first run under this fix, or an EXE built by an older + `run_setup.bat`) is a safe, one-time-cost "not fresh" default, never a false "fresh." `write` + (new subroutine `:write_fast_hash`, called from `:success`) unconditionally (re)writes the + hash after a genuine fresh build attempt -- gated on `HP_FASTPATH_USED` being unset so the + already-fast reuse case never pays a redundant second hash pass (the `:try_fast_exe` check + already computed it once; nothing changed, so the stored value is already correct). Uses + `[System.Security.Cryptography.SHA256]` directly, not `Get-FileHash`, per this repo's own + "Prefer raw .NET types over Utility-module cmdlets" lesson (Utility-module auto-load is not + guaranteed in the `-File` invocation shape embedded helpers run under on real Windows + PowerShell 5.1, invisible to local `pwsh`/Linux testing). + + **Coverage gap closed**: `tests/selfapps_fastpath_hash.ps1` (uv lane, non-gating for first + landing) is the literal scenario this item asked for -- run 1 builds the EXE printing a V1 + token; entry.py is rewritten to print V2 and its mtime backdated to 2001-09-09 (well before + the EXE's own mtime) before run 2; asserts run 2's captured EXE stdout shows V2 (not V1) and + the "Fast path: reusing" line is absent, proving a genuine rebuild happened rather than the + old mtime-only check wrongly reusing the stale EXE. `tests/test_fast_check.py` unit-tests the + isolated script directly via real `pwsh` (6 scenarios: no stored hash, write-then-fresh, + backdated-mtime content change, dependency-file-only change, rewrite-after-change, missing + EXE) plus a `PayloadSync` byte-equality check against `tools/fast_check.ps1`. + `tests/harness.ps1`'s `batch.fastpath.hash_write` statically guards the `:write_fast_hash` + wiring (label exists, called from `:success` with the `HP_FASTPATH_USED` gate, invokes the + payload in `write` mode). See `docs/agent-ndjson.md` for the full row registry entry. + - **Item 42: console output is verbose across all log levels, and every fresh build ends with two unexplained Y/N prompts -- both plausibly overwhelming for the actual target audience (beginners with no setup experience).** Confirmed reasoned-from-source and CI capture. New; not diff --git a/docs/agent-interconnect.md b/docs/agent-interconnect.md index 01c3178e..09ffc82b 100644 --- a/docs/agent-interconnect.md +++ b/docs/agent-interconnect.md @@ -1184,6 +1184,26 @@ individual test file pins it locally -- see the "Accepted gap" entry in path is completely provider-independent. The test correctly validates the EXE fast path, not the env-state or uv venv fast path. +**`HP_FAST_CHECK`'s own freshness signal (CLAUDE.md Active Backlog Item 39): content-hash, not +mtime -- read side and write side are two separate call sites, must stay paired.** `:try_fast_exe` +(read/`check` mode, unchanged call site) compares a stored `~fast_check.hash.txt` against a +composite SHA256 over the same non-infra `*.py` files already scanned, extended to +`requirements.txt`/`pyproject.toml`/`runtime.txt` when present. **The write side is a SEPARATE +subroutine, `:write_fast_hash`, called from `:success`** (not inlined there -- a set-then-read of +the same var inside one parenthesized block would hit the classic parse-time-`%VAR%`-expansion +trap, see `docs/agent-lessons-learned.md`), gated on `if not defined HP_FASTPATH_USED` so the +already-fast reuse case never pays a redundant second hash pass -- `:try_fast_exe`'s own `check` +call already computed the same hash once this run; if it matched (fast path taken), the stored +value is already correct and rewriting it would be pure waste. **Any future change to either the +scanned file set or the hash algorithm must update BOTH `:try_fast_exe`'s check invocation and +`:write_fast_hash`'s write invocation identically** -- they share the single `tools/fast_check.ps1` +script (mode-dispatched via a second positional arg), so a change to the shared script's file +enumeration or hashing logic automatically stays in sync between the two call sites; only a +change to the CALL SITES themselves (e.g. adding a new mode, or passing different exe/cwd context) +risks the two drifting apart. A missing stored hash (first run after upgrading to this fix, or an +EXE built by an older `run_setup.bat` with no hash file at all) is a safe "not fresh" default -- +forces exactly one rebuild, never a false "fresh." + ### ~dependency_installed.txt: pip freeze output and its consumers `~dependency_installed.txt` is written after install via `pip freeze` (run_setup.bat lines 1122-1134): diff --git a/docs/agent-ndjson.md b/docs/agent-ndjson.md index 0beaa7e3..d1472c48 100644 --- a/docs/agent-ndjson.md +++ b/docs/agent-ndjson.md @@ -197,6 +197,7 @@ batch.conda.probe.deferred, uv.python.preference.configured, batch.script_root.trailing_backslash, batch.progress.conda_create, batch.progress.pyi_build, batch.progress.dep_install, +batch.fastpath.hash_write, batch.pyi.collect.precheck, batch.pyi.hidden_import.recover, batch.smoke.kill_warn, @@ -991,6 +992,37 @@ Non-gating for its first landing, matching this repo's established graduation pa self.exe.warnfix.venv_repair ``` +## selfapps-fastpath-hash NDJSON rows (selfapps_fastpath_hash.ps1, uv lane only, non-gating) + +CLAUDE.md Active Backlog Item 39: the EXE fast path's freshness check (`HP_FAST_CHECK`, +`tools/fast_check.ps1`) switched from mtime-only over `*.py` files to a content-hash +comparison over the same file set, extended to `requirements.txt`/`pyproject.toml`/ +`runtime.txt`. Closes two exposures: (a) a timestamp-preserving delivery method (a ZIP, +xcopy, robocopy) carrying a genuinely changed file whose mtime still predates the built +EXE was previously silently treated as fresh; (b) a dependency-file-only change was +previously invisible to the scan regardless of mtime. `:write_fast_hash` (called from +`:success`, gated on `HP_FASTPATH_USED` being unset so the already-fast reuse case never +pays a redundant re-hash pass) writes the stored hash after a genuine fresh build attempt. + +This test is the coverage-gap Item 39 itself named ("no scenario backdates a source file's +mtime below the EXE's to test this"): run 1 builds the EXE from an entry file printing a V1 +token; entry.py is then rewritten to print a V2 token and its mtime is set to 2001-09-09 +(well before `dist\.exe`'s own mtime) before run 2. Asserts run 2's captured EXE +stdout (`~run.out.txt`, written by both the fast-path-reuse launch and the fresh-build +smokerun) shows the V2 token, not V1 -- direct evidence a genuine rebuild happened, not +just that some log line says "rebuilding" -- and that the "Fast path: reusing" log line is +absent. The isolated `tools/fast_check.ps1` script itself is unit-tested directly via real +`pwsh` in `tests/test_fast_check.py` (6 scenarios: no stored hash, write-then-fresh, +backdated-mtime content change, dependency-file-only change, rewrite-after-change, missing +EXE) -- this selfapps test is what proves the full `:try_fast_exe -> :run_entry_smoke -> +:success -> :write_fast_hash` cycle wires correctly end-to-end on real Windows CI, which +`test_fast_check.py` alone cannot. Non-gating for its first landing, matching this repo's +established graduation pattern (see CLAUDE.md's "CI lane gating maturity" periodic check). + +``` +self.fastpath.hash.backdated_mtime +``` + ## selfapps-cache-selfheal NDJSON rows (test_ci_cache_selfheal.ps1, `real` lane only, GATING) Item 19 follow-on (docs/agent-closed-backlog.md): the cache-lane self-heal logic diff --git a/run_setup.bat b/run_setup.bat index ea9859f4..d3c0682e 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -2186,6 +2186,10 @@ if not defined HP_FASTPATH_USED ( rem other console output. call :print_fastpath_ambiguous_note ) +rem CLAUDE.md Active Backlog Item 39: only after a genuine fresh build attempt this run +rem (HP_FASTPATH_USED unset) -- the fast-reuse case already means the stored hash matches +rem current sources, so writing again here would be a pure, wasted redundant re-hash. +if not defined HP_FASTPATH_USED call :write_fast_hash call :release_lock rem REQ-016: retain terminal window on success so user can read the output. if not defined HP_CI_LANE ( @@ -3572,6 +3576,25 @@ if not "%HP_SMOKE_RC%"=="0" if not defined HP_PROBE_EXCEEDED ( set "HP_SMOKE_RC=" ) exit /b 0 +:write_fast_hash +rem derived requirement (CLAUDE.md Active Backlog Item 39): after a genuine fresh build +rem attempt (never after a fast-path reuse -- see the :success call site's own +rem HP_FASTPATH_USED gate, which means sources already match the stored hash and a +rem rewrite would be a redundant, wasted re-hash pass), (re)write the composite +rem source-content hash the EXE fast path's freshness check compares against on the next +rem run. A dedicated subroutine, not inlined at :success, specifically so no line here +rem sits inside a parenthesized block -- see docs/agent-lessons-learned.md's "Parse-time +rem vs. runtime variable expansion" entry for why a set-then-read-in-the-same-block +rem pattern is unsafe, and this repo's own house preference for goto/call-based dispatch +rem over inlined blocks wherever a payload emit + invoke sequence is involved. +if not exist "dist\%ENVNAME%.exe" exit /b 0 +set "HP_FAST_CHECK_PS=~fast_check.ps1" +if exist "%HP_FAST_CHECK_PS%" del "%HP_FAST_CHECK_PS%" >nul 2>&1 +call :emit_from_base64 "%HP_FAST_CHECK_PS%" HP_FAST_CHECK +if errorlevel 1 exit /b 0 +powershell -NoProfile -ExecutionPolicy Bypass -File "%HP_FAST_CHECK_PS%" "dist\%ENVNAME%.exe" write >> "%LOG%" 2>&1 +if exist "%HP_FAST_CHECK_PS%" del "%HP_FAST_CHECK_PS%" >nul 2>&1 +exit /b 0 :run_entry_smoke call :record_chosen_entry "%HP_ENTRY%" rem CLAUDE.md Active Backlog Item 45: a failed env-create can fall through :die (exit /b @@ -5082,17 +5105,16 @@ set "HP_PYPROJ_DEPS=IiIicHlwcm9qX2RlcHMgKEhQX1BZUFJPSl9ERVBTKSAtLSBleHRyYWN0cyBb set "HP_CONDARC=Y2hhbm5lbHM6CiAgLSBjb25kYS1mb3JnZQpjaGFubmVsX3ByaW9yaXR5OiBzdHJpY3QKc2hvd19jaGFubmVsX3VybHM6IHRydWUK" set "HP_DETECT_PY=IiIiZGV0ZWN0X3B5dGhvbiAoSFBfREVURUNUX1BZKSAtLSBSRVEtMDA0IFRpZXIgMS8yIHJlcXVpcmVzLXB5dGhvbiBkZXRlY3Rvci4KClJ1biBmcm9tIHRoZSBhcHBsaWNhdGlvbiBkaXJlY3Rvcnk7IHByaW50cyBhIG5vcm1hbGl6ZWQgY29uZGEtc3ludGF4CiJweXRob248b3A+PHZlcj5bLC4uLl0iIGNvbnN0cmFpbnQgdG8gc3Rkb3V0LCBvciBhbiBlbXB0eSBsaW5lIGlmIG5laXRoZXIKdGllciBmb3VuZCBhbnl0aGluZyAodGhlIGNhbGxlciB0aGVuIGxldHMgdGhlIHNlbGVjdGVkIHByb3ZpZGVyIHBpY2sgdGhlCmxhdGVzdCBhdmFpbGFibGUgUHl0aG9uIC0tIFJFUS0wMDQgVGllciAzLCBoYW5kbGVkIG91dHNpZGUgdGhpcyBoZWxwZXIpLgpEZXRlY3Rpb24gb3JkZXI6CgogIDEuIHJ1bnRpbWUudHh0IC0tIGEgYmFyZSAicHl0aG9uLVguWVsuWl0iIG9yICJYLllbLlpdIiBsaW5lIHBpbnMgYW4gZXhhY3QKICAgICBtaW5vciB2ZXJzaW9uIChweXRob249WC5ZOyB0aGUgcGF0Y2ggY29tcG9uZW50LCBpZiBwcmVzZW50LCBpcyBub3QKICAgICBmb3J3YXJkZWQgLS0gcHJvdmlkZXJzIHBpbiBieSBtaW5vciBvbmx5KS4KICAyLiBweXByb2plY3QudG9tbCdzIFtwcm9qZWN0XSByZXF1aXJlcy1weXRob24gLS0gYSBQRVAgNDQwIHNwZWNpZmllcgogICAgIChlLmcuICI+PTMuMTAsPDQiLCAifj0zLjExIikgaXMgcGFyc2VkIGFuZCBleHBhbmRlZCBpbnRvIG9uZSBvciBtb3JlCiAgICAgY29uZGEtc3ludGF4IGNsYXVzZXMgdmlhIHBlcDQ0MF90b19jb25kYSgpLgoKcGVwNDQwX3RvX2NvbmRhKCkgYWxzbyBkb3VibGVzIGFzIGEgc3RhbmRhbG9uZSBDTEkgdXRpbGl0eSAoc2VlIG1haW4oKSkgZm9yCnRyYW5zbGF0aW5nIGFyYml0cmFyeSBQRVAgNDQwIHNwZWNpZmllcnMsIGluY2x1ZGluZyB0aGUgIn49IiBjb21wYXRpYmxlLXJlbGVhc2UKb3BlcmF0b3IsIHdoaWNoIGNvbmRhIGhhcyBubyBuYXRpdmUgZXF1aXZhbGVudCBmb3IgYW5kIG11c3QgYmUgZXhwYW5kZWQgaW50bwphbiBleHBsaWNpdCA+PS88IHJhbmdlLgoKVGhpcyBpcyB0aGUgY2Fub25pY2FsIHNvdXJjZSBmb3IgdGhlIEhQX0RFVEVDVF9QWSBiYXNlNjQgcGF5bG9hZCBlbWJlZGRlZCBpbgpydW5fc2V0dXAuYmF0LiBBZnRlciBlZGl0aW5nLCByZS1lbmNvZGUgYW5kIHBhc3RlIGl0IGludG8gdGhlCmBzZXQgIkhQX0RFVEVDVF9QWT0uLi4iYCBsaW5lOyB0ZXN0cy90ZXN0X2RldGVjdF9weXRob24ucHkgYXNzZXJ0cyB0aGUKZW1iZWRkZWQgcGF5bG9hZCBtYXRjaGVzIHRoaXMgZmlsZS4KIiIiCl9fdmVyc2lvbl9fID0gImRldGVjdF9weXRob24gdjIgKDIwMjUtMDktMjQpIgpfX2FsbF9fID0gWyJwZXA0NDBfdG9fY29uZGEiLCAiZGV0ZWN0X3JlcXVpcmVzX3B5dGhvbiIsICJtYWluIl0KT1JERVIgPSB7Ij09IjogMCwgIiE9IjogMSwgIj49IjogMiwgIj4iOiAzLCAiPD0iOiA0LCAiPCI6IDV9CgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzeXMKCiMgSGVscGVyIGltcGxlbWVudHMgdGhlIFJFQURNRSBib290c3RyYXAgY29udHJhY3QuIFBFUCA0NDAgZGV0YWlsczoKIyBodHRwczovL3BlcHMucHl0aG9uLm9yZy9wZXAtMDQ0MC8KCkNEID0gb3MuZ2V0Y3dkKCkKUlVOVElNRV9QQVRIID0gb3MucGF0aC5qb2luKENELCAicnVudGltZS50eHQiKQpQWVBST0pFQ1RfUEFUSCA9IG9zLnBhdGguam9pbihDRCwgInB5cHJvamVjdC50b21sIikKUFlQUk9KRUNUX1JFID0gcmUuY29tcGlsZSgicmVxdWlyZXMtcHl0aG9uXFxzKj1cXHMqWydcIl0oW14nXCJdKylbJ1wiXSIsIHJlLklHTk9SRUNBU0UpClNQRUNfUEFUVEVSTiA9IHJlLmNvbXBpbGUocicofj18PT18IT18Pj18PD18Pnw8KVxzKihbMC05XSsoPzpcLlswLTldKykqKScpCgoKZGVmIHZlcnNpb25fa2V5KHRleHQ6IHN0cik6CiAgICAiIiJSZXR1cm4gYSB0dXBsZSB1c2FibGUgZm9yIG51bWVyaWMgb3JkZXJpbmcgb2YgZG90dGVkIHZlcnNpb25zLiIiIgogICAgcGFydHMgPSBbXQogICAgZm9yIGNodW5rIGluIHRleHQuc3BsaXQoJy4nKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIHBhcnRzLmFwcGVuZChpbnQoY2h1bmspKQogICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBwYXJ0cy5hcHBlbmQoMCkKICAgIHJldHVybiB0dXBsZShwYXJ0cykKCgpkZWYgYnVtcF9mb3JfY29tcGF0aWJsZSh2ZXJzaW9uOiBzdHIpIC0+IHN0cjoKICAgICIiIlRyYW5zbGF0ZSB0aGUgUEVQIDQ0MCBjb21wYXRpYmxlIHJlbGVhc2UgdXBwZXIgYm91bmQuIiIiCiAgICBwaWVjZXMgPSBbaW50KGl0ZW0pIGZvciBpdGVtIGluIHZlcnNpb24uc3BsaXQoJy4nKSBpZiBpdGVtLmlzZGlnaXQoKV0KICAgIGlmIG5vdCBwaWVjZXM6CiAgICAgICAgcmV0dXJuIHZlcnNpb24KICAgIGlmIGxlbihwaWVjZXMpID49IDM6CiAgICAgICAgcmV0dXJuIGYie3BpZWNlc1swXX0ue3BpZWNlc1sxXSArIDF9IgogICAgaWYgbGVuKHBpZWNlcykgPT0gMjoKICAgICAgICByZXR1cm4gZiJ7cGllY2VzWzBdICsgMX0uMCIKICAgIHJldHVybiBzdHIocGllY2VzWzBdICsgMSkKCgpkZWYgZXhwYW5kX2NsYXVzZShvcDogc3RyLCB2ZXJzaW9uOiBzdHIpOgogICAgaWYgb3AgPT0gIn49IjoKICAgICAgICB1cHBlciA9IGJ1bXBfZm9yX2NvbXBhdGlibGUodmVyc2lvbikKICAgICAgICByZXR1cm4gWygiPj0iLCB2ZXJzaW9uKSwgKCI8IiwgdXBwZXIpXQogICAgcmV0dXJuIFsob3AsIHZlcnNpb24pXQoKCmRlZiBwZXA0NDBfdG9fY29uZGEoc3BlYzogc3RyKSAtPiBzdHI6CiAgICAiIiJSZXR1cm4gInB5dGhvbiIgY29uc3RyYWludHMgZXhwYW5kZWQgZnJvbSBhIHJlcXVpcmVzLXB5dGhvbiBzcGVjLiIiIgogICAgY2xhdXNlcyA9IFtdCiAgICBmb3IgcmF3IGluIHNwZWMuc3BsaXQoJywnKToKICAgICAgICByYXcgPSByYXcuc3RyaXAoKQogICAgICAgIGlmIG5vdCByYXc6CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgbWF0Y2ggPSBTUEVDX1BBVFRFUk4ubWF0Y2gocmF3KQogICAgICAgIGlmIG5vdCBtYXRjaDoKICAgICAgICAgICAgY29udGludWUKICAgICAgICBvcCwgdmVyc2lvbiA9IG1hdGNoLmdyb3VwcygpCiAgICAgICAgY2xhdXNlcy5leHRlbmQoZXhwYW5kX2NsYXVzZShvcCwgdmVyc2lvbikpCiAgICBpZiBub3QgY2xhdXNlczoKICAgICAgICByZXR1cm4gIiIKICAgIGRlZHVwID0ge30KICAgIGZvciBvcCwgdmVyc2lvbiBpbiBjbGF1c2VzOgogICAgICAgIGRlZHVwWyhvcCwgdmVyc2lvbildID0gKG9wLCB2ZXJzaW9uKQogICAgb3JkZXJlZCA9IHNvcnRlZChkZWR1cC52YWx1ZXMoKSwga2V5PWxhbWJkYSBpdGVtOiAoT1JERVIuZ2V0KGl0ZW1bMF0sIDk5KSwgdmVyc2lvbl9rZXkoaXRlbVsxXSkpKQogICAgcmV0dXJuICJweXRob24iICsgIiwiLmpvaW4oZiJ7b3B9e3ZlcnNpb259IiBmb3Igb3AsIHZlcnNpb24gaW4gb3JkZXJlZCkKCgpkZWYgcmVhZF9ydW50aW1lX3NwZWMoKSAtPiBzdHI6CiAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoUlVOVElNRV9QQVRIKToKICAgICAgICByZXR1cm4gIiIKICAgIHdpdGggb3BlbihSVU5USU1FX1BBVEgsICdyJywgZW5jb2Rpbmc9J3V0Zi04JywgZXJyb3JzPSdpZ25vcmUnKSBhcyBoYW5kbGU6CiAgICAgICAgdGV4dCA9IGhhbmRsZS5yZWFkKCkKICAgIG1hdGNoID0gcmUuc2VhcmNoKHInKD86cHl0aG9uWy09XSk/XHMqKFswLTldKyg/OlwuWzAtOV0rKXswLDJ9KScsIHRleHQpCiAgICBpZiBub3QgbWF0Y2g6CiAgICAgICAgcmV0dXJuICIiCiAgICBwYXJ0cyA9IG1hdGNoLmdyb3VwKDEpLnNwbGl0KCcuJykKICAgIG1ham9yX21pbm9yID0gJy4nLmpvaW4ocGFydHNbOjJdKQogICAgcmV0dXJuIGYncHl0aG9uPXttYWpvcl9taW5vcn0nCgoKZGVmIHJlYWRfcHlwcm9qZWN0X3NwZWMoKSAtPiBzdHI6CiAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoUFlQUk9KRUNUX1BBVEgpOgogICAgICAgIHJldHVybiAiIgogICAgd2l0aCBvcGVuKFBZUFJPSkVDVF9QQVRILCAncicsIGVuY29kaW5nPSd1dGYtOCcsIGVycm9ycz0naWdub3JlJykgYXMgaGFuZGxlOgogICAgICAgIHRleHQgPSBoYW5kbGUucmVhZCgpCiAgICBtYXRjaCA9IFBZUFJPSkVDVF9SRS5zZWFyY2godGV4dCkKICAgIGlmIG5vdCBtYXRjaDoKICAgICAgICByZXR1cm4gIiIKICAgIHJldHVybiBwZXA0NDBfdG9fY29uZGEobWF0Y2guZ3JvdXAoMSkpCgoKZGVmIGRldGVjdF9yZXF1aXJlc19weXRob24oKSAtPiBzdHI6CiAgICAiIiJSZXR1cm4gYmVzdC1lZmZvcnQgcmVxdWlyZXMtcHl0aG9uIGNvbnN0cmFpbnQgZm9yIHRoZSBjdXJyZW50IHByb2plY3QuIiIiCiAgICBydW50aW1lX3NwZWMgPSByZWFkX3J1bnRpbWVfc3BlYygpCiAgICBpZiBydW50aW1lX3NwZWM6CiAgICAgICAgcmV0dXJuIHJ1bnRpbWVfc3BlYwogICAgcmV0dXJuIHJlYWRfcHlwcm9qZWN0X3NwZWMoKQoKCmRlZiBtYWluKGFyZ3Y9Tm9uZSkgLT4gTm9uZToKICAgICIiIkNMSSBlbnRyeSBwb2ludCB0aGF0IHByaW50cyBub3JtYWxpemVkIHJlcXVpcmVzLXB5dGhvbiBjb25zdHJhaW50cy4iIiIKICAgIGFyZ3MgPSBsaXN0KHN5cy5hcmd2WzE6XSBpZiBhcmd2IGlzIE5vbmUgZWxzZSBhcmd2KQogICAgaWYgYXJncyBhbmQgYXJnc1swXSA9PSAiLS1zZWxmLXRlc3QiOgogICAgICAgIGZvciBzYW1wbGUgaW4gKCJ+PTMuMTAiLCAifj0zLjguMSIpOgogICAgICAgICAgICBzeXMuc3Rkb3V0LndyaXRlKHBlcDQ0MF90b19jb25kYShzYW1wbGUpICsgIlxuIikKCiAgICAgICAgcmV0dXJuCiAgICBpZiBhcmdzOgogICAgICAgIGZvciBpdGVtIGluIGFyZ3M6CiAgICAgICAgICAgIHN5cy5zdGRvdXQud3JpdGUocGVwNDQwX3RvX2NvbmRhKGl0ZW0pICsgIlxuIikKCiAgICAgICAgcmV0dXJuCiAgICBzeXMuc3Rkb3V0LndyaXRlKGRldGVjdF9yZXF1aXJlc19weXRob24oKSArICJcbiIpCgoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK" set "HP_PRINT_PYVER=aW1wb3J0IHN5cwoKcHJpbnQoZiJweXRob24te3N5cy52ZXJzaW9uX2luZm9bMF19LntzeXMudmVyc2lvbl9pbmZvWzFdfS57c3lzLnZlcnNpb25faW5mb1syXX0iKQo=" -rem HP_FAST_CHECK decoded content: -rem $exe = $args[0] -rem if (-not $exe) { $exe = $env:HP_FAST_EXE } -rem $infraPattern = '(?i)(^|[/\\])(\.git|\.github|dist|\.venv|\.uv_env|__pycache__|\.conda)([/\\]|$)' -rem $sources = Get-ChildItem -Recurse -File -Filter '*.py' | Where-Object { $_.FullName -notmatch $infraPattern -and $_.Name -notlike '~*.py' } -rem if (-not $sources) { exit 1 } -rem $latest = ($sources | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1).LastWriteTimeUtc -rem $exeTime = (Get-Item -LiteralPath $exe).LastWriteTimeUtc -rem if ($exeTime -ge $latest) { 'fresh' } +rem HP_FAST_CHECK decoded content (CLAUDE.md Active Backlog Item 39): content-hash freshness +rem check over the same *.py file set (unchanged filter), extended to requirements.txt/ +rem pyproject.toml/runtime.txt. Two modes via the second positional arg: 'check' (default, +rem called from :try_fast_exe) prints 'fresh' iff a stored ~fast_check.hash.txt exists and +rem matches the current source hash; 'write' (called from :success) unconditionally +rem (re)writes it. See tools/fast_check.ps1 (the canonical source) for the full script and +rem docs/agent-lessons-learned.md's "Prefer raw .NET types over Utility-module cmdlets" +rem entry for why this uses [System.Security.Cryptography.SHA256] directly, not Get-FileHash. rem If HP_FAST_CHECK changes, update this decoded comment block to match the base64 payload. -set "HP_FAST_CHECK=JGV4ZSA9ICRhcmdzWzBdCmlmICgtbm90ICRleGUpIHsgJGV4ZSA9ICRlbnY6SFBfRkFTVF9FWEUgfQokaW5mcmFQYXR0ZXJuID0gJyg/aSkoXnxbL1xcXSkoXC5naXR8XC5naXRodWJ8ZGlzdHxcLnZlbnZ8XC51dl9lbnZ8X19weWNhY2hlX198XC5jb25kYSkoWy9cXF18JCknCiRzb3VyY2VzID0gR2V0LUNoaWxkSXRlbSAtUmVjdXJzZSAtRmlsZSAtRmlsdGVyICcqLnB5JyB8IFdoZXJlLU9iamVjdCB7ICRfLkZ1bGxOYW1lIC1ub3RtYXRjaCAkaW5mcmFQYXR0ZXJuIC1hbmQgJF8uTmFtZSAtbm90bGlrZSAnfioucHknIH0KaWYgKC1ub3QgJHNvdXJjZXMpIHsgZXhpdCAxIH0KJGxhdGVzdCA9ICgkc291cmNlcyB8IFNvcnQtT2JqZWN0IC1Qcm9wZXJ0eSBMYXN0V3JpdGVUaW1lVXRjIC1EZXNjZW5kaW5nIHwgU2VsZWN0LU9iamVjdCAtRmlyc3QgMSkuTGFzdFdyaXRlVGltZVV0YwokZXhlVGltZSA9IChHZXQtSXRlbSAtTGl0ZXJhbFBhdGggJGV4ZSkuTGFzdFdyaXRlVGltZVV0YwppZiAoJGV4ZVRpbWUgLWdlICRsYXRlc3QpIHsgJ2ZyZXNoJyB9Cg==" +set "HP_FAST_CHECK=IyBBU0NJSSBvbmx5LiBDTEFVREUubWQgQWN0aXZlIEJhY2tsb2cgSXRlbSAzOTogdGhlIEVYRSBmYXN0IHBhdGgncyBmcmVzaG5lc3MgY2hlY2sgd2FzCiMgbXRpbWUtb25seSBvdmVyICoucHkgZmlsZXMsIHNvIChhKSBhIHRpbWVzdGFtcC1wcmVzZXJ2aW5nIGRlbGl2ZXJ5IG1ldGhvZCAoYSBaSVAsIHhjb3B5LAojIHJvYm9jb3B5KSBjb3VsZCBjYXJyeSBhIGdlbnVpbmVseSBjaGFuZ2VkIGZpbGUgd2hvc2UgbXRpbWUgc3RpbGwgcHJlZGF0ZXMgdGhlIGJ1aWx0IEVYRSwKIyBzaWxlbnRseSByZXVzaW5nIHN0YWxlIGxvZ2ljIHdpdGggbm8gc2lnbmFsIHRvIHRoZSB1c2VyLCBhbmQgKGIpIGEgcmVxdWlyZW1lbnRzLnR4dC8KIyBweXByb2plY3QudG9tbC9ydW50aW1lLnR4dCBjaGFuZ2Ugd2FzIGludmlzaWJsZSB0byB0aGUgc2NhbiBlbnRpcmVseSwgbXRpbWUgb3Igbm90LiBGaXhlcwojIGJvdGg6IGZyZXNobmVzcyBpcyBub3cgYSBjb250ZW50LWhhc2ggY29tcGFyaXNvbiAobmV2ZXIgZm9vbGVkIGJ5IGEgcHJlc2VydmVkL2JhY2tkYXRlZAojIG10aW1lKSBvdmVyIHRoZSBTQU1FICoucHkgZmlsZSBzZXQgYWxyZWFkeSBzY2FubmVkLCBleHRlbmRlZCB0byBpbmNsdWRlIHRob3NlIHRocmVlCiMgZGVwZW5kZW5jeSBmaWxlcy4KIwojIFR3byBtb2Rlcywgc2VsZWN0ZWQgYnkgdGhlIHNlY29uZCBwb3NpdGlvbmFsIGFyZ3VtZW50IChkZWZhdWx0ICdjaGVjaycpOgojICAgY2hlY2sgIChydW4gZnJvbSA6dHJ5X2Zhc3RfZXhlKTogcHJpbnQgJ2ZyZXNoJyBpZmYgYSBzdG9yZWQgaGFzaCBleGlzdHMgQU5EIG1hdGNoZXMgdGhlCiMgICAgICAgICAgaGFzaCBvZiB0aGUgQ1VSUkVOVCBzb3VyY2Ugc2V0LiBQcmludHMgbm90aGluZyAoZmFsbHMgdGhyb3VnaCB0byBhIHJlYnVpbGQpIG9uIGFueQojICAgICAgICAgIG1pc21hdGNoLCBvciB3aGVuIG5vIHN0b3JlZCBoYXNoIGV4aXN0cyB5ZXQgKGUuZy4gdGhlIGZpcnN0IHJ1biB1bmRlciB0aGlzIGZpeCwKIyAgICAgICAgICBvciBkaXN0XDxlbnY+LmV4ZSB3YXMgYnVpbHQgYnkgYW4gb2xkZXIgcnVuX3NldHVwLmJhdCB3aXRoIG5vIGhhc2ggZmlsZSBhdCBhbGwpIC0tCiMgICAgICAgICAgYSBzYWZlLCBvbmUtdGltZS1jb3N0IGRlZmF1bHQgdGhhdCBuZXZlciBtaXMtcmVwb3J0cyBzdGFsZW5lc3MgYXMgZnJlc2huZXNzLgojICAgd3JpdGUgIChydW4gZnJvbSA6c3VjY2Vzcywgb25seSB3aGVuIGEgZnJlc2ggYnVpbGQgYXR0ZW1wdCBqdXN0IGhhcHBlbmVkIC0tIHNlZSB0aGUKIyAgICAgICAgICBjYWxsIHNpdGUncyBvd24gSFBfRkFTVFBBVEhfVVNFRCBnYXRlIGZvciB3aHkgdGhlIGZhc3QtcmV1c2UgY2FzZSBza2lwcyB0aGlzIGFuZAojICAgICAgICAgIGF2b2lkcyBhIHJlZHVuZGFudCBzZWNvbmQgaGFzaCBwYXNzKTogdW5jb25kaXRpb25hbGx5IChyZSl3cml0ZSB0aGUgaGFzaCBvZiB0aGUKIyAgICAgICAgICBDVVJSRU5UIHNvdXJjZSBzZXQuIEFsd2F5cyBydW4gZnJvbSB0aGUgYXBwIHJvb3QgYXMgQ1dEIChib3RoIGNhbGwgc2l0ZXMgYWxyZWFkeQojICAgICAgICAgIGd1YXJhbnRlZSB0aGlzKSwgc28gcmVsYXRpdmUgcGF0aHMgY2FwdHVyZWQgdmlhIFJlc29sdmUtUGF0aCAtUmVsYXRpdmUgbWF0Y2gKIyAgICAgICAgICBiZXR3ZWVuIHRoZSB3cml0ZSBhbmQgYSBsYXRlciBjaGVjay4KIwojIFVzZXMgW1N5c3RlbS5TZWN1cml0eS5DcnlwdG9ncmFwaHkuU0hBMjU2XSBkaXJlY3RseSwgbm90IEdldC1GaWxlSGFzaCAtLSBzZWUKIyBkb2NzL2FnZW50LWxlc3NvbnMtbGVhcm5lZC5tZCdzICJQcmVmZXIgcmF3IC5ORVQgdHlwZXMgb3ZlciBVdGlsaXR5LW1vZHVsZSBjbWRsZXRzIiBlbnRyeToKIyBNaWNyb3NvZnQuUG93ZXJTaGVsbC5VdGlsaXR5IGlzIG5vdCBndWFyYW50ZWVkIHRvIGF1dG8tbG9hZCBpbiB0aGUgZm9yL2YtYmFja3RpY2sgYW5kCiMgLUZpbGUgaW52b2NhdGlvbiBzaGFwZXMgdGhpcyByZXBvJ3MgZW1iZWRkZWQgaGVscGVycyBydW4gdW5kZXIgb24gcmVhbCBXaW5kb3dzIFBvd2VyU2hlbGwKIyA1LjEsIGFuZCB0aGlzIGNsYXNzIG9mIGdhcCBpcyBpbnZpc2libGUgdG8gbG9jYWwgcHdzaCAoTGludXgsIFBvd2VyU2hlbGwgNykgdGVzdGluZy4KIwojIFRoaXMgaXMgdGhlIGNhbm9uaWNhbCBzb3VyY2UgZm9yIHRoZSBIUF9GQVNUX0NIRUNLIGJhc2U2NCBwYXlsb2FkIGVtYmVkZGVkIGluCiMgcnVuX3NldHVwLmJhdC4gQWZ0ZXIgZWRpdGluZywgcmUtc3luYyB2aWE6CiMgICBweXRob24gdG9vbHMvc3luY19wYXlsb2FkLnB5IEhQX0ZBU1RfQ0hFQ0sgdG9vbHMvZmFzdF9jaGVjay5wczEKIyB0ZXN0cy90ZXN0X2Zhc3RfY2hlY2sucHkgYXNzZXJ0cyB0aGUgZW1iZWRkZWQgcGF5bG9hZCBtYXRjaGVzIHRoaXMgZmlsZSBieXRlLWZvci1ieXRlCiMgKGFmdGVyIENSTEYvTEYgbm9ybWFsaXphdGlvbiwgc2luY2UgdGhpcyBmaWxlIGlzIGVvbD1jcmxmIGJ1dCB3YXMgYXV0aG9yZWQgb24gTEYpLgoKJGV4ZSA9ICRhcmdzWzBdCmlmICgtbm90ICRleGUpIHsgJGV4ZSA9ICRlbnY6SFBfRkFTVF9FWEUgfQokbW9kZSA9ICRhcmdzWzFdCmlmICgtbm90ICRtb2RlKSB7ICRtb2RlID0gJ2NoZWNrJyB9CgokaGFzaEZpbGUgPSAnfmZhc3RfY2hlY2suaGFzaC50eHQnCiRpbmZyYVBhdHRlcm4gPSAnKD9pKShefFsvXFxdKShcLmdpdHxcLmdpdGh1YnxkaXN0fFwudmVudnxcLnV2X2VudnxfX3B5Y2FjaGVfX3xcLmNvbmRhKShbL1xcXXwkKScKJHB5ID0gQChHZXQtQ2hpbGRJdGVtIC1SZWN1cnNlIC1GaWxlIC1GaWx0ZXIgJyoucHknIHwgV2hlcmUtT2JqZWN0IHsgJF8uRnVsbE5hbWUgLW5vdG1hdGNoICRpbmZyYVBhdHRlcm4gLWFuZCAkXy5OYW1lIC1ub3RsaWtlICd+Ki5weScgfSkKJGRlcE5hbWVzID0gQCgncmVxdWlyZW1lbnRzLnR4dCcsICdweXByb2plY3QudG9tbCcsICdydW50aW1lLnR4dCcpCiRkZXBzID0gQCgkZGVwTmFtZXMgfCBXaGVyZS1PYmplY3QgeyBUZXN0LVBhdGggLUxpdGVyYWxQYXRoICRfIC1QYXRoVHlwZSBMZWFmIH0gfCBGb3JFYWNoLU9iamVjdCB7IEdldC1JdGVtIC1MaXRlcmFsUGF0aCAkXyB9KQokc291cmNlcyA9IEAoJHB5KSArIEAoJGRlcHMpCgppZiAoLW5vdCAkc291cmNlcykgewogIGlmICgkbW9kZSAtZXEgJ2NoZWNrJykgeyBleGl0IDEgfSBlbHNlIHsgZXhpdCAwIH0KfQoKJHNoYSA9IFtTeXN0ZW0uU2VjdXJpdHkuQ3J5cHRvZ3JhcGh5LlNIQTI1Nl06OkNyZWF0ZSgpCiRsaW5lcyA9IGZvcmVhY2ggKCRmIGluICgkc291cmNlcyB8IFNvcnQtT2JqZWN0IC1Qcm9wZXJ0eSBGdWxsTmFtZSkpIHsKICAkcmVsUGF0aCA9IFJlc29sdmUtUGF0aCAtTGl0ZXJhbFBhdGggJGYuRnVsbE5hbWUgLVJlbGF0aXZlCiAgJGJ5dGVzID0gW1N5c3RlbS5JTy5GaWxlXTo6UmVhZEFsbEJ5dGVzKCRmLkZ1bGxOYW1lKQogICRoYXNoQnl0ZXMgPSAkc2hhLkNvbXB1dGVIYXNoKCRieXRlcykKICAkaGV4ID0gLWpvaW4gKCRoYXNoQnl0ZXMgfCBGb3JFYWNoLU9iamVjdCB7ICRfLlRvU3RyaW5nKCd4MicpIH0pCiAgIiRyZWxQYXRofCRoZXgiCn0KJGNvbWJpbmVkID0gW3N0cmluZ106OkpvaW4oImBuIiwgJGxpbmVzKQokY29tYmluZWRCeXRlcyA9IFtTeXN0ZW0uVGV4dC5FbmNvZGluZ106OlVURjguR2V0Qnl0ZXMoJGNvbWJpbmVkKQokZGlnZXN0Qnl0ZXMgPSAkc2hhLkNvbXB1dGVIYXNoKCRjb21iaW5lZEJ5dGVzKQokZGlnZXN0ID0gLWpvaW4gKCRkaWdlc3RCeXRlcyB8IEZvckVhY2gtT2JqZWN0IHsgJF8uVG9TdHJpbmcoJ3gyJykgfSkKCmlmICgkbW9kZSAtZXEgJ3dyaXRlJykgewogIFtTeXN0ZW0uSU8uRmlsZV06OldyaXRlQWxsVGV4dCgkaGFzaEZpbGUsICRkaWdlc3QpCiAgZXhpdCAwCn0KCmlmICgtbm90IChUZXN0LVBhdGggLUxpdGVyYWxQYXRoICRoYXNoRmlsZSkpIHsgZXhpdCAxIH0KaWYgKC1ub3QgJGV4ZSkgeyBleGl0IDEgfQppZiAoLW5vdCAoVGVzdC1QYXRoIC1MaXRlcmFsUGF0aCAkZXhlKSkgeyBleGl0IDEgfQokc3RvcmVkID0gW1N5c3RlbS5JTy5GaWxlXTo6UmVhZEFsbFRleHQoJGhhc2hGaWxlKS5UcmltKCkKaWYgKCRzdG9yZWQgLWVxICRkaWdlc3QpIHsgJ2ZyZXNoJyB9Cg==" set "HP_EMBED_EXTRACT=IyBSRVEtMDA5IFRpZXIgNTogdmVyaWZpZXMgY2hlY2tzdW0sIGV4dHJhY3RzLCBhbmQgcGF0Y2hlcyB0aGUgZGlzYWJsZWQtc2l0ZS1pbXBvcnRzIC5fcHRoIGZpbGUKIyBmb3IgYW4gYWxyZWFkeS1kb3dubG9hZGVkIGVtYmVkZGFibGUgUHl0aG9uIHppcC4gQmF0Y2ggaGFzIGFscmVhZHkgZG93bmxvYWRlZCB0aGUgemlwICh2aWEgdGhlCiMgc2FtZSBjdXJsLXRoZW4tSW52b2tlLVdlYlJlcXVlc3QgcGF0dGVybiBhcyA6ZG93bmxvYWRfbWluaWNvbmRhX2V4ZS86ZG93bmxvYWRfZ2V0X3BpcCkgYW5kCiMgcGFzc2VzIGl0cyBwYXRoIHBsdXMgdGhlIGV4cGVjdGVkIFNIQTI1NiBhbmQgZGVzdGluYXRpb24gZGlyZWN0b3J5IGFzIGFyZ3MuIFRoaXMgc2NyaXB0IGRvZXMKIyBOT1QgZG93bmxvYWQgYW55dGhpbmcgaXRzZWxmIGFuZCBkb2VzIE5PVCBicmFuY2ggb24gcmVxdWVzdGVkIHZlcnNpb24gLS0gc2VlIHRoZSBQeXRob24tc2lkZQojIHN0YWdlIGZvciBwZXItcmVxdWVzdCB2ZXJzaW9uIHNlbGVjdGlvbi4KIyBBcmdzOiAkMSA9IHppcCBwYXRoLCAkMiA9IGV4cGVjdGVkIHNoYTI1NiAobG93ZXJjYXNlIGhleCksICQzID0gZGVzdGluYXRpb24gZGlyZWN0b3J5LgojIE91dHB1dDogb24gc3VjY2VzcywgcHJpbnRzIHRoZSBleHRyYWN0ZWQgcHl0aG9uLmV4ZSBwYXRoIG9uIHN0ZG91dCwgZXhpdCAwLiBPbiBmYWlsdXJlCiMgKGNoZWNrc3VtIG1pc21hdGNoLCBleHRyYWN0aW9uIGZhaWx1cmUsIG1pc3NpbmcgLl9wdGgsIG1pc3NpbmcgcHl0aG9uLmV4ZSksIHByaW50cyBub3RoaW5nIGFuZAojIGV4aXRzIDEgLS0gY2FsbGVyIGNoZWNrcyBib3RoIHN0ZG91dCBhbmQgZXhpdCBjb2RlLgojCiMgZGVyaXZlZCByZXF1aXJlbWVudDogaGFzaGluZy9leHRyYWN0aW9uL3RleHQtSU8gZGVsaWJlcmF0ZWx5IHVzZSByYXcgLk5FVCBBUElzCiMgKFtTeXN0ZW0uU2VjdXJpdHkuQ3J5cHRvZ3JhcGh5LlNIQTI1Nl0sIFtTeXN0ZW0uSU8uQ29tcHJlc3Npb24uWmlwRmlsZV0sIFtTeXN0ZW0uSU8uRmlsZV0pCiMgaW5zdGVhZCBvZiB0aGUgR2V0LUZpbGVIYXNoIC8gRXhwYW5kLUFyY2hpdmUgLyBHZXQtQ29udGVudCAvIFNldC1Db250ZW50IGNtZGxldHMuIENvbmZpcm1lZCB2aWEKIyByZWFsIENJIGZhaWx1cmUgKFdpbmRvd3MgUG93ZXJTaGVsbCA1LjEsIGludm9rZWQgYXMgYSBmb3IgL2YgYmFja3RpY2sgc3Vic2hlbGwgZnJvbSBydW5fc2V0dXAuYmF0KQojIHRoYXQgR2V0LUZpbGVIYXNoIHRocm93cyAibm90IHJlY29nbml6ZWQgYXMgdGhlIG5hbWUgb2YgYSBjbWRsZXQiIGluIHRoYXQgZXhhY3QgaW52b2NhdGlvbgojIGNvbnRleHQgLS0gaXRzIG1vZHVsZSAoTWljcm9zb2Z0LlBvd2VyU2hlbGwuVXRpbGl0eSkgd2FzIG5vdCBhdXRvLWxvYWRpbmcsIGV2ZW4gdGhvdWdoIHRoaXMKIyBzY3JpcHQgdGVzdGVkIGZpbmUgbG9jYWxseSB1bmRlciBwd3NoIChQb3dlclNoZWxsIDcgb24gTGludXgpIGJlZm9yZWhhbmQsIHdoaWNoIGRvZXMgbm90IHNoYXJlCiMgdGhlIHNhbWUgbW9kdWxlLWxvYWRpbmcgYmVoYXZpb3IuIFRlc3QtUGF0aC9HZXQtSXRlbSAoTWljcm9zb2Z0LlBvd2VyU2hlbGwuTWFuYWdlbWVudCkgd29ya2VkCiMgZmluZSBpbiB0aGUgc2FtZSBydW4sIHNvIHRoaXMgaXMgc2NvcGVkIHRvIFV0aWxpdHktbW9kdWxlIGNtZGxldHMgc3BlY2lmaWNhbGx5OyAuTkVUIHR5cGVzIGhhdmUKIyBubyBtb2R1bGUtbG9hZGluZyBkZXBlbmRlbmN5IGF0IGFsbCBhbmQgc2lkZXN0ZXAgdGhlIHdob2xlIGNsYXNzIG9mIGZhaWx1cmUuIFNlZQojIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kLgokWmlwUGF0aCA9ICRhcmdzWzBdCiRFeHBlY3RlZFNoYTI1NiA9ICRhcmdzWzFdCiREZXN0RGlyID0gJGFyZ3NbMl0KCnRyeSB7CiAgICBpZiAoLW5vdCAoVGVzdC1QYXRoIC1MaXRlcmFsUGF0aCAkWmlwUGF0aCkpIHsKICAgICAgICBbQ29uc29sZV06OkVycm9yLldyaXRlTGluZSgiW2VtYmVkX2V4dHJhY3RdIHppcCBub3QgZm91bmQ6ICRaaXBQYXRoIikKICAgICAgICBleGl0IDEKICAgIH0KICAgICRaaXBTaXplID0gKEdldC1JdGVtIC1MaXRlcmFsUGF0aCAkWmlwUGF0aCkuTGVuZ3RoCiAgICAkU2hhMjU2UHJvdmlkZXIgPSBbU3lzdGVtLlNlY3VyaXR5LkNyeXB0b2dyYXBoeS5TSEEyNTZdOjpDcmVhdGUoKQogICAgJEZpbGVTdHJlYW0gPSBbU3lzdGVtLklPLkZpbGVdOjpPcGVuUmVhZCgkWmlwUGF0aCkKICAgIHRyeSB7CiAgICAgICAgJEhhc2hCeXRlcyA9ICRTaGEyNTZQcm92aWRlci5Db21wdXRlSGFzaCgkRmlsZVN0cmVhbSkKICAgIH0gZmluYWxseSB7CiAgICAgICAgJEZpbGVTdHJlYW0uRGlzcG9zZSgpCiAgICAgICAgJFNoYTI1NlByb3ZpZGVyLkRpc3Bvc2UoKQogICAgfQogICAgJEFjdHVhbEhhc2ggPSAoW0JpdENvbnZlcnRlcl06OlRvU3RyaW5nKCRIYXNoQnl0ZXMpIC1yZXBsYWNlICctJywgJycpLlRvTG93ZXIoKQogICAgaWYgKCRBY3R1YWxIYXNoIC1uZSAkRXhwZWN0ZWRTaGEyNTYuVG9Mb3dlcigpKSB7CiAgICAgICAgW0NvbnNvbGVdOjpFcnJvci5Xcml0ZUxpbmUoIltlbWJlZF9leHRyYWN0XSBjaGVja3N1bSBtaXNtYXRjaDogc2l6ZT0kWmlwU2l6ZSBleHBlY3RlZD0kKCRFeHBlY3RlZFNoYTI1Ni5Ub0xvd2VyKCkpIGFjdHVhbD0kQWN0dWFsSGFzaCIpCiAgICAgICAgZXhpdCAxCiAgICB9CgogICAgaWYgKFRlc3QtUGF0aCAtTGl0ZXJhbFBhdGggJERlc3REaXIpIHsgUmVtb3ZlLUl0ZW0gLVJlY3Vyc2UgLUZvcmNlIC1MaXRlcmFsUGF0aCAkRGVzdERpciB9CiAgICBBZGQtVHlwZSAtQXNzZW1ibHlOYW1lIFN5c3RlbS5JTy5Db21wcmVzc2lvbi5GaWxlU3lzdGVtCiAgICBbU3lzdGVtLklPLkNvbXByZXNzaW9uLlppcEZpbGVdOjpFeHRyYWN0VG9EaXJlY3RvcnkoJFppcFBhdGgsICREZXN0RGlyKQoKICAgICRQdGhGaWxlID0gR2V0LUNoaWxkSXRlbSAtTGl0ZXJhbFBhdGggJERlc3REaXIgLUZpbHRlciAicHl0aG9uKi5fcHRoIiAtRmlsZSB8IFNlbGVjdC1PYmplY3QgLUZpcnN0IDEKICAgIGlmICgtbm90ICRQdGhGaWxlKSB7CiAgICAgICAgW0NvbnNvbGVdOjpFcnJvci5Xcml0ZUxpbmUoIltlbWJlZF9leHRyYWN0XSBubyBweXRob24qLl9wdGggZmlsZSBmb3VuZCB1bmRlciAkRGVzdERpciIpCiAgICAgICAgZXhpdCAxCiAgICB9CiAgICAjIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IHRoZSBlbWJlZGRhYmxlIHppcCdzIC5fcHRoIGZpbGUgc2hpcHMgd2l0aCBDUkxGIGxpbmUgZW5kaW5ncywgYW5kCiAgICAjIC5ORVQgcmVnZXggJCBpbiBtdWx0aWxpbmUgbW9kZSBtYXRjaGVzIGltbWVkaWF0ZWx5IGJlZm9yZSBcbiAtLSBpdCBkb2VzIG5vdCBza2lwIGEKICAgICMgcHJlY2VkaW5nIFxyLCBzbyBhbiBhbmNob3Igb2YgIl4jaW1wb3J0IHNpdGUkIiBhZ2FpbnN0IGEgQ1JMRiBsaW5lIHNpbGVudGx5IG5ldmVyIG1hdGNoZXMKICAgICMgKHRoZSBcciBzaXRzIGJldHdlZW4gInNpdGUiIGFuZCB0aGUgbWF0Y2ggcG9zaXRpb24pLiBccj8gaGFuZGxlcyBib3RoIGxpbmUtZW5kaW5nIHN0eWxlcy4KICAgICRQdGhDb250ZW50ID0gW1N5c3RlbS5JTy5GaWxlXTo6UmVhZEFsbFRleHQoJFB0aEZpbGUuRnVsbE5hbWUpCiAgICAkUHRoQ29udGVudCA9ICRQdGhDb250ZW50IC1yZXBsYWNlICcoP20pXiNpbXBvcnQgc2l0ZVxyPyQnLCAnaW1wb3J0IHNpdGUnCiAgICBbU3lzdGVtLklPLkZpbGVdOjpXcml0ZUFsbFRleHQoJFB0aEZpbGUuRnVsbE5hbWUsICRQdGhDb250ZW50LCBbU3lzdGVtLlRleHQuRW5jb2RpbmddOjpBU0NJSSkKCiAgICAkUHlFeGUgPSBKb2luLVBhdGggJERlc3REaXIgInB5dGhvbi5leGUiCiAgICBpZiAoLW5vdCAoVGVzdC1QYXRoIC1MaXRlcmFsUGF0aCAkUHlFeGUpKSB7CiAgICAgICAgW0NvbnNvbGVdOjpFcnJvci5Xcml0ZUxpbmUoIltlbWJlZF9leHRyYWN0XSBweXRob24uZXhlIG1pc3NpbmcgYWZ0ZXIgZXh0cmFjdGlvbjogJFB5RXhlIikKICAgICAgICBleGl0IDEKICAgIH0KICAgIFtDb25zb2xlXTo6V3JpdGUoJFB5RXhlKQp9IGNhdGNoIHsKICAgIFtDb25zb2xlXTo6RXJyb3IuV3JpdGVMaW5lKCJbZW1iZWRfZXh0cmFjdF0gZXhjZXB0aW9uOiAkKCRfLkV4Y2VwdGlvbi5NZXNzYWdlKSIpCiAgICBleGl0IDEKfQo=" set "HP_EMBED_PYVER_CHECK=IyBSRVEtMDA5IFRpZXIgNSwgUHl0aG9uIHN0YWdlOiBydW5zIHVuZGVyIHRoZSAiYWx3YXlzIGxhdGVzdCIgaW50ZXJwcmV0ZXIgfmVtYmVkX2V4dHJhY3QucHMxCiMgKFBvd2VyU2hlbGwgc3RhZ2UpIGFscmVhZHkgZG93bmxvYWRlZC92ZXJpZmllZC9leHRyYWN0ZWQuIFRoaXMgaXMgdGhlIE9OTFkgcGxhY2UgcGVyLXJlcXVlc3QKIyB2ZXJzaW9uIGxvZ2ljIGxpdmVzIC0tIGRlbGliZXJhdGVseSBQeXRob24sIG5vdCBQb3dlclNoZWxsLCByZXVzaW5nIHRoaXMgY29kZWJhc2UncyBwcm92ZW4KIyB2ZXJzaW9uLWRldGVjdGlvbiBwYXR0ZXJuIGluc3RlYWQgb2YgcmUtZGVyaXZpbmcgaXQgaW4gUG93ZXJTaGVsbC4gRnVsbCByYXRpb25hbGU6CiMgZG9jcy9hZ2VudC1pbnRlcmNvbm5lY3QubWQgIlN0YW5kYWxvbmUgUHl0aG9uLWRvd25sb2FkIHRpZXIiLiAiMy4xNCIgZW50cnkgYmVsb3cgTVVTVCBtYXRjaAojIEhQX0VNQkVEX0xBVEVTVF9QQVRDSC9IUF9FTUJFRF9MQVRFU1RfU0hBMjU2IGluIHJ1bl9zZXR1cC5iYXQgLS0gYSBQYXlsb2FkU3luYy1zdHlsZSB1bml0IHRlc3QKIyBhc3NlcnRzIHRoaXMuIExhc3QgcmVmcmVzaGVkOiAyMDI2LTA3LTA5LgppbXBvcnQgaGFzaGxpYgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzaHV0aWwKaW1wb3J0IHNvY2tldAppbXBvcnQgc3lzCmltcG9ydCB1cmxsaWIucmVxdWVzdAppbXBvcnQgemlwZmlsZQoKIyBkZXJpdmVkIHJlcXVpcmVtZW50OiB1cmxsaWIucmVxdWVzdC51cmxyZXRyaWV2ZSBoYXMgbm8gdGltZW91dD0gcGFyYW1ldGVyICh2ZXJpZmllZCB2aWEKIyBpbnNwZWN0LnNpZ25hdHVyZSAtLSBwYXNzaW5nIG9uZSByYWlzZXMgVHlwZUVycm9yKSwgc28gYSBzdGFsbGVkIChub3QgcmVmdXNlZCkgY29ubmVjdGlvbgojIGR1cmluZyBkb3dubG9hZF9hbmRfdmVyaWZ5KCkgd291bGQgb3RoZXJ3aXNlIGhhbmcgdGhpcyBvbmUtc2hvdCBzY3JpcHQgZm9yZXZlci4gQSBnbG9iYWwKIyBkZWZhdWx0IHRpbWVvdXQgaXMgc2FmZSBoZXJlIHNpbmNlIHRoZSB3aG9sZSBzY3JpcHQgZXhpdHMgaW1tZWRpYXRlbHkgYWZ0ZXIgdXNlIC0tIG5vdGhpbmcKIyBlbHNlIGluIHRoaXMgc2hvcnQtbGl2ZWQgcHJvY2VzcyBpcyBhZmZlY3RlZC4gTWlycm9ycyB0aGUgY3VybCAtLW1heC10aW1lIDEyMCBhbHJlYWR5IHVzZWQKIyBmb3IgdGhlIFBvd2VyU2hlbGwtc3RhZ2UgZG93bmxvYWQgb2YgdGhlIHNhbWUgemlwIGZhbWlseS4Kc29ja2V0LnNldGRlZmF1bHR0aW1lb3V0KDEyMCkKCiMgbWlub3IgLT4gKHBhdGNoLCBzaGEyNTYpCkVNQkVEX1BZVEhPTl9UQUJMRSA9IHsKICAgICIzLjEwIjogKCIzLjEwLjExIiwgIjYwODYxOWY4NjE5MDc1NjI5YzljNjlmMzYxMzUyYTBkYTZlZDdlNjJmODNhMGUxOWM2M2UwZWEzMmViNzYyOWQiKSwKICAgICIzLjExIjogKCIzLjExLjkiLCAiMDA5ZDZiZjdlM2IyZGRjYTNkNzg0ZmEwOWY5MGZlNTQzMzZkNWI2MGYwZTBmMzA1YzM3ZjQwMGJmODNjZmQzYiIpLAogICAgIjMuMTIiOiAoIjMuMTIuMTAiLCAiNGFjYmVkNmRkMWM3NDRiMDM3NmUzYjFjZjU3Y2U5MDZmOWRjOWU5NWU2ODgyNDU4NGM4MDk5YTYzMDI1YTNjMyIpLAogICAgIjMuMTMiOiAoIjMuMTMuMTQiLCAiOTBiNGU1Yjk4OThiNzJkNzQ0NjUwNTI0YmZmOTIzNzdjMzY3ZjQ0YmQ1ZmJkMDllMzE0ODY1NmMwODBhZDkwNyIpLAogICAgIjMuMTQiOiAoIjMuMTQuNiIsICJkZjkwMWU4NGE4OTZmZjFlZTcyMGFkMDMzNzdlMGM4ZDhjMjI0NGZkYTc5ODA4YWVlYWZmNjMxNmRmMWNiNzVjIiksCn0KTEFURVNUX01JTk9SID0gIjMuMTQiCkZMT09SX01JTk9SID0gIjMuMTAiCgpTUEVDX01JTk9SX1JFID0gcmUuY29tcGlsZShyIihbMC05XStcLlswLTldKykiKQoKCmRlZiBfbWlub3Jfa2V5KG1pbm9yKToKICAgIHRyeToKICAgICAgICBtYWpvciwgc3ViID0gbWlub3Iuc3BsaXQoIi4iKQogICAgICAgIHJldHVybiAoaW50KG1ham9yKSwgaW50KHN1YikpCiAgICBleGNlcHQgKFZhbHVlRXJyb3IsIEF0dHJpYnV0ZUVycm9yKToKICAgICAgICByZXR1cm4gKDAsIDApCgoKZGVmIHJlc29sdmVfcmVxdWVzdGVkX21pbm9yKHB5c3BlYyk6CiAgICAjIEV4dHJhY3RzICJYLlkiIGZyb20gYSBQWVNQRUMgc3RyaW5nIChlLmcuICJweXRob24+PTMuMTAsPDQuMCIpOyBOb25lIGlmIGVtcHR5L3VucGFyc2VhYmxlLgogICAgaWYgbm90IHB5c3BlYzoKICAgICAgICByZXR1cm4gTm9uZQogICAgbWF0Y2ggPSBTUEVDX01JTk9SX1JFLnNlYXJjaChweXNwZWMpCiAgICByZXR1cm4gbWF0Y2guZ3JvdXAoMSkgaWYgbWF0Y2ggZWxzZSBOb25lCgoKZGVmIHJlc29sdmVfdGFibGVfZW50cnkocmVxdWVzdGVkX21pbm9yKToKICAgICMgUmV0dXJucyAobWlub3IsIHBhdGNoLCBzaGEyNTYsIGZlbGxfYmFjayk7IG1pcnJvcnMgdGhlIFBvd2VyU2hlbGwgc3RhZ2UncyBvd24gcnVsZXMuCiAgICBpZiByZXF1ZXN0ZWRfbWlub3IgaW4gRU1CRURfUFlUSE9OX1RBQkxFOgogICAgICAgIHBhdGNoLCBzaGEyNTYgPSBFTUJFRF9QWVRIT05fVEFCTEVbcmVxdWVzdGVkX21pbm9yXQogICAgICAgIHJldHVybiByZXF1ZXN0ZWRfbWlub3IsIHBhdGNoLCBzaGEyNTYsIEZhbHNlCiAgICBtaW5vciA9IEZMT09SX01JTk9SIGlmIF9taW5vcl9rZXkocmVxdWVzdGVkX21pbm9yKSA8IF9taW5vcl9rZXkoRkxPT1JfTUlOT1IpIGVsc2UgTEFURVNUX01JTk9SCiAgICBwYXRjaCwgc2hhMjU2ID0gRU1CRURfUFlUSE9OX1RBQkxFW21pbm9yXQogICAgcmV0dXJuIG1pbm9yLCBwYXRjaCwgc2hhMjU2LCBUcnVlCgoKZGVmIGRvd25sb2FkX2FuZF92ZXJpZnkodXJsLCBleHBlY3RlZF9zaGEyNTYsIGRlc3RfemlwKToKICAgIHVybGxpYi5yZXF1ZXN0LnVybHJldHJpZXZlKHVybCwgZGVzdF96aXApCiAgICBkaWdlc3QgPSBoYXNobGliLnNoYTI1NigpCiAgICB3aXRoIG9wZW4oZGVzdF96aXAsICJyYiIpIGFzIGZoOgogICAgICAgIGZvciBjaHVuayBpbiBpdGVyKGxhbWJkYTogZmgucmVhZCgxIDw8IDIwKSwgYiIiKToKICAgICAgICAgICAgZGlnZXN0LnVwZGF0ZShjaHVuaykKICAgIGFjdHVhbCA9IGRpZ2VzdC5oZXhkaWdlc3QoKS5sb3dlcigpCiAgICBpZiBhY3R1YWwgIT0gZXhwZWN0ZWRfc2hhMjU2Lmxvd2VyKCk6CiAgICAgICAgb3MucmVtb3ZlKGRlc3RfemlwKQogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImNoZWNrc3VtIG1pc21hdGNoOiBleHBlY3RlZCB7fSwgZ290IHt9Ii5mb3JtYXQoZXhwZWN0ZWRfc2hhMjU2LCBhY3R1YWwpKQoKCmRlZiBleHRyYWN0X2FuZF9wYXRjaCh6aXBfcGF0aCwgZGVzdF9kaXIpOgogICAgaWYgb3MucGF0aC5pc2RpcihkZXN0X2Rpcik6CiAgICAgICAgc2h1dGlsLnJtdHJlZShkZXN0X2RpcikKICAgIHdpdGggemlwZmlsZS5aaXBGaWxlKHppcF9wYXRoKSBhcyB6ZjoKICAgICAgICB6Zi5leHRyYWN0YWxsKGRlc3RfZGlyKQogICAgcHRoX2ZpbGVzID0gW2YgZm9yIGYgaW4gb3MubGlzdGRpcihkZXN0X2RpcikgaWYgcmUubWF0Y2gociJecHl0aG9uXGQrXC5fcHRoJCIsIGYpXQogICAgaWYgbm90IHB0aF9maWxlczoKICAgICAgICByYWlzZSBGaWxlTm90Rm91bmRFcnJvcigibm8gcHl0aG9uKi5fcHRoIGZpbGUgZm91bmQgYWZ0ZXIgZXh0cmFjdGlvbiIpCiAgICBwdGhfcGF0aCA9IG9zLnBhdGguam9pbihkZXN0X2RpciwgcHRoX2ZpbGVzWzBdKQogICAgd2l0aCBvcGVuKHB0aF9wYXRoLCAiciIsIGVuY29kaW5nPSJhc2NpaSIpIGFzIGZoOgogICAgICAgIGNvbnRlbnQgPSBmaC5yZWFkKCkKICAgIGNvbnRlbnQgPSByZS5zdWIociIoP20pXiNpbXBvcnQgc2l0ZSQiLCAiaW1wb3J0IHNpdGUiLCBjb250ZW50KQogICAgd2l0aCBvcGVuKHB0aF9wYXRoLCAidyIsIGVuY29kaW5nPSJhc2NpaSIsIG5ld2xpbmU9IiIpIGFzIGZoOgogICAgICAgIGZoLndyaXRlKGNvbnRlbnQpCiAgICBweV9leGUgPSBvcy5wYXRoLmpvaW4oZGVzdF9kaXIsICJweXRob24uZXhlIikKICAgIGlmIG5vdCBvcy5wYXRoLmlzZmlsZShweV9leGUpOgogICAgICAgIHJhaXNlIEZpbGVOb3RGb3VuZEVycm9yKCJweXRob24uZXhlIG1pc3NpbmcgYWZ0ZXIgZXh0cmFjdGlvbiIpCiAgICByZXR1cm4gcHlfZXhlCgoKZGVmIG1haW4oKToKICAgICMgZGVzdF9kaXIgaXMgd2hlcmUgVEhJUyBydW5uaW5nIGludGVycHJldGVyIGxpdmVzOyBXaW5kb3dzIHdvbid0IGxldCBhIHByb2Nlc3MgcmVwbGFjZSBpdHMKICAgICMgb3duIGZpbGVzLCBzbyBhIHN3YXAgZXh0cmFjdHMgaW50byBhIHNpYmxpbmcgX3N3YXAgZGlyIGFuZCBiYXRjaCBtb3ZlcyBpdCBpbnRvIHBsYWNlIG9ubHkKICAgICMgYWZ0ZXIgdGhpcyBwcm9jZXNzIGV4aXRzIChsb2NrcyByZWxlYXNlZCkuIFNlZSBkb2NzL2FnZW50LWludGVyY29ubmVjdC5tZC4KICAgIGRlc3RfZGlyID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiIgogICAgc3dhcF9kaXIgPSBkZXN0X2Rpci5yc3RyaXAoIlxcLyIpICsgIl9zd2FwIgogICAgcHlzcGVjID0gb3MuZW52aXJvbi5nZXQoIlBZU1BFQyIsICIiKQogICAgcmVxdWVzdGVkX21pbm9yID0gcmVzb2x2ZV9yZXF1ZXN0ZWRfbWlub3IocHlzcGVjKQoKICAgIGlmIHJlcXVlc3RlZF9taW5vciBpcyBOb25lIG9yIHJlcXVlc3RlZF9taW5vciA9PSBMQVRFU1RfTUlOT1I6CiAgICAgICAgc3lzLnN0ZG91dC53cml0ZSgidW5jaGFuZ2VkfHt9XG4iLmZvcm1hdChMQVRFU1RfTUlOT1IpKQogICAgICAgIHJldHVybiAwCgogICAgbWlub3IsIHBhdGNoLCBzaGEyNTYsIGZlbGxfYmFjayA9IHJlc29sdmVfdGFibGVfZW50cnkocmVxdWVzdGVkX21pbm9yKQogICAgaWYgbWlub3IgPT0gTEFURVNUX01JTk9SOgogICAgICAgICMgQWJvdmUtY2VpbGluZyByZXF1ZXN0IChvbmx5IHBhdGggaGVyZSwgc2luY2UgZXhhY3QtbWF0Y2gtbGF0ZXN0IGlzIGhhbmRsZWQgYWJvdmUpOgogICAgICAgICMgbm8gc3dhcCBuZWVkZWQsIGJ1dCB0YWcgImZlbGxiYWNrIiBub3QgInVuY2hhbmdlZCIgc28gdGhlIGNhbGxlcidzIFdBUk4gc3RpbGwgZmlyZXMuCiAgICAgICAgc3lzLnN0ZG91dC53cml0ZSgiZmVsbGJhY2t8e31cbiIuZm9ybWF0KG1pbm9yKSkKICAgICAgICByZXR1cm4gMAoKICAgIHVybCA9ICJodHRwczovL3d3dy5weXRob24ub3JnL2Z0cC9weXRob24ve3B9L3B5dGhvbi17cH0tZW1iZWQtYW1kNjQuemlwIi5mb3JtYXQocD1wYXRjaCkKICAgIHppcF9wYXRoID0gb3MucGF0aC5qb2luKG9zLmVudmlyb24uZ2V0KCJURU1QIiwgIi4iKSwgInB5dGhvbi17fS1lbWJlZC1hbWQ2NC56aXAiLmZvcm1hdChwYXRjaCkpCiAgICB0cnk6CiAgICAgICAgZG93bmxvYWRfYW5kX3ZlcmlmeSh1cmwsIHNoYTI1NiwgemlwX3BhdGgpCiAgICAgICAgZXh0cmFjdF9hbmRfcGF0Y2goemlwX3BhdGgsIHN3YXBfZGlyKQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBleGM6CiAgICAgICAgc3lzLnN0ZGVyci53cml0ZSgiZW1iZWQgdmVyc2lvbiBzd2FwIGZhaWxlZDoge31cbiIuZm9ybWF0KGV4YykpCiAgICAgICAgaWYgb3MucGF0aC5pc2Rpcihzd2FwX2Rpcik6CiAgICAgICAgICAgIHNodXRpbC5ybXRyZWUoc3dhcF9kaXIsIGlnbm9yZV9lcnJvcnM9VHJ1ZSkKICAgICAgICByZXR1cm4gMQoKICAgIHRhZyA9ICJmZWxsYmFjayIgaWYgZmVsbF9iYWNrIGVsc2UgInN3YXBwZWQiCiAgICBzeXMuc3Rkb3V0LndyaXRlKCJ7fXx7fXx7fVxuIi5mb3JtYXQodGFnLCBtaW5vciwgc3dhcF9kaXIpKQogICAgcmV0dXJuIDAKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgc3lzLmV4aXQobWFpbigpKQo=" set "HP_FAILFAST_PROBE=IyBTbGljZSAyYi1DIGZhaWwtZmFzdCBwcm9iZTogbGF1bmNoZXMgdGhlIGNhbGxlcidzIHByb2dyYW0sIHdhaXRzIHVwIHRvIEhQX0ZBSUxGQVNUX1BST0JFX01TIHRvCiMgY2xhc3NpZnkgaXQgYXMgImV4aXRlZCBmYXN0IiAoc3RhbGUvYnJva2VuIGNhY2hlZCBhcnRpZmFjdCAtLSBkaXNjYXJkK3JlYnVpbGQgY2FuZGlkYXRlKSB2cy4KIyAic3RpbGwgcnVubmluZyIgKHRoZSB1c2VyJ3MgcmVhbCwgcG9zc2libHkgbG9uZy1ydW5uaW5nIHByb2dyYW0gLS0gbmV2ZXIgdG91Y2hlZCBhZ2FpbikuIE5ldmVyCiMgY2FsbHMgJHAuS2lsbCgpIC0tIHBhc3QgdGhlIHByb2JlIHdpbmRvdyB0aGUgd2FpdCBpcyB1bmJvdW5kZWQgc28gYSBoZWFsdGh5IGFwcCBpcyBuZXZlcgojIGZvcmNlLXN0b3BwZWQuCiMKIyBJbnB1dHMgdmlhIGVudiB2YXJzIChhdm9pZHMgY21kLmV4ZSBxdW90aW5nIGhhemFyZHMpOiBIUF9QUk9CRV9FWEUsIEhQX1BST0JFX0FSR1MsIEhQX1BST0JFX0NXRCwKIyBIUF9GQUlMRkFTVF9QUk9CRV9NUywgSFBfUFJPQkVfT1VUL0hQX1BST0JFX0VSUiAoZGVmYXVsdCB+cnVuLm91dC50eHQvfnJ1bi5lcnIudHh0KSwKIyBIUF9QUk9CRV9SRVNVTFQgKGRlZmF1bHQgfnByb2JlX3Jlc3VsdC50eHQgLS0gIiRleGNlZWRlZHwkZXhpdGNvZGUiLCBOT1Qgc3Rkb3V0KS4gQ2FsbGVyIG11c3QKIyBwcmUtdHJ1bmNhdGUgb3V0cHV0L3Jlc3VsdCBmaWxlcyBiZWZvcmUgaW52b2tpbmcuCiMKIyBkZXJpdmVkIHJlcXVpcmVtZW50IChbUkVRLTAyNl0gYXJndiBwYXNzdGhyb3VnaCk6IEhQX1BST0JFX0FSR1MgaXMgbm93IGEgZnVsbCwgYWxyZWFkeS1xdW90ZWQKIyBBcmd1bWVudHMgc3RyaW5nIChlLmcuIGAiZW50cnkucHkiICItLWZvbyIgImJhciJgKSwgdXNlZCB2ZXJiYXRpbSAtLSBub3QgYSBzaW5nbGUgYmFyZSBwYXRoCiMgcmUtcXVvdGVkIGhlcmUuIENhbGxlciBxdW90ZXMgZWFjaCB0b2tlbjsgc2VlIHJ1bl9zZXR1cC5iYXQncyBIUF9BUFBfQVJHUy4KIwojIExpdmUtdGVlcyB0aGUgY2hpbGQncyBzdGRvdXQvc3RkZXJyIHNvIGEgc3RkaW4taW50ZXJhY3RpdmUgcHJvZ3JhbSdzIHByb21wdHMgcmVhY2ggYSByZWFsCiMgZG91YmxlLWNsaWNrZWQgdXNlciwgaW5zdGVhZCBvZiBvbmx5IHdyaXRpbmcgY2FwdHVyZWQgb3V0cHV0IHRvIGRpc2sgYXQgZXhpdC4KIwojIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IGRvZXMgTk9UIHVzZSBSZWdpc3Rlci1PYmplY3RFdmVudCBvbiBPdXRwdXREYXRhUmVjZWl2ZWQvRXJyb3JEYXRhUmVjZWl2ZWQuCiMgVGhhdCB3YXMgdGhlIG9yaWdpbmFsIGRlc2lnbiBhbmQgd2FzIGZvdW5kLCB2aWEgdGhpcyByZXBvJ3Mgb3duIGxvY2FsIHB3c2ggdGVzdGluZywgdG8KIyByZW9yZGVyIGxpbmVzIFdJVEhJTiBhIHNpbmdsZSBzdHJlYW0gKGUuZy4gcm91bmQgMidzIG91dHB1dCBsYW5kaW5nIGJlZm9yZSByb3VuZCAxJ3MgaW4gdGhlCiMgY2FwdHVyZWQvdGVlZCB0ZXh0LCBub24tZGV0ZXJtaW5pc3RpY2FsbHkpIC0tIHJvb3QtY2F1c2VkIHRvIGEgY29uZmlybWVkLCBmaWxlZCBQb3dlclNoZWxsIGJ1ZwojIChQb3dlclNoZWxsL1Bvd2VyU2hlbGwjMTE5MzcpOiB0aG9zZSBldmVudHMgZGlzcGF0Y2ggdmlhIFRocmVhZFBvb2wuUXVldWVVc2VyV29ya0l0ZW0sIHdoaWNoCiMgZG9lcyBub3QgZ3VhcmFudGVlIGRlbGl2ZXJ5IG9yZGVyIHdoZW4gc2V2ZXJhbCBsaW5lcyBhcnJpdmUgY2xvc2UgdG9nZXRoZXIuIEZpeGVkIGJ5IHBvbGxpbmcKIyByZWFkcyBkaXJlY3RseSBpbnN0ZWFkOiBvbmx5IE9ORSByZWFkIGlzIGV2ZXIgaW4gZmxpZ2h0IHBlciBzdHJlYW0gYXQgYSB0aW1lICh0aGUgbmV4dCByZWFkIGlzCiMgbm90IGlzc3VlZCB1bnRpbCB0aGUgY3VycmVudCBvbmUgaXMgY29uc3VtZWQpLCBzbyB0aGVyZSBpcyBubyBwb3NzaWJsZSBvdXQtb2Ytb3JkZXIgZGVsaXZlcnkKIyBmb3IgYSBzaW5nbGUgc3RyZWFtIC0tIG9yZGVyaW5nIGlzIHNlbGYtc2VxdWVuY2VkLCBub3QgZGVwZW5kZW50IG9uIGFueSBydW50aW1lJ3MKIyBjYWxsYmFjay1zY2hlZHVsaW5nIGd1YXJhbnRlZS4gQ3Jvc3Mtc3RyZWFtIChzdGRvdXQgdnMgc3RkZXJyKSBpbnRlcmxlYXZpbmcgd2FzIG5ldmVyCiMgZ3VhcmFudGVlZCBhbmQgc3RpbGwgaXNuJ3QgLS0gdGhhdCByZWZsZWN0cyB0aGUgY2hpbGQncyBvd24gdHdvIGluZGVwZW5kZW50IHBpcGVzLCBub3QgYSBidWcuCiMKIyBkZXJpdmVkIHJlcXVpcmVtZW50IChGaW5kaW5nIDksIDIwMjYtMDctMjQpOiByZWFkcyB2aWEgU3RyZWFtUmVhZGVyLlJlYWRBc3luYyhjaGFyW10sIGludCwgaW50KQojIChyYXcgY2h1bmtzKSwgTk9UIFJlYWRMaW5lQXN5bmMoKS4gUmVhZExpbmVBc3luYygpIG9ubHkgcmV0dXJucyBvbmNlIGl0IHNlZXMgYSBmdWxsCiMgbmV3bGluZS10ZXJtaW5hdGVkIGxpbmUgLS0gY29uZmlybWVkIGVtcGlyaWNhbGx5IHRoYXQgUHl0aG9uJ3Mgb3duIGBpbnB1dCgicHJvbXB0IilgIChubwojIHRyYWlsaW5nIG5ld2xpbmUgYnkgZGVzaWduLCBzbyB0aGUgY3Vyc29yIHN0YXlzIG9uIHRoZSBzYW1lIGxpbmUpIGlzIGdlbnVpbmVseSBmbHVzaGVkIHRvIHRoZQojIE9TIHBpcGUgaW1tZWRpYXRlbHkgYnV0IHN0YXlzIGludmlzaWJsZSB0byBhIFJlYWRMaW5lQXN5bmMtYmFzZWQgcmVhZGVyIHVudGlsIHNvbWV0aGluZyBlbHNlCiMgbGF0ZXIgZmx1c2hlcyBhIG5ld2xpbmUsIG9yIHRoZSBwcm9jZXNzIGV4aXRzLiBBIGNodW5rLWJhc2VkIHJlYWQgc3VyZmFjZXMgd2hhdGV2ZXIgYnl0ZXMgYXJlCiMgYWN0dWFsbHkgYXZhaWxhYmxlIHRoZSBtb21lbnQgdGhleSBhcnJpdmUsIG1hdGNoaW5nIGhvdyBhIHJlYWwgdGVybWluYWwgYmVoYXZlcy4gRU9GIGlzIGEKIyAwLWxlbmd0aCByZWFkIChub3QgYSBudWxsIHJlc3VsdCB0aGUgd2F5IFJlYWRMaW5lQXN5bmMgc2lnbmFscyBpdCkuIFNlZQojIGRvY3MvcGxhbi1jbGktaW50ZXJhY3RpdmUtdmVyaWZpY2F0aW9uLm1kIEZpbmRpbmcgOSBmb3IgdGhlIGZ1bGwgZW1waXJpY2FsIHRyYWNlLgojCiMgRnVsbCByYXRpb25hbGUgKyBjaXRhdGlvbnM6IGRvY3MvcGxhbi1jbGktaW50ZXJhY3RpdmUtdmVyaWZpY2F0aW9uLm1kIEZpbmRpbmdzIDViLzYvNy84LzkuCiMKIyBUaGlzIGlzIHRoZSBjYW5vbmljYWwgc291cmNlIGZvciB0aGUgSFBfRkFJTEZBU1RfUFJPQkUgYmFzZTY0IHBheWxvYWQgZW1iZWRkZWQgaW4KIyBydW5fc2V0dXAuYmF0LiBBZnRlciBlZGl0aW5nLCByZS1lbmNvZGUgYW5kIHBhc3RlIGl0IGludG8gdGhlIGBzZXQgIkhQX0ZBSUxGQVNUX1BST0JFPS4uLiJgCiMgbGluZTsgdGVzdHMvdGVzdF9mYWlsZmFzdF9wcm9iZS5weSBhc3NlcnRzIHRoZSBlbWJlZGRlZCBwYXlsb2FkIG1hdGNoZXMgdGhpcyBmaWxlICh3aXRoCiMgQ1JMRi9MRiBub3JtYWxpemVkLCBwZXIgdGhlIC5wczEgUGF5bG9hZFN5bmMgY29udmVudGlvbiAtLSBzZWUKIyBkb2NzL2FnZW50LWxlc3NvbnMtbGVhcm5lZC5tZCAiRW1iZWRkZWQgSGVscGVyIFVwZGF0ZSBXb3JrZmxvdyIpLgokZXhlID0gJGVudjpIUF9QUk9CRV9FWEUKJHJhd0FyZ3MgPSAkZW52OkhQX1BST0JFX0FSR1MKJHdvcmtEaXIgPSAkZW52OkhQX1BST0JFX0NXRAokcHJvYmVNcyA9IFtpbnRdJGVudjpIUF9GQUlMRkFTVF9QUk9CRV9NUwokb3V0UGF0aCA9ICRlbnY6SFBfUFJPQkVfT1VUCmlmICgtbm90ICRvdXRQYXRoKSB7ICRvdXRQYXRoID0gJ35ydW4ub3V0LnR4dCcgfQokZXJyUGF0aCA9ICRlbnY6SFBfUFJPQkVfRVJSCmlmICgtbm90ICRlcnJQYXRoKSB7ICRlcnJQYXRoID0gJ35ydW4uZXJyLnR4dCcgfQokcmVzdWx0UGF0aCA9ICRlbnY6SFBfUFJPQkVfUkVTVUxUCmlmICgtbm90ICRyZXN1bHRQYXRoKSB7ICRyZXN1bHRQYXRoID0gJ35wcm9iZV9yZXN1bHQudHh0JyB9Cgokc2kgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvCiRzaS5GaWxlTmFtZSA9ICRleGUKaWYgKCRyYXdBcmdzKSB7ICRzaS5Bcmd1bWVudHMgPSAkcmF3QXJncyB9CiRzaS5Xb3JraW5nRGlyZWN0b3J5ID0gJHdvcmtEaXIKJHNpLlVzZVNoZWxsRXhlY3V0ZSA9ICRmYWxzZQokc2kuUmVkaXJlY3RTdGFuZGFyZE91dHB1dCA9ICR0cnVlCiRzaS5SZWRpcmVjdFN0YW5kYXJkRXJyb3IgPSAkdHJ1ZQokcCA9IE5ldy1PYmplY3QgU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MKJHAuU3RhcnRJbmZvID0gJHNpCiRwLlN0YXJ0KCkgfCBPdXQtTnVsbApXcml0ZS1Ib3N0ICJbSU5GT10gUHJvY2VzcyBJRCAkKCRwLklkKS4gSWYgaXQgc2VlbXMgc3R1Y2s6IFRhc2sgTWFuYWdlciA+IERldGFpbHMgdGFiID4gZmluZCB0aGlzIFBJRCA+IEVuZCBUYXNrICh0aGlzIHdpbmRvdyBzdGF5cyBvcGVuKS4iCgokb3V0QnVmID0gTmV3LU9iamVjdCBTeXN0ZW0uVGV4dC5TdHJpbmdCdWlsZGVyCiRlcnJCdWYgPSBOZXctT2JqZWN0IFN5c3RlbS5UZXh0LlN0cmluZ0J1aWxkZXIKJG91dENodW5rQnVmID0gTmV3LU9iamVjdCBjaGFyW10gNDA5NgokZXJyQ2h1bmtCdWYgPSBOZXctT2JqZWN0IGNoYXJbXSA0MDk2CiRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZEFzeW5jKCRvdXRDaHVua0J1ZiwgMCwgJG91dENodW5rQnVmLkxlbmd0aCkKJGVyclRhc2sgPSAkcC5TdGFuZGFyZEVycm9yLlJlYWRBc3luYygkZXJyQ2h1bmtCdWYsIDAsICRlcnJDaHVua0J1Zi5MZW5ndGgpCiRvdXREb25lID0gJGZhbHNlCiRlcnJEb25lID0gJGZhbHNlCgokc3cgPSBbU3lzdGVtLkRpYWdub3N0aWNzLlN0b3B3YXRjaF06OlN0YXJ0TmV3KCkKJGV4Y2VlZGVkID0gMAp3aGlsZSAoKC1ub3QgJHAuSGFzRXhpdGVkKSAtb3IgKC1ub3QgJG91dERvbmUpIC1vciAoLW5vdCAkZXJyRG9uZSkpIHsKICAgIGlmICgoLW5vdCAkb3V0RG9uZSkgLWFuZCAkb3V0VGFzay5Jc0NvbXBsZXRlZCkgewogICAgICAgICRuID0gJG91dFRhc2suUmVzdWx0CiAgICAgICAgaWYgKCRuIC1lcSAwKSB7CiAgICAgICAgICAgICRvdXREb25lID0gJHRydWUKICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAkY2h1bmsgPSBbc3RyaW5nXTo6bmV3KCRvdXRDaHVua0J1ZiwgMCwgJG4pCiAgICAgICAgICAgIFtDb25zb2xlXTo6T3V0LldyaXRlKCRjaHVuaykKICAgICAgICAgICAgJG51bGwgPSAkb3V0QnVmLkFwcGVuZCgkY2h1bmspCiAgICAgICAgICAgICRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZEFzeW5jKCRvdXRDaHVua0J1ZiwgMCwgJG91dENodW5rQnVmLkxlbmd0aCkKICAgICAgICB9CiAgICB9CiAgICBpZiAoKC1ub3QgJGVyckRvbmUpIC1hbmQgJGVyclRhc2suSXNDb21wbGV0ZWQpIHsKICAgICAgICAkbiA9ICRlcnJUYXNrLlJlc3VsdAogICAgICAgIGlmICgkbiAtZXEgMCkgewogICAgICAgICAgICAkZXJyRG9uZSA9ICR0cnVlCiAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgJGNodW5rID0gW3N0cmluZ106Om5ldygkZXJyQ2h1bmtCdWYsIDAsICRuKQogICAgICAgICAgICBbQ29uc29sZV06OkVycm9yLldyaXRlKCRjaHVuaykKICAgICAgICAgICAgJG51bGwgPSAkZXJyQnVmLkFwcGVuZCgkY2h1bmspCiAgICAgICAgICAgICRlcnJUYXNrID0gJHAuU3RhbmRhcmRFcnJvci5SZWFkQXN5bmMoJGVyckNodW5rQnVmLCAwLCAkZXJyQ2h1bmtCdWYuTGVuZ3RoKQogICAgICAgIH0KICAgIH0KICAgIGlmICgoLW5vdCAkZXhjZWVkZWQpIC1hbmQgKCRzdy5FbGFwc2VkTWlsbGlzZWNvbmRzIC1nZSAkcHJvYmVNcykpIHsKICAgICAgICAkZXhjZWVkZWQgPSAxCiAgICB9CiAgICBTdGFydC1TbGVlcCAtTWlsbGlzZWNvbmRzIDIwCn0KJHAuV2FpdEZvckV4aXQoKQoKJG91dEJ1Zi5Ub1N0cmluZygpIHwgU2V0LUNvbnRlbnQgLVBhdGggJG91dFBhdGggLUVuY29kaW5nIEFTQ0lJCiRlcnJCdWYuVG9TdHJpbmcoKSB8IFNldC1Db250ZW50IC1QYXRoICRlcnJQYXRoIC1FbmNvZGluZyBBU0NJSQoiJGV4Y2VlZGVkfCQoJHAuRXhpdENvZGUpIiB8IFNldC1Db250ZW50IC1QYXRoICRyZXN1bHRQYXRoIC1FbmNvZGluZyBBU0NJSQo=" diff --git a/tests/harness.ps1 b/tests/harness.ps1 index 54344c90..23eb468c 100644 --- a/tests/harness.ps1 +++ b/tests/harness.ps1 @@ -405,6 +405,18 @@ $depMsgPos = $AllText.IndexOf('[INFO] Installing dependencies') $depCallPos = $AllText.IndexOf('[TRACE] heuristic augmentation: ~prep_requirements.py') $depInstallProgress = ($depMsgPos -ge 0) -and ($depCallPos -ge 0) -and ($depMsgPos -lt $depCallPos) Write-Result 'batch.progress.dep_install' "Progress message before dependency install phase: user sees 'Installing dependencies' before the slow pip/conda/uv install step" $depInstallProgress @{ msgIdx = $depMsgPos; opIdx = $depCallPos; ordered = $depInstallProgress } +# derived requirement (CLAUDE.md Active Backlog Item 39): the EXE fast path's freshness +# check switched from mtime-only to a content-hash comparison; the hash must be (re)written +# after a genuine fresh build (:success, gated on HP_FASTPATH_USED being UNSET so the +# already-fast reuse case never pays a redundant re-hash pass) via a dedicated subroutine +# (not inlined, to avoid the parenthesized-block set-then-read hazard). Static wiring guard +# only -- runtime proof is tests/test_fast_check.py (the embedded script itself, via real +# pwsh) plus whatever selfapps coverage exercises :try_fast_exe end-to-end. +$hasWriteFastHashLabel = $AllText -match '(?m)^:write_fast_hash\s*$' +$hasWriteFastHashCall = $AllText -match 'if not defined HP_FASTPATH_USED call :write_fast_hash' +$hasWriteModeArg = $AllText -match '"%HP_FAST_CHECK_PS%"\s+"dist\\%ENVNAME%\.exe"\s+write' +$hasFastHashWiring = $hasWriteFastHashLabel -and $hasWriteFastHashCall -and $hasWriteModeArg +Write-Result 'batch.fastpath.hash_write' 'EXE fast path freshness check: :write_fast_hash subroutine exists, is called from :success gated on HP_FASTPATH_USED, and invokes the payload in write mode' $hasFastHashWiring @{ hasLabel = $hasWriteFastHashLabel; hasCall = $hasWriteFastHashCall; hasWriteModeArg = $hasWriteModeArg } # derived requirement: pre-build --collect-submodules double-gate (REQ-005.x) must stay wired. # Static guard against silent deletion; runtime proof is self.collect.submodules (selfapps_collect.ps1). $collectCall = $AllText -match 'call :compute_collect_flags' diff --git a/tests/selfapps_fastpath_hash.ps1 b/tests/selfapps_fastpath_hash.ps1 new file mode 100644 index 00000000..636026f7 --- /dev/null +++ b/tests/selfapps_fastpath_hash.ps1 @@ -0,0 +1,152 @@ +# ASCII only +# selfapps_fastpath_hash.ps1 - regression guard for CLAUDE.md Active Backlog Item 39: the +# EXE fast path's freshness check must detect a genuine content change even when the +# changed file's mtime is backdated below the built EXE's own mtime -- the exact failure +# mode a timestamp-preserving delivery method (a ZIP, xcopy, robocopy) produces. Before this +# fix, freshness was mtime-only over *.py files, so this scenario silently reused the stale +# EXE with no signal to the user (see CLAUDE.md's Item 39 "Realistic scenario"). +# +# Run 1: builds the EXE from entry.py printing FASTPATH_TOKEN_V1. +# Run 2: entry.py is rewritten to print FASTPATH_TOKEN_V2 instead, but its mtime is set to a +# date well BEFORE dist\.exe's own mtime (simulating a delivery method that +# preserves the file's original authored timestamp). The OLD mtime-only check would +# have wrongly reused the stale V1 EXE here; the new content-hash check +# (tools/fast_check.ps1) must detect the change regardless of mtime and rebuild. +# Assert: run 2's captured EXE stdout (~run.out.txt, written by both the fast-path-reuse +# launch and the fresh-build smokerun -- see docs/agent-interconnect.md's "EXE fast path" +# section) shows FASTPATH_TOKEN_V2, not V1 -- direct evidence the NEW content actually ran, +# not just that some log line happened to say "rebuilding". Also asserts the "Fast path: +# reusing" log line is ABSENT for run 2, confirming a genuine rebuild occurred rather than +# the new token coincidentally reaching the old EXE some other way. +# +# Lane: uv, non-gating for its first landing -- the isolated tools/fast_check.ps1 script is +# already verified via real pwsh (tests/test_fast_check.py), but this is the first time the +# full :try_fast_exe -> :run_entry_smoke -> :success -> :write_fast_hash cycle is exercised +# end-to-end on real Windows CI. Promote once proven stable across several real runs, +# matching this repo's established graduation pattern (CLAUDE.md's "CI lane gating +# maturity" periodic check). +param() +$ErrorActionPreference = 'Continue' +$here = $PSScriptRoot +$repo = Split-Path -Path $here -Parent +$nd = Join-Path $here '~test-results.ndjson' +$ciNd = Join-Path $repo 'ci_test_results.ndjson' +if (-not (Test-Path $nd)) { New-Item -ItemType File -Path $nd -Force | Out-Null } +if (-not (Test-Path $ciNd)) { New-Item -ItemType File -Path $ciNd -Force | Out-Null } + +function Write-NdjsonRow { + param([hashtable]$Row) + $lane = [Environment]::GetEnvironmentVariable('HP_CI_LANE') + if ($lane -and -not $Row.ContainsKey('lane')) { $Row['lane'] = $lane } + $json = $Row | ConvertTo-Json -Compress -Depth 8 + Add-Content -LiteralPath $nd -Value $json -Encoding Ascii + Add-Content -LiteralPath $ciNd -Value $json -Encoding Ascii +} + +# Non-Windows skip +if (-not $IsWindows) { + $platform = [System.Environment]::OSVersion.Platform.ToString() + Write-NdjsonRow ([ordered]@{ + id = 'self.fastpath.hash.backdated_mtime' + req = 'REQ-018' + pass = $true + desc = 'EXE fast path content-hash freshness (skipped on non-Windows)' + details = [ordered]@{ skip = $true; platform = $platform; reason = 'non-windows-host' } + }) + exit 0 +} + +$batchPath = Join-Path $repo 'run_setup.bat' +if (-not (Test-Path $batchPath)) { + Write-NdjsonRow ([ordered]@{ + id = 'self.fastpath.hash.backdated_mtime' + req = 'REQ-018' + pass = $false + desc = 'EXE fast path content-hash freshness: run_setup.bat not found' + details = [ordered]@{ error = 'run_setup.bat not found at ' + $batchPath } + }) + exit 1 +} + +$workDir = Join-Path $here '~selftest_fastpath_hash' +if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir } +New-Item -ItemType Directory -Force -Path $workDir | Out-Null +Copy-Item -Path $batchPath -Destination $workDir -Force + +Set-Content -Path (Join-Path $workDir 'entry.py') -Value @' +print("FASTPATH_TOKEN_V1") +'@ -Encoding ASCII + +$prev = if (Test-Path Env:HP_SKIP_PIPREQS) { $env:HP_SKIP_PIPREQS } else { $null } +$env:HP_SKIP_PIPREQS = '1' + +function Invoke-Bootstrap { + param([string]$LogName) + Push-Location $workDir + try { + cmd /c "call run_setup.bat > $LogName 2>&1" + return $LASTEXITCODE + } finally { + Pop-Location + } +} + +try { + # Run 1: fresh build, entry.py prints V1. + $run1Exit = Invoke-Bootstrap '~fastpath_hash_run1.log' + + # Rewrite entry.py to print V2, then backdate its mtime well before dist\.exe's own + # mtime -- simulating a ZIP/xcopy/robocopy delivery that preserves the original + # authored timestamp instead of stamping "now" on extraction. + $entryPath = Join-Path $workDir 'entry.py' + Set-Content -Path $entryPath -Value @' +print("FASTPATH_TOKEN_V2") +'@ -Encoding ASCII + $backdated = Get-Date -Year 2001 -Month 9 -Day 9 + (Get-Item -LiteralPath $entryPath).LastWriteTime = $backdated + (Get-Item -LiteralPath $entryPath).LastWriteTimeUtc = $backdated.ToUniversalTime() + + # Run 2: sources genuinely changed (V2), but entry.py's mtime lies about it. + $run2Exit = Invoke-Bootstrap '~fastpath_hash_run2.log' +} finally { + if ($null -eq $prev) { + Remove-Item Env:HP_SKIP_PIPREQS -ErrorAction SilentlyContinue + } else { + $env:HP_SKIP_PIPREQS = $prev + } +} + +$run2LogPath = Join-Path $workDir '~fastpath_hash_run2.log' +$run2SetupLog = Join-Path $workDir '~setup.log' +$run2Lines = if (Test-Path $run2LogPath) { Get-Content -LiteralPath $run2LogPath -Encoding ASCII } else { @() } +$run2Setup = if (Test-Path $run2SetupLog) { Get-Content -LiteralPath $run2SetupLog -Raw -Encoding ASCII } else { '' } +$run2Combined = ($run2Lines -join "`n") + "`n" + $run2Setup + +$runOutPath = Join-Path $workDir '~run.out.txt' +$runOutText = if (Test-Path $runOutPath) { Get-Content -LiteralPath $runOutPath -Raw -Encoding ASCII } else { '' } + +$tokenV2Seen = $runOutText -match 'FASTPATH_TOKEN_V2' +$tokenV1Seen = $runOutText -match 'FASTPATH_TOKEN_V1' +$fastPathReusedRun2 = $run2Combined -match [regex]::Escape('Fast path: reusing') + +# Genuine rebuild detected: run 2 shows the NEW token, not the old one, and did not take +# the "reusing" fast path. +$pass = ($run1Exit -eq 0) -and ($run2Exit -eq 0) -and $tokenV2Seen -and (-not $tokenV1Seen) -and (-not $fastPathReusedRun2) + +Write-NdjsonRow ([ordered]@{ + id = 'self.fastpath.hash.backdated_mtime' + req = 'REQ-018' + pass = $pass + desc = 'EXE fast path content-hash freshness detects a genuine change even when the changed file mtime is backdated below the EXE' + details = [ordered]@{ + run1Exit = $run1Exit + run2Exit = $run2Exit + tokenV2Seen = $tokenV2Seen + tokenV1Seen = $tokenV1Seen + fastPathReusedRun2 = $fastPathReusedRun2 + run2Log = '~fastpath_hash_run2.log' + } +}) + +if (-not $pass) { exit 1 } +exit 0 diff --git a/tests/test_fast_check.py b/tests/test_fast_check.py new file mode 100644 index 00000000..13f9b5a8 --- /dev/null +++ b/tests/test_fast_check.py @@ -0,0 +1,111 @@ +"""Tests for tools/fast_check.ps1 -- CLAUDE.md Active Backlog Item 39's fix for the EXE fast +path's freshness check. Previously mtime-only over *.py files: (a) a timestamp-preserving +delivery method (a ZIP, xcopy, robocopy) could carry a genuinely changed file whose mtime +still predates the built EXE, silently reusing stale logic with no signal to the user, and +(b) a requirements.txt/pyproject.toml/runtime.txt change was invisible to the scan entirely. +Fixed by switching to a content-hash comparison (never fooled by a preserved/backdated +mtime) over the same *.py file set, extended to include those three dependency files. + +No importable functions exist -- exercised end-to-end via a real `pwsh` subprocess, mirroring +tests/test_migrate_gitattributes.py's own harness. +""" +import base64 +import re +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +SOURCE = REPO / "tools" / "fast_check.ps1" +PWSH = shutil.which("pwsh") + + +def _run_check(cwd, exe="dist/myapp.exe", mode="check"): + result = subprocess.run( + [PWSH, "-NoProfile", "-NonInteractive", "-File", str(SOURCE), exe, mode], + cwd=str(cwd), + capture_output=True, + text=True, + timeout=30, + ) + return result.returncode, result.stdout.strip(), result.stderr + + +@unittest.skipUnless(PWSH, "pwsh not available") +class FreshnessBehavior(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp() + self.root = Path(self.tmp) + (self.root / "dist").mkdir() + (self.root / "dist" / "myapp.exe").write_text("fake-exe") + (self.root / "entry.py").write_text("print('hello')\n") + (self.root / "requirements.txt").write_text("requests==2.0\n") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_check_with_no_stored_hash_is_not_fresh(self): + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + def test_write_then_check_is_fresh(self): + _run_check(self.root, mode="write") + self.assertTrue((self.root / "~fast_check.hash.txt").exists()) + rc, out, err = _run_check(self.root) + self.assertEqual(out, "fresh", err) + + def test_content_change_with_backdated_mtime_is_not_fresh(self): + # The core Item 39 regression case: a ZIP/xcopy/robocopy delivery can carry a + # genuinely changed file whose mtime is preserved from BEFORE the build -- the old + # mtime-only check would have wrongly reported "fresh" here. + _run_check(self.root, mode="write") + entry = self.root / "entry.py" + entry.write_text("print('changed')\n") + old = 1000000000 # 2001-09-09, far before dist/myapp.exe's own mtime + import os + os.utime(entry, (old, old)) + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + def test_dependency_file_only_change_is_not_fresh(self): + # requirements.txt is not a *.py file -- the old mtime scan never looked at it at + # all, so this change (Item 39's exposure (b)) was previously invisible regardless + # of mtime. + _run_check(self.root, mode="write") + (self.root / "requirements.txt").write_text("requests==3.0\n") + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + def test_rewrite_after_change_is_fresh_again(self): + _run_check(self.root, mode="write") + (self.root / "entry.py").write_text("print('changed')\n") + _run_check(self.root, mode="write") + rc, out, err = _run_check(self.root) + self.assertEqual(out, "fresh", err) + + def test_missing_exe_is_not_fresh(self): + _run_check(self.root, mode="write") + (self.root / "dist" / "myapp.exe").unlink() + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + +class PayloadSync(unittest.TestCase): + def test_embedded_base64_matches_source(self): + # See tests/test_migrate_gitattributes.py's own PayloadSync test for why CRLF/LF + # normalization is required here (`*.ps1 text eol=crlf` in .gitattributes). + bat = (REPO / "run_setup.bat").read_text(encoding="ascii", errors="replace") + m = re.search(r'set "HP_FAST_CHECK=([A-Za-z0-9+/=]+)"', bat) + self.assertIsNotNone(m, "HP_FAST_CHECK payload not found in run_setup.bat") + decoded = base64.b64decode(m.group(1)).replace(b"\r\n", b"\n") + source = SOURCE.read_bytes().replace(b"\r\n", b"\n") + self.assertEqual( + decoded, source, + "HP_FAST_CHECK base64 is out of sync with tools/fast_check.ps1; re-sync it.", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/fast_check.ps1 b/tools/fast_check.ps1 new file mode 100644 index 00000000..38c0ed36 --- /dev/null +++ b/tools/fast_check.ps1 @@ -0,0 +1,73 @@ +# ASCII only. CLAUDE.md Active Backlog Item 39: the EXE fast path's freshness check was +# mtime-only over *.py files, so (a) a timestamp-preserving delivery method (a ZIP, xcopy, +# robocopy) could carry a genuinely changed file whose mtime still predates the built EXE, +# silently reusing stale logic with no signal to the user, and (b) a requirements.txt/ +# pyproject.toml/runtime.txt change was invisible to the scan entirely, mtime or not. Fixes +# both: freshness is now a content-hash comparison (never fooled by a preserved/backdated +# mtime) over the SAME *.py file set already scanned, extended to include those three +# dependency files. +# +# Two modes, selected by the second positional argument (default 'check'): +# check (run from :try_fast_exe): print 'fresh' iff a stored hash exists AND matches the +# hash of the CURRENT source set. Prints nothing (falls through to a rebuild) on any +# mismatch, or when no stored hash exists yet (e.g. the first run under this fix, +# or dist\.exe was built by an older run_setup.bat with no hash file at all) -- +# a safe, one-time-cost default that never mis-reports staleness as freshness. +# write (run from :success, only when a fresh build attempt just happened -- see the +# call site's own HP_FASTPATH_USED gate for why the fast-reuse case skips this and +# avoids a redundant second hash pass): unconditionally (re)write the hash of the +# CURRENT source set. Always run from the app root as CWD (both call sites already +# guarantee this), so relative paths captured via Resolve-Path -Relative match +# between the write and a later check. +# +# Uses [System.Security.Cryptography.SHA256] directly, not Get-FileHash -- see +# docs/agent-lessons-learned.md's "Prefer raw .NET types over Utility-module cmdlets" entry: +# Microsoft.PowerShell.Utility is not guaranteed to auto-load in the for/f-backtick and +# -File invocation shapes this repo's embedded helpers run under on real Windows PowerShell +# 5.1, and this class of gap is invisible to local pwsh (Linux, PowerShell 7) testing. +# +# This is the canonical source for the HP_FAST_CHECK base64 payload embedded in +# run_setup.bat. After editing, re-sync via: +# python tools/sync_payload.py HP_FAST_CHECK tools/fast_check.ps1 +# tests/test_fast_check.py asserts the embedded payload matches this file byte-for-byte +# (after CRLF/LF normalization, since this file is eol=crlf but was authored on LF). + +$exe = $args[0] +if (-not $exe) { $exe = $env:HP_FAST_EXE } +$mode = $args[1] +if (-not $mode) { $mode = 'check' } + +$hashFile = '~fast_check.hash.txt' +$infraPattern = '(?i)(^|[/\\])(\.git|\.github|dist|\.venv|\.uv_env|__pycache__|\.conda)([/\\]|$)' +$py = @(Get-ChildItem -Recurse -File -Filter '*.py' | Where-Object { $_.FullName -notmatch $infraPattern -and $_.Name -notlike '~*.py' }) +$depNames = @('requirements.txt', 'pyproject.toml', 'runtime.txt') +$deps = @($depNames | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | ForEach-Object { Get-Item -LiteralPath $_ }) +$sources = @($py) + @($deps) + +if (-not $sources) { + if ($mode -eq 'check') { exit 1 } else { exit 0 } +} + +$sha = [System.Security.Cryptography.SHA256]::Create() +$lines = foreach ($f in ($sources | Sort-Object -Property FullName)) { + $relPath = Resolve-Path -LiteralPath $f.FullName -Relative + $bytes = [System.IO.File]::ReadAllBytes($f.FullName) + $hashBytes = $sha.ComputeHash($bytes) + $hex = -join ($hashBytes | ForEach-Object { $_.ToString('x2') }) + "$relPath|$hex" +} +$combined = [string]::Join("`n", $lines) +$combinedBytes = [System.Text.Encoding]::UTF8.GetBytes($combined) +$digestBytes = $sha.ComputeHash($combinedBytes) +$digest = -join ($digestBytes | ForEach-Object { $_.ToString('x2') }) + +if ($mode -eq 'write') { + [System.IO.File]::WriteAllText($hashFile, $digest) + exit 0 +} + +if (-not (Test-Path -LiteralPath $hashFile)) { exit 1 } +if (-not $exe) { exit 1 } +if (-not (Test-Path -LiteralPath $exe)) { exit 1 } +$stored = [System.IO.File]::ReadAllText($hashFile).Trim() +if ($stored -eq $digest) { 'fresh' } From 8eaa271f72d05f09d74a9dad8cae16bf363042f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 13:19:13 +0000 Subject: [PATCH 2/3] Fix real correctness bug in fast-hash write gate (CodeRabbit review) HP_FASTPATH_USED being unset does not prove a build succeeded this run -- a skipped or genuinely failed rebuild also leaves it unset, and if a stale dist\.exe from an earlier successful run was still sitting there untouched, the original gate would write the CURRENT (changed) sources' hash paired against that OLD binary. The next run would then wrongly trust the stale EXE as "fresh", reintroducing Item 39's own bug one layer down. Fixed with a new, more precise flag, HP_FRESH_BUILD_OK: reset once per fresh build attempt, set ONLY in the 4 genuine build-success branches inside :run_entry_smoke's PyInstaller/Tier-A build block, never on a warn_build_incomplete path. Since a build can only be attempted after the fast path declines to fire, this is a strictly more precise replacement for the old gate, not an additional condition -- :success now checks HP_FRESH_BUILD_OK alone. Also fixes two smaller review findings: harness.ps1's static wiring check now scopes its assertions to :success/:write_fast_hash's own bodies (bounded by the next label) instead of whole-file matches that could pass on unrelated text; the new selfapps test now asserts run 1 genuinely wrote the stored hash before proceeding, closing a gap where a completely broken write side would have been indistinguishable from a working one (a missing hash file also safely forces a rebuild). Uploads the new test's scenario logs as CI artifacts, matching every sibling scenario's convention. Two other CodeRabbit findings investigated and NOT applied, with reasoning left in a PR reply: the $IsWindows Windows-PowerShell-5.1 concern doesn't apply here since this script (like 47 others in the repo) runs via `shell: pwsh` in CI, not classic powershell.exe; the suggested TLS 1.2 SecurityProtocol assignment doesn't apply since fast_check.ps1 makes no network calls at all. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017SQ1rvJxDbE71pTXJ4QvLV --- .github/workflows/batch-check.yml | 8 +++++ CLAUDE.md | 54 +++++++++++++++++++++++-------- docs/agent-interconnect.md | 45 ++++++++++++++++++++------ run_setup.bat | 43 ++++++++++++++++++------ tests/harness.ps1 | 29 +++++++++++------ tests/selfapps_fastpath_hash.ps1 | 16 +++++++-- 6 files changed, 150 insertions(+), 45 deletions(-) diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index 82364118..2bdd020b 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -1908,6 +1908,14 @@ jobs: tests\~selftest_exefastpath\~exefastpath_run2.log tests/~selftest_exefastpath/~setup.log tests\~selftest_exefastpath\~setup.log + tests/~selftest_fastpath_hash/~fastpath_hash_run1.log + tests\~selftest_fastpath_hash\~fastpath_hash_run1.log + tests/~selftest_fastpath_hash/~fastpath_hash_run2.log + tests\~selftest_fastpath_hash\~fastpath_hash_run2.log + tests/~selftest_fastpath_hash/~setup.log + tests\~selftest_fastpath_hash\~setup.log + tests/~selftest_fastpath_hash/~run.out.txt + tests\~selftest_fastpath_hash\~run.out.txt tests/~pandas_excel/~pandas_excel.log tests\~pandas_excel\~pandas_excel.log tests/~pipgap/~pipgap.log diff --git a/CLAUDE.md b/CLAUDE.md index 7214206d..630ac6a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -877,26 +877,54 @@ but several represent real gaps worth closing before calling the path fully rele and matches; a missing stored hash (first run under this fix, or an EXE built by an older `run_setup.bat`) is a safe, one-time-cost "not fresh" default, never a false "fresh." `write` (new subroutine `:write_fast_hash`, called from `:success`) unconditionally (re)writes the - hash after a genuine fresh build attempt -- gated on `HP_FASTPATH_USED` being unset so the - already-fast reuse case never pays a redundant second hash pass (the `:try_fast_exe` check - already computed it once; nothing changed, so the stored value is already correct). Uses - `[System.Security.Cryptography.SHA256]` directly, not `Get-FileHash`, per this repo's own - "Prefer raw .NET types over Utility-module cmdlets" lesson (Utility-module auto-load is not - guaranteed in the `-File` invocation shape embedded helpers run under on real Windows - PowerShell 5.1, invisible to local `pwsh`/Linux testing). + hash after a genuine fresh build attempt. Uses `[System.Security.Cryptography.SHA256]` + directly, not `Get-FileHash`, per this repo's own "Prefer raw .NET types over Utility-module + cmdlets" lesson (Utility-module auto-load is not guaranteed in the `-File` invocation shape + embedded helpers run under on real Windows PowerShell 5.1, invisible to local `pwsh`/Linux + testing). + + **A real bug in the first-shipped `write` gate, found by CodeRabbit's review of this same + PR (#460) before landing.** The original gate was `if not defined HP_FASTPATH_USED call + :write_fast_hash` -- but `HP_FASTPATH_USED` being unset does not prove a build actually + SUCCEEDED this run; a skipped or FAILED rebuild also leaves it unset, and if a stale + `dist\.exe` from an EARLIER successful run was still sitting there untouched, this gate + would write the CURRENT (changed) sources' hash paired against that OLD binary -- the next + run would then wrongly trust the stale EXE as "fresh," silently reintroducing the exact class + of bug this item exists to fix, just relocated one layer down. Fixed with a new, more precise + flag, `HP_FRESH_BUILD_OK` -- reset once per fresh build attempt (alongside + `HP_NUITKA_FALLBACK_USED` in `:run_entry_smoke`'s own init block) and set ONLY in the 4 + genuine build-success branches (PyInstaller producing `dist\%ENVNAME%.exe` cleanly, or Tier + A/Nuitka succeeding after PyInstaller didn't, across all 3 of its own failure sub-branches) -- + never on any `:warn_build_incomplete` path. `HP_FRESH_BUILD_OK` being set already implies the + fast path was not taken (a build can only be attempted after the fast path declines to fire), + so this is a strictly more precise REPLACEMENT for the old `HP_FASTPATH_USED` check, not an + additional condition alongside it -- `:success` now gates on `if defined HP_FRESH_BUILD_OK` + alone. A later repair-loop rebuild (`:hidden_import_recover`/`:dll_bundle_recover`) failing + after the initial build already succeeded does NOT unset the flag -- reasoned as correct, + since those loops fix BUNDLING issues, not SOURCE CONTENT, so the already-built EXE from the + same run still genuinely reflects the current sources even if a later repair attempt fails; + `:write_fast_hash`'s own pre-existing `if not exist "dist\%ENVNAME%.exe" exit /b 0` guard + remains the correct backstop for the rarer case where a repair-loop failure actually removes + the file. **Coverage gap closed**: `tests/selfapps_fastpath_hash.ps1` (uv lane, non-gating for first landing) is the literal scenario this item asked for -- run 1 builds the EXE printing a V1 - token; entry.py is rewritten to print V2 and its mtime backdated to 2001-09-09 (well before - the EXE's own mtime) before run 2; asserts run 2's captured EXE stdout shows V2 (not V1) and - the "Fast path: reusing" line is absent, proving a genuine rebuild happened rather than the - old mtime-only check wrongly reusing the stale EXE. `tests/test_fast_check.py` unit-tests the + token, and asserts `~fast_check.hash.txt` was genuinely written before proceeding (a + CodeRabbit-review addition: without this, a completely broken write side would be + indistinguishable from a working one, since a missing hash file also safely forces a rebuild); + entry.py is rewritten to print V2 and its mtime backdated to 2001-09-09 (well before the EXE's + own mtime) before run 2; asserts run 2's captured EXE stdout shows V2 (not V1) and the "Fast + path: reusing" line is absent, proving a genuine rebuild happened rather than the old + mtime-only check wrongly reusing the stale EXE. `tests/test_fast_check.py` unit-tests the isolated script directly via real `pwsh` (6 scenarios: no stored hash, write-then-fresh, backdated-mtime content change, dependency-file-only change, rewrite-after-change, missing EXE) plus a `PayloadSync` byte-equality check against `tools/fast_check.ps1`. `tests/harness.ps1`'s `batch.fastpath.hash_write` statically guards the `:write_fast_hash` - wiring (label exists, called from `:success` with the `HP_FASTPATH_USED` gate, invokes the - payload in `write` mode). See `docs/agent-ndjson.md` for the full row registry entry. + wiring -- label exists, called from `:success` with the `HP_FRESH_BUILD_OK` gate, invokes the + payload in `write` mode -- via SCOPED body extraction (bounded by each subroutine's own next + label, another CodeRabbit-review fix: the original check used whole-file `-match`, which could + pass even if the call or the write invocation were removed, as long as matching text happened + to exist anywhere else in the file). See `docs/agent-ndjson.md` for the full row registry entry. - **Item 42: console output is verbose across all log levels, and every fresh build ends with two unexplained Y/N prompts -- both plausibly overwhelming for the actual target audience diff --git a/docs/agent-interconnect.md b/docs/agent-interconnect.md index 09ffc82b..159b3949 100644 --- a/docs/agent-interconnect.md +++ b/docs/agent-interconnect.md @@ -1191,16 +1191,41 @@ composite SHA256 over the same non-infra `*.py` files already scanned, extended `requirements.txt`/`pyproject.toml`/`runtime.txt` when present. **The write side is a SEPARATE subroutine, `:write_fast_hash`, called from `:success`** (not inlined there -- a set-then-read of the same var inside one parenthesized block would hit the classic parse-time-`%VAR%`-expansion -trap, see `docs/agent-lessons-learned.md`), gated on `if not defined HP_FASTPATH_USED` so the -already-fast reuse case never pays a redundant second hash pass -- `:try_fast_exe`'s own `check` -call already computed the same hash once this run; if it matched (fast path taken), the stored -value is already correct and rewriting it would be pure waste. **Any future change to either the -scanned file set or the hash algorithm must update BOTH `:try_fast_exe`'s check invocation and -`:write_fast_hash`'s write invocation identically** -- they share the single `tools/fast_check.ps1` -script (mode-dispatched via a second positional arg), so a change to the shared script's file -enumeration or hashing logic automatically stays in sync between the two call sites; only a -change to the CALL SITES themselves (e.g. adding a new mode, or passing different exe/cwd context) -risks the two drifting apart. A missing stored hash (first run after upgrading to this fix, or an +trap, see `docs/agent-lessons-learned.md`), gated on `if defined HP_FRESH_BUILD_OK`. + +**`HP_FRESH_BUILD_OK`, not `HP_FASTPATH_USED`, is the correct gate -- a real bug CodeRabbit's +review caught before this shipped.** The first-shipped gate was `if not defined HP_FASTPATH_USED`, +reasoning "skip the redundant re-hash when the fast path already proved nothing changed." But +`HP_FASTPATH_USED` being unset only proves the fast path was NOT taken -- it does NOT prove a +build SUCCEEDED. A skipped or genuinely FAILED rebuild also leaves it unset, and if a stale +`dist\.exe` from an EARLIER successful run was still sitting there untouched, that gate would +write the CURRENT (changed) sources' hash paired against the OLD binary -- the next run would then +wrongly trust the stale EXE as "fresh," reintroducing Item 39's own bug one layer down. Fixed with +`HP_FRESH_BUILD_OK`: reset once per fresh build attempt in `:run_entry_smoke`'s own init block +(alongside `HP_NUITKA_FALLBACK_USED`), set ONLY in the 4 genuine build-success branches inside that +subroutine's PyInstaller/Tier-A build block (PyInstaller producing `dist\%ENVNAME%.exe` cleanly, or +Tier A/Nuitka succeeding after PyInstaller didn't, across all 3 of ITS OWN failure sub-branches) -- +never on any `:warn_build_incomplete` path. Since a build can only be ATTEMPTED after the fast path +has already declined to fire, `HP_FRESH_BUILD_OK` being set strictly implies `HP_FASTPATH_USED` was +never set either -- so this is a precision REPLACEMENT for the old gate, not an additional +condition layered on top of it; `:success` now checks `HP_FRESH_BUILD_OK` alone. + +**A later repair-loop rebuild failing does NOT need to unset the flag, by design.** +`:hidden_import_recover`/`:dll_bundle_recover` can each rebuild `dist\.exe` again AFTER the +initial build already succeeded and set `HP_FRESH_BUILD_OK=1` -- but these loops repair BUNDLING +issues (a missing DLL, a missing hidden import), not SOURCE CONTENT, so even if a later repair +attempt fails, the EXE from the initial successful build in the SAME run still genuinely reflects +the current source snapshot the hash is computed from. `:write_fast_hash`'s own pre-existing +`if not exist "dist\%ENVNAME%.exe" exit /b 0` guard remains the correct backstop for the rarer case +where a repair-loop failure actually removes the file rather than merely leaving it imperfect. + +**Any future change to either the scanned file set or the hash algorithm must update BOTH +`:try_fast_exe`'s check invocation and `:write_fast_hash`'s write invocation identically** -- they +share the single `tools/fast_check.ps1` script (mode-dispatched via a second positional arg), so a +change to the shared script's file enumeration or hashing logic automatically stays in sync between +the two call sites; only a change to the CALL SITES themselves (e.g. adding a new mode, or passing +different exe/cwd context) risks the two drifting apart. A missing stored hash (first run after +upgrading to this fix, or an EXE built by an older `run_setup.bat` with no hash file at all) is a safe "not fresh" default -- forces exactly one rebuild, never a false "fresh." diff --git a/run_setup.bat b/run_setup.bat index d3c0682e..884bc404 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -2186,10 +2186,16 @@ if not defined HP_FASTPATH_USED ( rem other console output. call :print_fastpath_ambiguous_note ) -rem CLAUDE.md Active Backlog Item 39: only after a genuine fresh build attempt this run -rem (HP_FASTPATH_USED unset) -- the fast-reuse case already means the stored hash matches -rem current sources, so writing again here would be a pure, wasted redundant re-hash. -if not defined HP_FASTPATH_USED call :write_fast_hash +rem CLAUDE.md Active Backlog Item 39: gated on HP_FRESH_BUILD_OK, not merely "HP_FASTPATH_USED +rem unset" (CodeRabbit review, PR #460, caught a real bug in the original gate: a skipped or +rem failed rebuild also leaves HP_FASTPATH_USED unset, but a stale dist\%ENVNAME%.exe left over +rem from an earlier successful run would still be sitting there -- writing the CURRENT sources' +rem hash against that OLD binary would make the next run wrongly trust it as "fresh"). Set ONLY +rem in :run_entry_smoke's own genuine build-success branches -- see the reset/set sites there. +rem HP_FRESH_BUILD_OK being set already implies the fast path was not taken (a build can only be +rem attempted after the fast path declined to fire), so this alone is a strictly more precise +rem replacement for the old HP_FASTPATH_USED check, not an additional condition alongside it. +if defined HP_FRESH_BUILD_OK call :write_fast_hash call :release_lock rem REQ-016: retain terminal window on success so user can read the output. if not defined HP_CI_LANE ( @@ -3577,12 +3583,14 @@ if not "%HP_SMOKE_RC%"=="0" if not defined HP_PROBE_EXCEEDED ( ) exit /b 0 :write_fast_hash -rem derived requirement (CLAUDE.md Active Backlog Item 39): after a genuine fresh build -rem attempt (never after a fast-path reuse -- see the :success call site's own -rem HP_FASTPATH_USED gate, which means sources already match the stored hash and a -rem rewrite would be a redundant, wasted re-hash pass), (re)write the composite -rem source-content hash the EXE fast path's freshness check compares against on the next -rem run. A dedicated subroutine, not inlined at :success, specifically so no line here +rem derived requirement (CLAUDE.md Active Backlog Item 39): called only when the caller's own +rem HP_FRESH_BUILD_OK gate confirms a genuine build succeeded THIS run (see :run_entry_smoke's +rem reset + 4 success-branch sets) -- never for a fast-path reuse (nothing changed, stored hash +rem already correct) and never for a skipped/failed rebuild (would pair the CURRENT sources' +rem hash with a stale leftover EXE from an earlier run -- a real bug CodeRabbit's review caught +rem on PR #460 in an earlier, less precise HP_FASTPATH_USED-only gate). (Re)writes the +rem composite source-content hash the EXE fast path's freshness check compares against on the +rem next run. A dedicated subroutine, not inlined at :success, specifically so no line here rem sits inside a parenthesized block -- see docs/agent-lessons-learned.md's "Parse-time rem vs. runtime variable expansion" entry for why a set-then-read-in-the-same-block rem pattern is unsafe, and this repo's own house preference for goto/call-based dispatch @@ -3752,6 +3760,17 @@ if not defined HP_BUILD_OK ( rem before this check -- warnfix can still fire and even rebuild over the Nuitka-built EXE, see rem the HP_NUITKA_FALLBACK_USED clear a few dozen lines below for how that case is handled. set "HP_NUITKA_FALLBACK_USED=" + rem CLAUDE.md Active Backlog Item 39 (CodeRabbit review, PR #460): a real correctness bug -- + rem gating :write_fast_hash on "HP_FASTPATH_USED unset" alone does not prove a build actually + rem SUCCEEDED this run; a stale dist\%ENVNAME%.exe left over from an EARLIER successful run + rem would still be sitting there if THIS run's rebuild is skipped/fails, and the freshness + rem hash would get (re)written for the CURRENT (changed) sources paired with that OLD binary + rem -- the next run would then wrongly trust the stale EXE as "fresh." HP_FRESH_BUILD_OK is + rem set ONLY in the genuine-success branches below -- PyInstaller producing + rem dist\%ENVNAME%.exe, or Tier A/Nuitka succeeding after it didn't -- never on a + rem warn_build_incomplete path. + rem Reset once per fresh build attempt, same reasoning as HP_NUITKA_FALLBACK_USED above. + set "HP_FRESH_BUILD_OK=" rem REQ-009/REQ-005.10 (cascade-vs-postexec fix): reset the "this provider's dependencies rem look incomplete" flag at the start of every fresh build attempt, not just when warnfix rem happens to run again -- a provider that needs no warnfix repair at all must not inherit @@ -3801,6 +3820,7 @@ if not defined HP_BUILD_OK ( call :warn_build_incomplete "[WARN] PyInstaller execution failed; will verify your code directly via Python instead. reason=test_forced_fail" ) else ( set "HP_NUITKA_FALLBACK_USED=1" + set "HP_FRESH_BUILD_OK=1" ) ) else ( "%HP_PY%" -m PyInstaller -y --onefile --clean --log-level WARN %HP_PYI_EXPAT% %HP_PYI_COLLECT% --name "%ENVNAME%" "%HP_ENTRY%" >> "%LOG%" 2>&1 @@ -3816,6 +3836,7 @@ if not defined HP_BUILD_OK ( call :warn_build_incomplete "[WARN] PyInstaller execution failed; will verify your code directly via Python instead. reason=build_error" ) else ( set "HP_NUITKA_FALLBACK_USED=1" + set "HP_FRESH_BUILD_OK=1" ) ) else ( if defined HP_TEST_FORCE_OUTPUT_VANISH if exist "dist\%ENVNAME%.exe" ( @@ -3830,9 +3851,11 @@ if not defined HP_BUILD_OK ( call :warn_build_incomplete "[WARN] PyInstaller did not produce dist\%ENVNAME%.exe; will verify your code directly via Python instead. reason=missing_output" ) else ( set "HP_NUITKA_FALLBACK_USED=1" + set "HP_FRESH_BUILD_OK=1" ) ) else ( call :log "[INFO] PyInstaller produced dist\%ENVNAME%.exe" + set "HP_FRESH_BUILD_OK=1" ) ) ) diff --git a/tests/harness.ps1 b/tests/harness.ps1 index 23eb468c..0d9e149d 100644 --- a/tests/harness.ps1 +++ b/tests/harness.ps1 @@ -407,16 +407,27 @@ $depInstallProgress = ($depMsgPos -ge 0) -and ($depCallPos -ge 0) -and ($depMsgP Write-Result 'batch.progress.dep_install' "Progress message before dependency install phase: user sees 'Installing dependencies' before the slow pip/conda/uv install step" $depInstallProgress @{ msgIdx = $depMsgPos; opIdx = $depCallPos; ordered = $depInstallProgress } # derived requirement (CLAUDE.md Active Backlog Item 39): the EXE fast path's freshness # check switched from mtime-only to a content-hash comparison; the hash must be (re)written -# after a genuine fresh build (:success, gated on HP_FASTPATH_USED being UNSET so the -# already-fast reuse case never pays a redundant re-hash pass) via a dedicated subroutine -# (not inlined, to avoid the parenthesized-block set-then-read hazard). Static wiring guard -# only -- runtime proof is tests/test_fast_check.py (the embedded script itself, via real -# pwsh) plus whatever selfapps coverage exercises :try_fast_exe end-to-end. -$hasWriteFastHashLabel = $AllText -match '(?m)^:write_fast_hash\s*$' -$hasWriteFastHashCall = $AllText -match 'if not defined HP_FASTPATH_USED call :write_fast_hash' -$hasWriteModeArg = $AllText -match '"%HP_FAST_CHECK_PS%"\s+"dist\\%ENVNAME%\.exe"\s+write' +# after a genuine fresh build (:success, gated on HP_FRESH_BUILD_OK -- set ONLY in +# :run_entry_smoke's own genuine build-success branches, never on a skipped/failed rebuild +# or a fast-path reuse -- CodeRabbit review, PR #460, caught the original HP_FASTPATH_USED +# -only gate wrongly allowing a stale leftover EXE's hash to be overwritten) via a dedicated +# subroutine (not inlined, to avoid the parenthesized-block set-then-read hazard). Bodies are +# extracted (bounded by the next label) rather than matched against the whole file, so this +# check cannot pass on unrelated text elsewhere -- mirrors the :hidden_import_recover / +# :dll_bundle_recover scoped-extraction precedent above (see $hiLogSizeAdvance). Static +# wiring guard only -- runtime proof is tests/test_fast_check.py (the embedded script +# itself, via real pwsh) plus tests/selfapps_fastpath_hash.ps1's end-to-end coverage. +$successMatch = [regex]::Match($AllText, '(?ms)^:success\r?\n(?.*?)(?=^:provider_cascade)') +$hasWriteFastHashCall = $successMatch.Success -and ( + $successMatch.Groups['body'].Value -match [regex]::Escape('if defined HP_FRESH_BUILD_OK call :write_fast_hash') +) +$writeFastHashMatch = [regex]::Match($AllText, '(?ms)^:write_fast_hash\r?\n(?.*?)(?=^:run_entry_smoke)') +$hasWriteFastHashLabel = $writeFastHashMatch.Success +$hasWriteModeArg = $writeFastHashMatch.Success -and ( + $writeFastHashMatch.Groups['body'].Value -match '"%HP_FAST_CHECK_PS%"\s+"dist\\%ENVNAME%\.exe"\s+write' +) $hasFastHashWiring = $hasWriteFastHashLabel -and $hasWriteFastHashCall -and $hasWriteModeArg -Write-Result 'batch.fastpath.hash_write' 'EXE fast path freshness check: :write_fast_hash subroutine exists, is called from :success gated on HP_FASTPATH_USED, and invokes the payload in write mode' $hasFastHashWiring @{ hasLabel = $hasWriteFastHashLabel; hasCall = $hasWriteFastHashCall; hasWriteModeArg = $hasWriteModeArg } +Write-Result 'batch.fastpath.hash_write' 'EXE fast path freshness check: :write_fast_hash subroutine exists, is called from :success gated on HP_FRESH_BUILD_OK, and invokes the payload in write mode' $hasFastHashWiring @{ hasLabel = $hasWriteFastHashLabel; hasCall = $hasWriteFastHashCall; hasWriteModeArg = $hasWriteModeArg } # derived requirement: pre-build --collect-submodules double-gate (REQ-005.x) must stay wired. # Static guard against silent deletion; runtime proof is self.collect.submodules (selfapps_collect.ps1). $collectCall = $AllText -match 'call :compute_collect_flags' diff --git a/tests/selfapps_fastpath_hash.ps1 b/tests/selfapps_fastpath_hash.ps1 index 636026f7..9ade2809 100644 --- a/tests/selfapps_fastpath_hash.ps1 +++ b/tests/selfapps_fastpath_hash.ps1 @@ -95,6 +95,14 @@ try { # Run 1: fresh build, entry.py prints V1. $run1Exit = Invoke-Bootstrap '~fastpath_hash_run1.log' + # derived requirement (CodeRabbit review, PR #460): without this, a completely broken + # :write_fast_hash (e.g. HP_FRESH_BUILD_OK never getting set) would be indistinguishable + # from a working one -- a MISSING hash file also forces run 2 to rebuild (the safe + # default), so every assertion below would still pass even if the write side never fired + # at all. Assert the write side was genuinely exercised, not just coincidentally masked. + $hashFileAfterRun1 = Join-Path $workDir '~fast_check.hash.txt' + $hashWrittenAfterRun1 = Test-Path -LiteralPath $hashFileAfterRun1 + # Rewrite entry.py to print V2, then backdate its mtime well before dist\.exe's own # mtime -- simulating a ZIP/xcopy/robocopy delivery that preserves the original # authored timestamp instead of stamping "now" on extraction. @@ -129,9 +137,10 @@ $tokenV2Seen = $runOutText -match 'FASTPATH_TOKEN_V2' $tokenV1Seen = $runOutText -match 'FASTPATH_TOKEN_V1' $fastPathReusedRun2 = $run2Combined -match [regex]::Escape('Fast path: reusing') -# Genuine rebuild detected: run 2 shows the NEW token, not the old one, and did not take -# the "reusing" fast path. -$pass = ($run1Exit -eq 0) -and ($run2Exit -eq 0) -and $tokenV2Seen -and (-not $tokenV1Seen) -and (-not $fastPathReusedRun2) +# Genuine rebuild detected: run 2 shows the NEW token, not the old one, did not take the +# "reusing" fast path, AND run 1 genuinely exercised the write side (not just coincidentally +# masked by the missing-hash-file safe default). +$pass = ($run1Exit -eq 0) -and ($run2Exit -eq 0) -and $hashWrittenAfterRun1 -and $tokenV2Seen -and (-not $tokenV1Seen) -and (-not $fastPathReusedRun2) Write-NdjsonRow ([ordered]@{ id = 'self.fastpath.hash.backdated_mtime' @@ -141,6 +150,7 @@ Write-NdjsonRow ([ordered]@{ details = [ordered]@{ run1Exit = $run1Exit run2Exit = $run2Exit + hashWrittenAfterRun1 = $hashWrittenAfterRun1 tokenV2Seen = $tokenV2Seen tokenV1Seen = $tokenV1Seen fastPathReusedRun2 = $fastPathReusedRun2 From df2d7660d8f38b79ad60f79d66c4efef5971c6f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 14:33:36 +0000 Subject: [PATCH 3/3] Fix Item 39 hash-freshness robustness gaps from CodeRabbit review round 2 Four fixes from a second CodeRabbit review round on PR #460 (all against commit 8eaa271, the HP_FRESH_BUILD_OK fix): defensively reset HP_FRESH_BUILD_OK at the very top of the file (before the first :try_fast_exe call), not just inside :run_entry_smoke, so a provider-cascade re-entry hitting the preflight-failure early-return can never leave a stale value behind; :write_fast_hash now captures and logs a nonzero hash-write exit code instead of silently swallowing it; selfapps_fastpath_hash.ps1 now validates the persisted hash's actual content (not just existence) and asserts the backdated-mtime precondition the whole scenario depends on genuinely held; and the new ~fast_check.hash.txt artifact is now included in the CI log upload step. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017SQ1rvJxDbE71pTXJ4QvLV --- .github/workflows/batch-check.yml | 2 ++ CLAUDE.md | 29 ++++++++++++++++++++++ run_setup.bat | 13 ++++++++++ tests/selfapps_fastpath_hash.ps1 | 40 +++++++++++++++++++++++-------- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index 2bdd020b..5a31b6ba 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -1916,6 +1916,8 @@ jobs: tests\~selftest_fastpath_hash\~setup.log tests/~selftest_fastpath_hash/~run.out.txt tests\~selftest_fastpath_hash\~run.out.txt + tests/~selftest_fastpath_hash/~fast_check.hash.txt + tests\~selftest_fastpath_hash\~fast_check.hash.txt tests/~pandas_excel/~pandas_excel.log tests\~pandas_excel\~pandas_excel.log tests/~pipgap/~pipgap.log diff --git a/CLAUDE.md b/CLAUDE.md index 630ac6a5..0f80a1a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -926,6 +926,35 @@ but several represent real gaps worth closing before calling the path fully rele pass even if the call or the write invocation were removed, as long as matching text happened to exist anywhere else in the file). See `docs/agent-ndjson.md` for the full row registry entry. + **A second CodeRabbit review round on the same PR caught four more findings, all fixed before + landing.** (1) A defensive `set "HP_FRESH_BUILD_OK="` was added at the very top of the file + (alongside the pre-existing `HP_FASTPATH_USED=` reset, before the first `:try_fast_exe` call of + the whole run) -- `:run_entry_smoke`'s own per-build-attempt reset sits past a preflight-failure + early-return (`HP_PREFLIGHT_FAILED`), so a provider-cascade re-entry hitting that early return + would skip the in-subroutine reset; no concrete exploit was confirmed against the current + single-invocation call graph (every write site pairs a genuinely fresh build with unchanged + sources even across this path), but the fix is a zero-cost, unconditional close of the whole + class of risk regardless. (2) `:write_fast_hash` now captures the PowerShell write call's own + exit code and logs `[WARN] Fast-path hash write failed; the next run will rebuild.` on a nonzero + result, instead of silently deleting the helper and returning success either way -- matches this + repo's own "Bootstrap must fail fast and explicitly -- no silent fallbacks unless explicitly + logged" principle; still safe either way (a failed write leaves no stored hash, forcing exactly + one more rebuild), just no longer silent. (3) `tests/selfapps_fastpath_hash.ps1`'s + `hashWrittenAfterRun1` check was `Test-Path`-only (existence, not content) -- a truncated or + malformed write would pass the same way a working one would, since a broken file is just as + "not fresh" as a missing one to the read side. Now reads the file and validates it matches + `tools/fast_check.ps1`'s own canonical format (a 64-char lowercase hex SHA256 digest, no + trailing newline) via `hashContentValid`, folded into `$pass`. (4) The same test never confirmed + its own core precondition -- that `entry.py`'s backdated mtime assignment actually took, and is + genuinely older than the built EXE's own mtime -- before trusting run 2's outcome as evidence the + NEW hash-based path (not the OLD mtime-only path, coincidentally rebuilding for an unrelated + reason) did the detecting. Added `mtimePreconditionHolds` (compares `entry.py`'s and the built + EXE's `LastWriteTimeUtc`), also folded into `$pass`. `.github/workflows/batch-check.yml`'s + "Upload test logs" step gained both path-style variants for `~fast_check.hash.txt` under + `tests/~selftest_fastpath_hash/` (a new observable artifact this PR introduced, per this repo's + own coding guideline that every new artifact needs both slash-style upload paths) -- previously + only the run logs/`~setup.log`/`~run.out.txt` were wired. + - **Item 42: console output is verbose across all log levels, and every fresh build ends with two unexplained Y/N prompts -- both plausibly overwhelming for the actual target audience (beginners with no setup experience).** Confirmed reasoned-from-source and CI capture. New; not diff --git a/run_setup.bat b/run_setup.bat index 884bc404..948b3966 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -652,6 +652,13 @@ call :compute_interactive_run rem --- Very top EXE fast path: reuse dist\%ENVNAME%.exe when sources are unchanged --- set "HP_FASTPATH_USED=" +rem CLAUDE.md Active Backlog Item 39 (CodeRabbit review, PR #460): defensive reset. Every real +rem write site for HP_FRESH_BUILD_OK lives inside :run_entry_smoke, whose own per-build-attempt +rem reset sits past a preflight-failure early-return (HP_PREFLIGHT_FAILED) -- a provider-cascade +rem re-entry that hits that early return would skip the in-subroutine reset. Clearing it here too, +rem before the very first :try_fast_exe call of the whole run, closes that gap unconditionally so +rem :write_fast_hash below can never fire off a value this run never genuinely earned. +set "HP_FRESH_BUILD_OK=" rem REQ-016: start clean so an inherited env var can never trigger a false "EXE rem unverified" caveat; :run_exe_smokerun sets this only on a real non-zero EXE exit. set "HP_EXE_VERIFY_FAILED=" @@ -3601,7 +3608,13 @@ if exist "%HP_FAST_CHECK_PS%" del "%HP_FAST_CHECK_PS%" >nul 2>&1 call :emit_from_base64 "%HP_FAST_CHECK_PS%" HP_FAST_CHECK if errorlevel 1 exit /b 0 powershell -NoProfile -ExecutionPolicy Bypass -File "%HP_FAST_CHECK_PS%" "dist\%ENVNAME%.exe" write >> "%LOG%" 2>&1 +set "HP_FAST_HASH_RC=%ERRORLEVEL%" +rem derived requirement (CodeRabbit review, PR #460): a write failure here is not fatal -- the +rem next run's missing-hash default safely forces exactly one rebuild -- but a silent failure +rem could repeat unnoticed across many runs. Log it explicitly rather than swallowing it. +if not "%HP_FAST_HASH_RC%"=="0" call :log "[WARN] Fast-path hash write failed; the next run will rebuild." if exist "%HP_FAST_CHECK_PS%" del "%HP_FAST_CHECK_PS%" >nul 2>&1 +set "HP_FAST_HASH_RC=" exit /b 0 :run_entry_smoke call :record_chosen_entry "%HP_ENTRY%" diff --git a/tests/selfapps_fastpath_hash.ps1 b/tests/selfapps_fastpath_hash.ps1 index 9ade2809..70997858 100644 --- a/tests/selfapps_fastpath_hash.ps1 +++ b/tests/selfapps_fastpath_hash.ps1 @@ -100,8 +100,16 @@ try { # from a working one -- a MISSING hash file also forces run 2 to rebuild (the safe # default), so every assertion below would still pass even if the write side never fired # at all. Assert the write side was genuinely exercised, not just coincidentally masked. + # derived requirement (CodeRabbit review round 2): Test-Path alone only proves the file + # exists -- an empty or malformed write (e.g. a truncated/failed PowerShell -File call) + # would still make run 2 rebuild for an unrelated reason (missing-hash safe default), so + # a broken write side could hide behind that same safe default. Read the content and + # validate it matches tools/fast_check.ps1's own canonical format (a 64-char lowercase + # hex SHA256 digest, no trailing newline -- see its WriteAllText call). $hashFileAfterRun1 = Join-Path $workDir '~fast_check.hash.txt' $hashWrittenAfterRun1 = Test-Path -LiteralPath $hashFileAfterRun1 + $hashContentAfterRun1 = if ($hashWrittenAfterRun1) { Get-Content -LiteralPath $hashFileAfterRun1 -Raw -Encoding ASCII } else { '' } + $hashContentValid = $hashWrittenAfterRun1 -and ($hashContentAfterRun1 -match '^[0-9a-f]{64}$') # Rewrite entry.py to print V2, then backdate its mtime well before dist\.exe's own # mtime -- simulating a ZIP/xcopy/robocopy delivery that preserves the original @@ -114,6 +122,15 @@ print("FASTPATH_TOKEN_V2") (Get-Item -LiteralPath $entryPath).LastWriteTime = $backdated (Get-Item -LiteralPath $entryPath).LastWriteTimeUtc = $backdated.ToUniversalTime() + # derived requirement (CodeRabbit review round 2): if Windows silently rejected or + # normalized the backdated timestamp assignment above, the OLD mtime-only check would + # still (coincidentally) trigger a rebuild for the wrong reason, and this scenario would + # pass without ever having genuinely exercised the new content-hash path. Confirm the + # precondition actually holds -- entry.py's mtime is genuinely older than the EXE dist + # produced in run 1 -- before trusting run 2's outcome as evidence of hash-based detection. + $exeCandidates = @(Get-ChildItem -Path (Join-Path $workDir 'dist') -Filter '*.exe' -ErrorAction SilentlyContinue) + $mtimePreconditionHolds = ($exeCandidates.Count -gt 0) -and ((Get-Item -LiteralPath $entryPath).LastWriteTimeUtc -lt $exeCandidates[0].LastWriteTimeUtc) + # Run 2: sources genuinely changed (V2), but entry.py's mtime lies about it. $run2Exit = Invoke-Bootstrap '~fastpath_hash_run2.log' } finally { @@ -138,9 +155,10 @@ $tokenV1Seen = $runOutText -match 'FASTPATH_TOKEN_V1' $fastPathReusedRun2 = $run2Combined -match [regex]::Escape('Fast path: reusing') # Genuine rebuild detected: run 2 shows the NEW token, not the old one, did not take the -# "reusing" fast path, AND run 1 genuinely exercised the write side (not just coincidentally -# masked by the missing-hash-file safe default). -$pass = ($run1Exit -eq 0) -and ($run2Exit -eq 0) -and $hashWrittenAfterRun1 -and $tokenV2Seen -and (-not $tokenV1Seen) -and (-not $fastPathReusedRun2) +# "reusing" fast path, run 1 genuinely wrote a well-formed hash (not just coincidentally +# masked by the missing-hash-file safe default), and the backdated-mtime precondition this +# whole scenario depends on actually held (entry.py genuinely predates the EXE). +$pass = ($run1Exit -eq 0) -and ($run2Exit -eq 0) -and $hashContentValid -and $mtimePreconditionHolds -and $tokenV2Seen -and (-not $tokenV1Seen) -and (-not $fastPathReusedRun2) Write-NdjsonRow ([ordered]@{ id = 'self.fastpath.hash.backdated_mtime' @@ -148,13 +166,15 @@ Write-NdjsonRow ([ordered]@{ pass = $pass desc = 'EXE fast path content-hash freshness detects a genuine change even when the changed file mtime is backdated below the EXE' details = [ordered]@{ - run1Exit = $run1Exit - run2Exit = $run2Exit - hashWrittenAfterRun1 = $hashWrittenAfterRun1 - tokenV2Seen = $tokenV2Seen - tokenV1Seen = $tokenV1Seen - fastPathReusedRun2 = $fastPathReusedRun2 - run2Log = '~fastpath_hash_run2.log' + run1Exit = $run1Exit + run2Exit = $run2Exit + hashWrittenAfterRun1 = $hashWrittenAfterRun1 + hashContentValid = $hashContentValid + mtimePreconditionHolds = $mtimePreconditionHolds + tokenV2Seen = $tokenV2Seen + tokenV1Seen = $tokenV1Seen + fastPathReusedRun2 = $fastPathReusedRun2 + run2Log = '~fastpath_hash_run2.log' } })