Instruction file imported from tvofi/heatpump_optimizer (
.cursor/rules/gate-scoping.mdc). Copyright stays with the author.
Running the gate
./tests/run.sh is unscoped and long. On a branch, what you almost always want
is the scoped gate against your merge base:
GATE_SCOPE=auto GOLDEN_MODE=drift GOLDEN_REF=$(git merge-base origin/main HEAD) ./tests/run.sh
/tmp/hpo-gate.lock serialises anything that runs tests/stress.py, which
measures the machine while it solves and is wrong if something else is running.
run.sh takes it, waiting, for a FULL or stress.py run. Hold it across
commands with tests/gate_lock.py — not mkdir and a shell pid:
python3 tests/gate_lock.py take --label <your-label>
HPO_GATE_LOCK_LABEL=<your-label> GATE_SCOPE=auto GOLDEN_MODE=drift \
GOLDEN_REF=$(git merge-base origin/main HEAD) ./tests/run.sh
python3 tests/gate_lock.py renew --label <your-label> # between commands
python3 tests/gate_lock.py release --label <your-label>
The owner file carries your label and an expires_at lease (30 minutes, above
the longest observed gate). Every script under lock renews it; an expired lease
or an abandoned hold (holding marker, no live flock) may be taken without
forensics. run.sh holds flock for the gate run so a crash drops flock and
a waiter can take immediately — the lease covers the window between commands
when nothing holds flock (#404). Setting HPO_GATE_LOCK_LABEL without the
take above fails at once: the label means I hold it already.
The mypy census runs here too, when you have the pins. The typing lane is
source-only, so #1091 and #1099 each pushed a census regression a local gate
could have caught. HPO_TYPING_PYTHON points the ruler at an interpreter
carrying them; unset it prints the install command and what goes unchecked, and
an unpinned one fails rather than measuring (#1095).
A gate run regenerates the D6 claims artifacts under tools/audit/round4/D6/
against the live VERSION. git checkout -- tools/audit/round4/D6 after it,
before any git add -A sweeps them into your commit (#1054).
Never run a full tests/derive_closures.sh off Linux
The union that lets a Darwin recording grow a node closure without dropping
files only Linux strace saw lives inside closure.py's if partial: branch,
and --single is what passes --partial. The full path does not, so a full
re-derivation on this box replaces the Linux recordings wholesale:
card_drift.mjs measured 66 scripts → 6. Use --single on the one script.
Node lanes (tests/card.mjs, tests/card_drift.mjs) record on Darwin via
node --import tests/node_fs_trace.mjs (Node fs / loader, not strace).
What --single may repair against a CI UNDER-SCOPED is ci-autofix.md's.
--single cannot re-record a script whose own checks pin the classification
being re-recorded: the recording exits 1 until tests/closures.json already
lists the files. closure.py merge's refusal names the two-step route out, and
closure.py selftest pins both refusals against the wrong command (#1071).