diff --git a/CHANGELOG.md b/CHANGELOG.md index b7324bd1..5a15e58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version - **`Agent Handoff 1.17` stops an untrusted checkout from executing a command during session start.** The `session-start` launcher ran its Git reads with the full inherited process environment and no configuration isolation, so a repository-local or ancestor `core.fsmonitor` setting named a hook that Git ran — unconditionally, before the operator had seen anything — as soon as that checkout was opened as a session-start target ([#235](https://github.com/L3DigitalNet/project-standards/issues/235)). Every read now runs with an explicit minimal environment (`PATH` and `HOME` only, so no `GIT_DIR`, `GIT_WORK_TREE`, or `GIT_CONFIG_*` value from the harness can redirect it) and passes `-c core.fsmonitor=`, which outranks every configuration file, plus `--no-optional-locks` so the read cannot race a concurrent write. The injected session context is byte-identical to 1.16; reconcile replaces the installed hook because its digest moved. Catalog 5 promotes `agent-handoff@1.17` and retains 1.16. - **`Agent Handoff 1.17`'s launcher reports the payload version that ships it.** 1.15 and 1.16 carried forward the 1.14 binary byte for byte, so `session-start --version` answered `1.14` on both — the one question the stale-launcher diagnostic exists to answer, answered with a version the consumer never selected ([#229](https://github.com/L3DigitalNet/project-standards/issues/229)). 1.17 is re-linked from its own payload path with its own stamp and prints `agent-handoff session-start 1.17`; a contract test now pins that equality against the catalog's default version, so a future cut cannot reintroduce the drift. The 1.14–1.16 bytes are published and immutable and keep answering `1.14`. +- **`scripts/verify.sh --full` runs its ordinary lane in parallel, and the compatibility matrix drops 21 rows and 57 duplicate wheel arms** ([#236](https://github.com/L3DigitalNet/project-standards/issues/236), [#227](https://github.com/L3DigitalNet/project-standards/issues/227)). The `--full` ordinary lane ran single-process for roughly 50 minutes of every release train to claim test isolation, a claim the written record of past trains never once shows catching a red ([`docs/research/2026-09-01-release-train-wall-clock.md`](docs/research/2026-09-01-release-train-wall-clock.md) §2, lever F); it now runs the identical selection at `-n 16 --dist load`, still under the default trace core, so the coverage-core cross-check against the `sysmon` fast gate that the lane also carried is unaffected. `--full` therefore gains a `coverage-combine` lane, which its single data file previously made unnecessary. In the compatibility matrix the 36 pairwise rows now run against the source distribution only — pairwise interaction between two packages' managed blocks is distribution-independent, and source/wheel parity is still proven per package by the single-package, full-set, and partial-migration rows — and the 21 legacy pairwise rows are removed as redundant with the 14 partial-migration rows and the `all-namespace-legacy` full-set row. Collected compatibility rows fall from 150 to 129. Repository tooling only: no package, payload, or consumer-visible byte changes. + ## [5.28.0] — 2026-09-01 ### Added diff --git a/docs/research/2026-09-01-release-train-wall-clock.md b/docs/research/2026-09-01-release-train-wall-clock.md index 5e242240..fbd4c458 100644 --- a/docs/research/2026-09-01-release-train-wall-clock.md +++ b/docs/research/2026-09-01-release-train-wall-clock.md @@ -158,3 +158,5 @@ paths-ignore: - **Cheapest safe adoption today:** lever A. Run the pre-tag battery as `rexec -- env VERIFY_FULL_COMPAT_WORKERS=16 scripts/verify.sh --full`. No file changes, no claim weakened, ~30 minutes off every train. If it proves out over a train or two, the owner can decide whether the default at `scripts/verify.sh:70` should move (a code change this report does not make). - **Right long-term, even though it costs more up front:** lever D plus the §4 verdict record, then lever C for the hosted path. The record is the piece with lasting value: it converts "the gate ran" into "these five verdicts cover this exact tree", which is what makes any future reuse or lane-scoped re-run safe. Lever E should not be built until that record exists and has been exercised. - **Do not adopt yet:** lever F. The serial ordinary lane has never been shown to earn its 45 minutes, but it carries the coverage-core cross-check as well, and A+C recover more time without giving up anything. + +**Adopted 2026-09-01 (owner decision, issue #236 C4) — in the narrowed form this section asks for.** The lane is replaced rather than retired: `--full` now runs the identical selection at `-n 16 --dist load` under the default trace core, so the coverage-core cross-check against the `sysmon` fast gate survives intact and only the isolation claim of §2 — the one this report could find no evidence for — is given up. `--full` gains a `coverage-combine` lane, which the previous single-process data file made unnecessary. diff --git a/scripts/verify.sh b/scripts/verify.sh index 9181e520..744c9a9b 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -8,11 +8,19 @@ # (its assertions are timing-sensitive and must not share the machine), # then `coverage combine` + `coverage report`. # -# --full runs the legacy serial sequence instead (statics, serial ordinary -# coverage run, compatibility, performance, report). That is the release-prep -# cross-check: it is the configuration the coverage baseline was established -# under, so a disagreement between it and the fast gate is a real signal -# rather than a parallelism artifact. +# --full runs the same lane selections one at a time (statics, ordinary +# coverage run, compatibility, performance, coverage combine, report). That is +# the release-prep cross-check: it varies the coverage core and the machine +# contention, not the test selection, so a disagreement between it and the fast +# gate is a real signal rather than a parallelism artifact. +# +# Its ordinary lane runs at -n "$ORDINARY_WORKERS" like the fast gate's, not +# single-process (issue #236 C4, lever F of +# docs/research/2026-09-01-release-train-wall-clock.md, adopted 2026-09-01). +# The single-process form cost ~50 minutes of every train and its isolation +# claim was never once the thing that caught a red; the coverage-core claim it +# also carried is the half worth keeping, and that one only needs the lane to +# run under the trace core, not to run serially. # # Every command runs from `.venv/bin` rather than through `uv run`. Concurrent # `uv run` invocations contend on the uv cache (the 2026-07-29 failure class); @@ -34,10 +42,10 @@ # next to the data file; in-root they race the read-only digest # proofs and the wheel-source copytree. # COVERAGE_CORE sysmon in the fast gate only (proved report-identical to the -# serial trace core). --full keeps the default trace core so the -# cross-check varies the core as well as the parallelism — -# otherwise a sysmon-specific divergence would be undetectable -# by the very lane documented to catch it. +# trace core). --full keeps the default trace core, and that is +# now the whole of what the two modes vary in their ordinary +# lane — otherwise a sysmon-specific divergence would be +# undetectable by the very lane documented to catch it. # # Usage: # scripts/verify.sh fast gate (default) @@ -372,17 +380,6 @@ lane_ordinary() { reap_basetemp "$BASETEMP_ROOT/ordinary" } -lane_ordinary_serial() { - local args=( - --source=project_standards -m pytest - -m "not performance and not compatibility" - --basetemp="$BASETEMP_ROOT/ordinary" - ) - [[ "$SMOKE" == "1" ]] && args+=(-k test_repository_workflow) - "$VENV_BIN/coverage" run "${args[@]}" || return $? - reap_basetemp "$BASETEMP_ROOT/ordinary" -} - lane_compatibility() { local workers="$1" local args=( @@ -457,9 +454,17 @@ if [[ "$MODE" == "fast" ]]; then serial_lane_unless_red coverage-combine lane_coverage_combine else serial_lane_unless_red statics lane_statics - serial_lane_unless_red ordinary lane_ordinary_serial + # The same lane the fast gate runs, deliberately: what --full varies is the + # coverage core (trace here, sysmon there) and the fact that nothing else is + # on the machine, not the selection or the worker count. + serial_lane_unless_red ordinary lane_ordinary serial_lane_unless_red compatibility lane_compatibility "$FULL_COMPAT_WORKERS" serial_lane_unless_red performance lane_performance + # Required since the ordinary lane became parallel here too: xdist workers + # write their own `.coverage.*` files (tests/conftest.py), so without a + # combine the report would see only the controller process and the fail-under + # would trip on a lane that actually passed. + serial_lane_unless_red coverage-combine lane_coverage_combine fi serial_lane_unless_red coverage-report lane_coverage_report diff --git a/tests/package_compatibility/test_catalog_matrix.py b/tests/package_compatibility/test_catalog_matrix.py index 5cb66867..0153f8ce 100644 --- a/tests/package_compatibility/test_catalog_matrix.py +++ b/tests/package_compatibility/test_catalog_matrix.py @@ -55,12 +55,10 @@ # satisfy the byte-identical re-enable assertion. The fresh rows carry the # catalog-5-only packages' full pair and full-set coverage. _LEGACY_DEFAULTS = legacy_migratable_ids() -_LEGACY_PAIRS = tuple(combinations(_LEGACY_DEFAULTS, 2)) _PARTIAL_MIGRATION_ROWS = tuple((standard_id,) for standard_id in _LEGACY_DEFAULTS) + tuple( tuple(candidate for candidate in _LEGACY_DEFAULTS if candidate != omitted) for omitted in _LEGACY_DEFAULTS ) -_PAIR_ROWS = tuple((pair, False) for pair in _PAIRS) + tuple((pair, True) for pair in _LEGACY_PAIRS) _MANDATORY_GROUPS = ( ("python-tooling", "agent-handoff", "markdown-tooling"), ("adr", "markdown-frontmatter"), @@ -343,27 +341,30 @@ def test_each_package_converges_alone_from_source_and_wheel( def _pair_row_id(value: object) -> str: - if isinstance(value, tuple): - return "+".join(cast("tuple[str, ...]", value)) - return "migrated" if value else "fresh" + return "+".join(cast("tuple[str, ...]", value)) -@pytest.mark.parametrize(("standard_ids", "migrated"), _PAIR_ROWS, ids=_pair_row_id) +@pytest.mark.parametrize("standard_ids", _PAIRS, ids=_pair_row_id) def test_every_unordered_pair_preserves_ownership_and_converges( tmp_path: Path, source_payload_distribution: InstalledDistribution, - wheel_payload_distribution: InstalledDistribution, standard_ids: tuple[str, str], - *, - migrated: bool, ) -> None: - _exercise_both( - tmp_path, - source_payload_distribution, - wheel_payload_distribution, - standard_ids, - migrated=migrated, - ) + """Prove the pairwise ownership and convergence claim, source distribution only. + + What each pair row is for is the interaction between two packages' managed + blocks, which is distribution-independent: both distributions serve the same + payload bytes. Dual-distribution parity is a separate claim, and it is still + proven for every package by the single-package rows, the two full-set rows + and the partial-migration rows (owner decision 2026-09-01, #227 E2#1; the + battery record carries no red that only the wheel arm ever produced). + + The 21 migrated pair rows are gone with it (#227 E2#2): a legacy pair adds + nothing over the partial-migration rows, which exercise the same migration + against every one-package and every one-omitted subset, plus the + all-namespace-legacy full-set row. + """ + exercise_fresh_lifecycle(tmp_path / "source", source_payload_distribution, standard_ids) @pytest.mark.parametrize("migrated", [False, True], ids=["fresh", "all-namespace-legacy"]) diff --git a/tests/test_repository_test_gate.py b/tests/test_repository_test_gate.py index 74c48c29..63b39573 100644 --- a/tests/test_repository_test_gate.py +++ b/tests/test_repository_test_gate.py @@ -403,6 +403,15 @@ def test_verify_gate__fast_mode__starts_parallel_lanes_before_serial_tail(tmp_pa def test_verify_gate__full_mode__preserves_serial_lane_order(tmp_path: Path) -> None: + """--full sequences the lanes one at a time, with the fast gate's selections. + + The ordinary lane is parallel here too since #236 C4 (lever F of + docs/research/2026-09-01-release-train-wall-clock.md): what --full still + varies is the coverage core and the exclusive machine, not the selection. + The combine is load-bearing rather than cosmetic — xdist workers write their + own ``.coverage.*`` files, so a report without it would see only the + controller process. + """ repo, log, environment = _gate_fixture(tmp_path, wheel_count=1) completed = _run_gate(repo, environment, "--full") @@ -413,11 +422,13 @@ def test_verify_gate__full_mode__preserves_serial_lane_order(tmp_path: Path) -> assert invocations[1] == "ruff format --check ." assert invocations[2] == "ruff check ." assert invocations[3].startswith( - "coverage run --source=project_standards -m pytest -m not performance and not compatibility" + "coverage run --source=project_standards -m pytest " + "-m not performance and not compatibility -n 16 --dist load" ) assert invocations[4].startswith("pytest -m compatibility -n 16") assert invocations[5].startswith("pytest -m performance") - assert invocations[6] == "coverage report --fail-under=0" + assert invocations[6] == "coverage combine" + assert invocations[7] == "coverage report --fail-under=0" def test_verify_gate__full_mode__red_ordinary_lane__skips_the_later_lanes( @@ -440,7 +451,7 @@ def test_verify_gate__full_mode__red_ordinary_lane__skips_the_later_lanes( summary = completed.stdout[completed.stdout.index("════ summary ════") :] assert "ordinary" in summary assert "FAILED (exit 13)" in summary - for lane in ("compatibility", "performance", "coverage-report"): + for lane in ("compatibility", "performance", "coverage-combine", "coverage-report"): assert lane in summary line = next(row for row in summary.splitlines() if row.strip().startswith(lane)) assert "skipped (--fail-fast)" in line