Skip to content

fix(PS-140): scope the cross-package gate's skip to the ROOT so a rename can fail it - #369

Open
ywatanabe1989 wants to merge 1 commit into
developfrom
fix/ps140-cross-package-gate
Open

fix(PS-140): scope the cross-package gate's skip to the ROOT so a rename can fail it#369
ywatanabe1989 wants to merge 1 commit into
developfrom
fix/ps140-cross-package-gate

Conversation

@ywatanabe1989

Copy link
Copy Markdown
Collaborator

The defect: a gate that cannot fail

tests/integration/test_cross_package_imports.py promised, in its own docstring:

Renames or moves in peer standalones (or in the umbrella's deep-import
surface) surface as a hard failure here — not as a silent
ModuleNotFoundError when an end user runs the CLI.

It did the opposite. Its helper caught ModuleNotFoundError raised by importing
the full dotted path and skipped on it:

try:
    return importlib.import_module(module_name)
except ModuleNotFoundError as exc:
    pytest.skip(f"{module_name}: not importable in this environment ...")

A renamed or moved submodule — scitex_io._load_cache
scitex_io._loading._load_cache, the incident this file was written for —
raises exactly that exception. So the one failure the gate exists to catch was
swallowed and the run reported green. An absent peer and a rename inside an
installed peer raise the same exception type, and the old code could not tell
them apart, so it treated every rename as an absence.

The fix: skip on the ROOT, hard-import the FULL path

The two cases are separable by scope, not by exception type:

  • the ROOT distribution is not installed → a legitimately absent peer (optional
    extra, marker-gated dependency, lean install). Skip.
  • the root IS installed and a SUBMODULE of it is missing → a rename or a move.
    Fail. That is the gate.
root = module_name.split(".")[0]
pytest.importorskip(root)              # skip on the ROOT
return importlib.import_module(module_name)   # hard-import the FULL path

Simply dropping the skip would be the opposite pole of the same bug: a lean
install where a peer is legitimately absent would fail — a gate that cannot
PASS in place of one that cannot FAIL.

The umbrella needs one further absence recognised, and it does not widen the
hole: scitex.<x> forwards to a separate scitex_<x> distribution, so
import scitex.io fails naming scitex_io while the root scitex is present.
That is skipped only when the missing name is a different top-level
distribution with no importable spec at all. A missing submodule of an
installed distribution never matches it, which is what keeps a rename loud.

Proof the gate is alive — both control arms, one run

A bogus entry "scitex_io._this_module_does_not_exist" was injected into the
old helper and the new one and both were run together:

tests/integration/test_ps140_control_old.py s      [ 50%]
tests/integration/test_ps140_control_new.py F      [100%]

SKIPPED [1] test_ps140_control_old.py:247:
  scitex_io._this_module_does_not_exist: not importable in this environment
  (auto-gen snapshot drift / optional peer absent):
  No module named 'scitex_io._this_module_does_not_exist'

FAILED test_ps140_control_new.py::test_cross_package_import[scitex_io._this_module_does_not_exist]
E   ModuleNotFoundError: No module named 'scitex_io._this_module_does_not_exist'

1 failed, 1 skipped

Old code: SKIPPED (green). New code: FAILED. The control files were then
deleted and the gate re-run:

110 passed, 49 skipped, 4 xfailed

Baseline on the same tree before the change: 110 passed, 84 skipped. Same 110
passing — the 35 skips that disappeared were the gate's blind spot.

Two consequences of the gate coming alive

31 stale snapshot rows, pruned. None appears anywhere in src/:
src/scitex/ has no bridge, _dev/ holds only __init__.py, and gen is a
module (gen.py) so scitex.gen._x can never be a submodule. They were rows
that could only ever skip. (For scale: the SSoT collector
_collect_cross_package_imports finds 18 cross-package imports in the
current source against 194 declared — the umbrella's snapshot has never been
regenerated, because PS-140 exempts the umbrella and so nothing ever complained.)

