Skip to content

chore(devenv): report installed packages below a declared floor, where a local pass is read (#15091) - #15125

Merged
mrveiss merged 3 commits into
Dev_new_guifrom
issue-15091-devenv-floor-guard
Aug 26, 2026
Merged

mrveiss merged 3 commits into
Dev_new_guifrom
issue-15091-devenv-floor-guard

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 26, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

The issue reads as "the dev box is below the declared floor, and installing is forbidden, so the
best available answer is a warning". Reading the tree contradicted the premise in a useful way.

scripts/setup-ci-parity-env.sh (#13573) already exists, already builds Python 3.14 plus
requirements-ci.txt + requirements-ci-test.txt, and installs nothing outside its own venv —
so matching the declared set has never required install rights on the box. Better still, that venv
is already built here, and it resolves fastapi 0.141.1 — the exact floor #14998 tripped over.
So the honest remedy is not "push and read CI"; it is "build (or activate) the parity venv", with
push-and-read-CI as the fallback. AC1 is therefore answered yes rather than no.

That relocates the defect. The environment is not incapable of meeting the floor — nothing
tells anyone which environment they are in at the moment a green result is read. pr-preflight.sh
already noticed the interpreter fallback and said so, but nothing anywhere in the repo compared an
installed package version against a declared floor; every existing check is either
interpreter-minor or requirements-text-vs-requirements-text.

Two placement findings shaped the fix:

  • A pre-commit hook is the wrong home. language: python local hooks run in a pre-commit-managed
    isolated venv, so a hook there cannot observe the ambient environment at all.
  • The rootdir conftest.py is the only file that runs before every local pytest, and
    pytest_terminal_summary puts the banner directly beneath the pass/fail counts — the line actually
    read. A pytest_report_header would scroll away behind the run. #14998's false pass came from a
    bare pytest run, not from preflight, so preflight alone would not have caught it.

Warn rather than fail, per the issue's own AC — and because 90 of 206 declarations are unsatisfied on
this box. A gate failing on that would be deleted within a day.

I did not use packaging for version comparison: it is not declared in any requirements file here,
and making a plugin that loads on every pytest session depend on a transitively-present package is
the same class of fragility. The comparator is ~15 lines and unit-tested, including the pre-release
and unequal-segment-count cases a naive tuple compare gets wrong.

repo_tests/router_mount_parity_test.py is owned by #15093 and untouched; it already cites this
issue at :30-40, and its deliberate choice to stay static (rather than assert a version) is why this
PR reports the environment instead of adding a runtime router guard.

What Changed

  • pipeline-scripts/check_dependency_floors.py (new). Walks the -r closure of
    requirements-ci.txt, requirements-ci-test.txt, autobot-backend/requirements.txt and
    autobot-slm-backend/requirements.txt, compares every lower bound (==, >=, ~=, >) against
    importlib.metadata, and prints installed and declared side by side. An absent distribution is
    not a shortfall — it says nothing about what CI would resolve. An empty enumeration raises
    (EmptyEnumerationError, exit 2): "0 declarations, all satisfied" is indistinguishable from a
    healthy environment, which is the defect bug(tests): api.terminal.router enumerates zero routes under python-suite shard 12/12 (unidentified sys.modules pollution) #15087 records. --strict exits 1; --all drops the cap.
  • repo_tests/dependency_floor_banner.py (new) + registration in conftest.py. Prints the report
    under the pass/fail counts of every pytest run. Loads the checker by path, registering it in
    sys.modules first — required, not incidental: @dataclass resolves annotations through
    sys.modules[cls.__module__], and a module executed without that key raises AttributeError on
    import. A test caught this; the plugin had worked only because a sibling test had already imported
    the module.
  • scripts/pr-preflight.sh: its existing interpreter section now also reports package floors.
    Exit 2 from the checker is a fail — the check breaking must not read as a clean environment.
  • Print gate: the two print() calls in main() carry # noqa: print, the settled idiom for a
    standalone gate in pipeline-scripts/ — 55 occurrences across that directory, and no file there
    imports get_logger or logging.getLogger
    . These are CLI gates whose stdout is their output
    and CI reads it, so a logger would be the wrong shape.
  • docs/developer/CLAUDE_WORKFLOW.md: records the decision (match the declared set via the parity
    venv; mismatch is reported, never gated), both reporting points, and the include_router finding.

Verification

AC1 — decide and record. Recorded in docs/developer/CLAUDE_WORKFLOW.md, beside the existing
setup-ci-parity-env.sh section where a reader already looks. (static)

AC2 — loud rather than silent. (unit + forensic) Live run on a bare, unrelated test file, with
the banner landing beside the count:

  fastapi: installed 0.135.2, declared >=0.141.1 (autobot-backend/requirements.txt:7)
  ... and 80 more; run pipeline-scripts/check_dependency_floors.py --all to list them
Reproduce the declared environment: scripts/setup-ci-parity-env.sh
============================== 22 passed in 1.85s ==============================

AC3 — do the two versions genuinely differ? (forensic) Yes. Below the floor, the installed
fastapi 0.135.2 include_router() copies eagerly — its body runs
for route in router.routes: ... self.add_api_route(...) — and _IncludedRouter does not exist
anywhere in the installed starlette 1.0.0. At the declared 0.141.1 / starlette 1.6.0 it defers,
appending an _IncludedRouter with path=None; CI job 98088603289 on #15085 dumped exactly that
while the same code read 26 routes locally. #15093 owns the behaviour and the blast-radius sweep;
this PR records the finding in the workflow doc so it is not rediscovered.

AC4 — any other unsatisfied floor? Count reported. (unit) Yes, many. 206 declarations
enumerated. 90 unsatisfied under the system interpreter (Python 3.10.12); 44 unsatisfied under
the CI-parity venv (Python 3.14.6). fastapi is unsatisfied in the first and satisfied in the second.

Gates. black --check and flake8 clean under Python 3.14 from the parity venv, so black's
AST safety check actually ran rather than being skipped as it is on 3.10. check_python_file_size.py
clean; no KNOWN_LARGE entry added; largest new file 246 lines.

Stream and exit-code split (unit) — confirmed deliberate, three distinct codes so a caller can
act on the difference. No FATAL line is ever printed while the process exits 0:

Case stdout stderr exit
empty enumeration (empty) FATAL: no version declarations found … 2
below floor, reporting 15 lines (empty) 0
below floor, --strict report (empty) 1

pr-preflight.sh never passes --strict, so it sees only 0 or 2, and treats 2 as fail.

Tests. 52 pass. New files land in shard 7/12 (pipeline-scripts/check_dependency_floors_test.py)
and shard 8/12 (repo_tests/dependency_floor_banner_test.py) per repo_tests/stable_shard.py.

Contrast mutations, all against this PR's final head:

# Mutation Observed
1 audit() no longer raises on an empty enumeration 3 failed, 49 passed — test_empty_tree_raises_instead_of_reporting_clean, test_requirement_files_that_declare_nothing_also_raise, test_empty_enumeration_exits_two_and_says_so
2 >= compared strictly (have > want) 5 failed, 38 passed — incl. test_ordering[0.141.1->=-0.141.1-True], test_ordering[1.6->=-1.6.0-True], test_at_or_above_floor_is_silent
3 banner returns before writing anything 2 failed, 7 passed — test_names_both_versions_when_below_floor, test_points_at_the_remedy

Not proven here. The deferred-inclusion behaviour itself is not reproduced locally — this box
cannot install 0.141.1, and the parity venv carries starlette 1.3.1, below the declared >=1.6.0.
The CI-side half rests on #15093's cited job. No i18n: this is developer tooling, and its neighbours
(sys_modules_leak_guard.py, check_env_var_registry.py) carry plain English with zero i18n imports.

Model Used

Claude Opus 5 (1M context)

Closes #15091

…e a local pass is read (#15091)

Local verification runs under whatever interpreter is on PATH. When its
packages are older than the versions this repo declares, every local gate
still passes, and the pass says nothing about CI, which installs the declared
set. Nothing said so. #14998 lost a diagnosis cycle to exactly that.

Adds pipeline-scripts/check_dependency_floors.py: it reads the -r closure of
requirements-ci.txt, requirements-ci-test.txt and the two backend requirement
files, compares each lower bound against importlib.metadata, and names the
installed and declared versions side by side. An absent distribution is not a
shortfall; an empty enumeration is an error, never a clean report (#15087).

Two callers, because a local pass is read in two places:
  - scripts/pr-preflight.sh, in the interpreter section that already reports
    the Python fallback but said nothing about packages
  - a pytest terminal-summary banner, so a bare pytest run that never touches
    preflight still says it

Reported, never gated. A box below floor stays usable for ordinary work; the
decision and the remedy (scripts/setup-ci-parity-env.sh) are recorded in
docs/developer/CLAUDE_WORKFLOW.md.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

…ts do (#15091)

The no-print-console hook flagged the two print() calls in main(). Every other
standalone gate in pipeline-scripts/ marks these with `# noqa: print` -- 55
occurrences across the directory -- and not one file there imports get_logger
or logging.getLogger. These are CLI gates whose stdout IS their output, and CI
reads it; routing it through a logger would be the wrong shape.

Stream and exit-code split confirmed deliberate: a fatal empty enumeration
goes to stderr with exit 2 and writes nothing to stdout, the report goes to
stdout with exit 0, and --strict exits 1. Three distinct codes, so a caller
can act on the difference.
@mrveiss
mrveiss merged commit c81ded2 into Dev_new_gui Aug 26, 2026
55 of 56 checks passed
@mrveiss
mrveiss deleted the issue-15091-devenv-floor-guard branch August 26, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant