Two things, both surfaced by finally being able to run the tree. ## A pre-existing silent red tests/test_lane_sha_verifier.py::TestExpectedLaneCoverage was RED on clean main — confirmed by running it in a pristine worktree at origin/main, and the file is untouched by any of this branch's work. deduction_serve_v1 (ADR-0256) and curriculum_serve_v1 (ADR-0262) shipped during the generalization arc and neither was added to EXPECTED_LANES. The roster's `extra` assertion is a deliberate tripwire: a new lane is supposed to fail it once so an author acknowledges the addition. Nobody did, and nothing caught it, because that file is in neither `smoke` nor `deductive` — no local gate and no CI job runs it. Same silent-red family as the exact-tuple pin S5 found in passing, and as the register-axis e2e tests that sat red for two days. This commit is the acknowledgement the tripwire was asking for. That it took running the full tree to find is the argument for the rest of this commit. ## scripts/ci/local-ci.sh AGENTS.md says the merge bar IS the local run. There was no single entry point for it, and on any host without Python 3.12.13 exactly there was no local run at all: pyproject pins requires-python == "3.12.13", so uv sync --locked fails outright and every gate becomes unrunnable. sh scripts/ci/local-ci.sh --tier smoke|gate|full `gate` is the three pre-push steps; `full` is the whole tree in parallel. Suite membership is read from core/cli_test.py::TEST_SUITES through the CLI and never restated, so this runner cannot drift from the hook the way smoke.yml drifted from the local list. Interpreter contract, deliberately fail-closed: the pinned interpreter is CANONICAL, and the runner refuses on anything else with instructions. --allow-interpreter-fallback opts into any 3.12.x and stamps every run NON-CANONICAL. Same discipline as ratified_ledger's missing_ok and the MLX skip path: a degraded mode is legitimate, a degraded mode reporting itself as the real thing is not. Evidence recorded in the script header so the flag is not cargo-culted: the committed-SHA / trace_hash / content_sha256 / lane-SHA pin set (119 tests) was run on 3.12.11 and 118 passed, the one failure being the stale roster above, which fails on 3.12.13 too. That is evidence the exact pin is not load-bearing for bit-exactness — it is NOT a ruling that it should be relaxed. Relaxing it is a reproducibility decision and belongs to a human. ## Observed, not fixed Something in the suite writes a real proposal artifact into teaching/proposals/derived_close_facts/ instead of a tmp_path. The three test files that name that sink all override it correctly, so the leak is elsewhere — likely an idle_tick/contemplation path exercising DEFAULT_SINK. Untracked residue, removed here, flagged for follow-up. Notable because it is only visible now that the parents[3] bug is fixed and the sink resolves inside the repo. [Verification]: gate tier via the new runner — smoke 236, warmed_session 10, deductive 285, all passed, correctly stamped NON-CANONICAL. Fail-closed path verified (exit 3 with instructions). lane_sha_verifier 6 passed. Full tests/ tree in progress at 63% with 0 failures at time of commit. NON-CANONICAL: Python 3.12.11, not the pinned 3.12.13. Not merge evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FduW6Krm3PPQv3P5iwBYtx
185 lines
6.8 KiB
Bash
Executable file
185 lines
6.8 KiB
Bash
Executable file
#!/bin/sh
|
|
# CORE local-first CI runner.
|
|
#
|
|
# AGENTS.md: "The real CI is local-first. Run validation in the worktree...
|
|
# Do not treat remote Actions / Docker job containers as the merge gate."
|
|
# This script is that runner — one entry point for the gate tiers, usable on
|
|
# any host, with an honest statement of which interpreter produced the result.
|
|
#
|
|
# sh scripts/ci/local-ci.sh # gate tier (what pre-push runs)
|
|
# sh scripts/ci/local-ci.sh --tier smoke
|
|
# sh scripts/ci/local-ci.sh --tier full # the whole tests/ tree, parallel
|
|
# sh scripts/ci/local-ci.sh --list
|
|
#
|
|
# WHY THIS EXISTS
|
|
# ---------------
|
|
# `pyproject.toml` pins `requires-python == "3.12.13"` — an EXACT patch
|
|
# version. On a host that does not have precisely 3.12.13, `uv sync --locked`
|
|
# fails outright and every gate becomes unrunnable. For a project whose merge
|
|
# bar IS the local run, an unrunnable local gate is the failure mode that
|
|
# matters most: it is how `tests/test_lane_sha_verifier.py` sat red on clean
|
|
# main from the deduction-serve arc until 2026-07-25 with nobody noticing.
|
|
#
|
|
# THE INTERPRETER CONTRACT (the part to read before trusting a green run)
|
|
# ----------------------------------------------------------------------
|
|
# The pinned interpreter is CANONICAL. A run on any other interpreter is
|
|
# NON-CANONICAL and this script says so, loudly, on every such run.
|
|
#
|
|
# Fallback is opt-in, never automatic — the same discipline as
|
|
# `core.ratified_ledger`'s `missing_ok`: a degraded mode is legitimate, and a
|
|
# degraded mode that reports itself as the real thing is not. Default is
|
|
# fail-closed with instructions.
|
|
#
|
|
# --allow-interpreter-fallback run on any 3.12.x, stamped NON-CANONICAL
|
|
#
|
|
# Measured note, so the flag is not cargo-culted: on 2026-07-25 the full
|
|
# committed-SHA / trace_hash / content_sha256 / lane-SHA pin set (119 tests)
|
|
# was run on 3.12.11 and 118 passed — the single failure was a stale lane
|
|
# roster that fails on 3.12.13 too. That is evidence the exact pin is not
|
|
# load-bearing for bit-exactness, NOT a ruling that it should be relaxed.
|
|
# Relaxing it is a reproducibility decision and belongs to a human.
|
|
|
|
set -eu
|
|
|
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
cd "${repo_root}"
|
|
|
|
TIER="gate"
|
|
ALLOW_FALLBACK=0
|
|
PYTEST_EXTRA=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: local-ci.sh [--tier TIER] [--allow-interpreter-fallback] [--list] [-- ARGS]
|
|
|
|
tiers:
|
|
smoke smoke suite only (~1 min) = CI smoke.yml parity
|
|
gate smoke + warmed_session + deductive (~2 min) = the pre-push gate
|
|
full the entire tests/ tree, parallel (~10 min) = the merge bar
|
|
|
|
options:
|
|
--allow-interpreter-fallback permit a non-pinned 3.12.x; marks run NON-CANONICAL
|
|
--list print the tier definitions and exit
|
|
-- pass remaining args through to pytest
|
|
EOF
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--tier) TIER="$2"; shift 2 ;;
|
|
--tier=*) TIER="${1#*=}"; shift ;;
|
|
--allow-interpreter-fallback) ALLOW_FALLBACK=1; shift ;;
|
|
--list) usage; exit 0 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; PYTEST_EXTRA="$*"; break ;;
|
|
*) echo "[local-ci] unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
case "${TIER}" in
|
|
smoke|gate|full) ;;
|
|
*) echo "[local-ci] unknown tier: ${TIER} (smoke|gate|full)" >&2; exit 2 ;;
|
|
esac
|
|
|
|
# --- interpreter resolution ------------------------------------------------
|
|
|
|
pinned="$(cat .python-version 2>/dev/null || echo '')"
|
|
[ -n "${pinned}" ] || pinned="3.12.13"
|
|
canonical=0
|
|
RUNNER=""
|
|
|
|
if command -v uv >/dev/null 2>&1 && uv python find "${pinned}" >/dev/null 2>&1; then
|
|
canonical=1
|
|
RUNNER="uv run --"
|
|
echo "[local-ci] interpreter: ${pinned} (CANONICAL — matches the repo pin)"
|
|
else
|
|
if [ "${ALLOW_FALLBACK}" -eq 0 ]; then
|
|
cat >&2 <<EOF
|
|
[local-ci] BLOCKED: the pinned interpreter (${pinned}) is not available here.
|
|
|
|
\`pyproject.toml\` pins requires-python == "${pinned}", so \`uv sync --locked\`
|
|
cannot resolve and the canonical gate cannot run on this host.
|
|
|
|
Either provision it: uv python install ${pinned}
|
|
or run degraded, knowingly:
|
|
sh scripts/ci/local-ci.sh --tier ${TIER} \\
|
|
--allow-interpreter-fallback
|
|
|
|
A degraded run is legitimate. A degraded run reported as canonical is not,
|
|
which is why this is opt-in rather than automatic.
|
|
EOF
|
|
exit 3
|
|
fi
|
|
fallback=""
|
|
for cand in python3.12 python3; do
|
|
if command -v "${cand}" >/dev/null 2>&1 && \
|
|
"${cand}" -c 'import sys; sys.exit(0 if sys.version_info[:2]==(3,12) else 1)' 2>/dev/null; then
|
|
fallback="${cand}"; break
|
|
fi
|
|
done
|
|
if [ -z "${fallback}" ]; then
|
|
echo "[local-ci] BLOCKED: no 3.12.x interpreter found at all." >&2
|
|
exit 3
|
|
fi
|
|
actual="$(${fallback} -c 'import sys; print(".".join(map(str, sys.version_info[:3])))')"
|
|
RUNNER="${fallback} -m"
|
|
cat >&2 <<EOF
|
|
|
|
############################################################
|
|
# NON-CANONICAL RUN #
|
|
# interpreter ${actual} != pinned ${pinned}
|
|
# Results are indicative, NOT merge evidence. #
|
|
# Re-run on ${pinned} before merging.
|
|
############################################################
|
|
|
|
EOF
|
|
fi
|
|
|
|
# Two invocation shapes, one interface. Suite membership is NEVER restated
|
|
# here — it is read from ``core/cli_test.py::TEST_SUITES`` through the CLI, so
|
|
# this runner cannot drift from the pre-push gate the way smoke.yml drifted
|
|
# from it.
|
|
if [ "${canonical}" -eq 1 ]; then
|
|
suite() { uv run core test --suite "$1" -q; }
|
|
pytest_() { uv run python -m pytest "$@"; }
|
|
else
|
|
suite() { ${RUNNER} core.cli test --suite "$1" -q; }
|
|
pytest_() { ${RUNNER} pytest "$@"; }
|
|
fi
|
|
|
|
step() {
|
|
label="$1"; shift
|
|
echo "[local-ci] ${label} ..."
|
|
if ! "$@"; then
|
|
echo "[local-ci] FAILED: ${label}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# --- gates -----------------------------------------------------------------
|
|
|
|
start="$(date +%s)"
|
|
|
|
case "${TIER}" in
|
|
smoke)
|
|
step "smoke suite" suite smoke
|
|
;;
|
|
gate)
|
|
echo "[local-ci] tier=gate — same three steps as scripts/hooks/pre-push"
|
|
step "(1/3) smoke suite" suite smoke
|
|
step "(2/3) warmed_session lane" pytest_ tests/test_warmed_session_lane.py -q --no-header
|
|
step "(3/3) deductive suite" suite deductive
|
|
;;
|
|
full)
|
|
jobs="$(nproc 2>/dev/null || echo 4)"
|
|
step "full tests/ tree (-n ${jobs})" \
|
|
pytest_ tests/ -q --no-header -p no:cacheprovider -n "${jobs}" --dist loadfile
|
|
;;
|
|
esac
|
|
|
|
elapsed=$(( $(date +%s) - start ))
|
|
if [ "${canonical}" -eq 1 ]; then
|
|
echo "[local-ci] PASSED tier=${TIER} in ${elapsed}s — CANONICAL (${pinned})."
|
|
else
|
|
echo "[local-ci] PASSED tier=${TIER} in ${elapsed}s — NON-CANONICAL; not merge evidence." >&2
|
|
fi
|