4 entries are a real, live defect — recorded as strict xfails, not skipped
away.
src/scitex imports scitex_dev.{docs,search,skills,types} from
cli/docs.py, cli/skills.py, cli/mcp.py, _mcp/__init__.py and
_mcp/_umbrella_tools.py, and the pinned scitex-dev==0.28.0 ships none of
them:

$ git -C ~/proj/scitex-dev ls-tree --name-only v0.28.0 src/scitex_dev/
... src/scitex_dev/_docs   src/scitex_dev/_skills ...      (no docs/skills/search/types)

So this is not a local version skew — it is broken against the version this
package pins. _show_list in cli/docs.py imports unguarded, so it raises at
runtime. Where the symbols moved to is not unambiguousscitex_dev._docs
exports none of get_docs / build_docs / search_docs — so the import sites
are left to a separate change rather than guessed at. strict=True means this
gate fails the day they are corrected, until the record is deleted; it cannot
rot into a permanent exemption.

This is the defect class PS-140 was written for, and it was invisible for as
long as the gate could not fail.

Audit note

scitex-dev ecosystem audit-project scitex reports no PS-140 both before
and after this change: check_ps140_integration_gate returns early on
_is_umbrella(repo), so the umbrella's gate is never checked by the rule. The
defect was real regardless, which is why it is fixed here. No PS-224 finding is
reported for this repo either.

…ame can fail it

The gate could not fail. Its helper caught ModuleNotFoundError from the FULL
dotted path and skipped on it, but a renamed or moved submodule raises exactly
that exception -- so the one failure the gate exists to catch was swallowed and
the run reported green. The file's own docstring promised the opposite
("Renames or moves ... surface as a hard failure here"), and the two cases it
undertook to distinguish were indistinguishable to it.

An absent peer and a rename inside an installed peer raise the same exception
type. They are separable by SCOPE, not by type: skip when the ROOT distribution
is not installed, hard-import the FULL path. Dropping the skip altogether would
be the opposite pole of the same bug -- a lean install where a peer is
legitimately absent (optional extra, marker-gated dep) would fail, giving a gate
that cannot PASS in place of one that cannot FAIL.

Both control arms, one run, bogus entry "scitex_io._this_module_does_not_exist"
injected into each helper:

    old helper -> SKIPPED  "not importable in this environment
                            (auto-gen snapshot drift / optional peer absent)"
    new helper -> FAILED   ModuleNotFoundError: No module named
                            'scitex_io._this_module_does_not_exist'

Removing the bogus entry returns the gate to green.

The umbrella needs one further absence recognised, and it does not widen the
hole: `scitex.<x>` forwards to a separate `scitex_<x>` distribution, so
`import scitex.io` fails naming `scitex_io` while the root `scitex` is present.
That is skipped only when the missing name is a DIFFERENT top-level
distribution with no importable spec -- a missing submodule of an installed
distribution never matches, which is what keeps a rename loud.

Two consequences of the gate coming alive:

- 31 declared entries were stale snapshot rows, pruned. None appears anywhere
  in src/: `src/scitex/` has no `bridge`, `_dev/` holds only `__init__.py`, and
  `gen` is a module (gen.py) so `scitex.gen._x` can never be a submodule.

- 4 entries are a real, live defect and are recorded as strict xfails rather
  than skipped away: src/scitex imports scitex_dev.{docs,search,skills,types}
  from cli/docs.py, cli/skills.py, cli/mcp.py, _mcp/__init__.py and
  _mcp/_umbrella_tools.py, and the pinned scitex-dev==0.28.0 ships none of them
  -- `git ls-tree v0.28.0 src/scitex_dev/` lists `_docs` and `_skills` only.
  This is not a local version skew. `_show_list` in cli/docs.py imports
  unguarded, so it raises at runtime. Where the symbols moved is not
  unambiguous (scitex_dev._docs exports none of get_docs/build_docs/
  search_docs), so the import sites are left to a separate change rather than
  guessed at; strict=True means the gate fails the day they are fixed until the
  record is deleted.

Readings on tests/integration/test_cross_package_imports.py:

    before  110 passed,  84 skipped
    after   110 passed,  49 skipped, 4 xfailed

Same 110 passing. The 35 skips that disappeared were the gate's blind spot.
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