raptor-core 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
raptor/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """raptor — the protocol spine of the RAPTOR family.
5
+
6
+ Pure contracts; depends on nothing. Schemas live in :mod:`raptor.schema`;
7
+ typing.Protocols live in :mod:`raptor.protocols`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .version import __version__
13
+
14
+ __all__ = ["__version__"]
@@ -0,0 +1,38 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """raptor.conformance — the interop certification declaration and its
5
+ reusable matrix-hygiene scanners. See :mod:`raptor.conformance.interop`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .interop import (
11
+ CERTIFIED,
12
+ ROADMAP,
13
+ ROW_KINDS,
14
+ ROWS,
15
+ UNIVERSE,
16
+ RowDecl,
17
+ assert_doc_rows_match,
18
+ assert_no_skip_tokens,
19
+ assert_rows_complete,
20
+ certified_framework,
21
+ register_row,
22
+ roadmap_framework,
23
+ )
24
+
25
+ __all__ = [
26
+ "CERTIFIED",
27
+ "ROADMAP",
28
+ "ROW_KINDS",
29
+ "ROWS",
30
+ "UNIVERSE",
31
+ "RowDecl",
32
+ "assert_doc_rows_match",
33
+ "assert_no_skip_tokens",
34
+ "assert_rows_complete",
35
+ "certified_framework",
36
+ "register_row",
37
+ "roadmap_framework",
38
+ ]
@@ -0,0 +1,178 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """raptor.conformance.doc_includes — reusable Sphinx `include`/`literalinclude`
5
+ target checker.
6
+
7
+ **Why this exists.** `eagle/docs/conf.py` sets `suppress_warnings = [...,
8
+ "docutils", ...]` -- the category Sphinx uses for "Include file ... not
9
+ found" -- so a broken `literalinclude` target prints one warning line and the
10
+ build still exits 0. Several of eagle's targets once pointed at a directory
11
+ later extracted to a downstream package, and this shipped silently for the
12
+ length of the pass. Blanket `-W` is not an option (most surfaced eagle
13
+ warnings are pre-existing doxygen duplicate-ID noise under the SAME
14
+ "docutils" category, so the suppression cannot be narrowed without also
15
+ un-suppressing that noise). This module is a targeted,
16
+ non-Sphinx-warning-dependent check instead: a plain filesystem scan that
17
+ reproduces Sphinx's own path-resolution rules and asserts every
18
+ `include`/`literalinclude` target exists.
19
+
20
+ **Why it lives here, not downstream:** the logic was always path-free (it
21
+ takes `docs_root` as an argument and knows nothing about which repos exist
22
+ on disk or where they live), but it once shipped from a downstream sibling's
23
+ own suite. That placement broke silently the day that sibling's own suite
24
+ shape changed (a sibling-free venv-gate leg could no longer resolve the tool
25
+ at all, landing the file blocked-at-collection instead of running). The
26
+ claim "eagle's includes resolve" is eagle's; a claim gated only in a
27
+ downstream sibling's suite stops running the day that suite changes shape.
28
+ Moving the LOGIC here (raptor ships to every consumer; `include =
29
+ ["raptor*"]`) lets each owner (`eagle/python/tests/test_doc_includes.py`,
30
+ a downstream package's own test) certify its OWN docs tree directly, with
31
+ raptor keeping zero sibling/repo knowledge.
32
+
33
+ **Resolution rules (reproduce Sphinx's `include`/`literalinclude` directives
34
+ exactly -- verified against the live eagle tree, not assumed):**
35
+
36
+ 1. a target starting with "/" is relative to the docs SOURCE directory
37
+ (`docs_root`, the directory `conf.py` lives in) -- never the filesystem
38
+ root (a target like `/schemas/manifest-v1.schema.json` resolves to
39
+ `<docs_root>/schemas/manifest-v1.schema.json`, not `/schemas/...` on
40
+ disk);
41
+ 2. otherwise the target is relative to the directory containing the
42
+ document that names it (the common case);
43
+ 3. `_build/` is excluded -- it holds generated copies of the same source
44
+ pages (`_build/html/_sources/**`), so scanning it double-counts every
45
+ hit and would also flag the generated tree's own copies once a source
46
+ fix has already landed but the stale `_build/` has not been rebuilt.
47
+
48
+ Handles both reStructuredText directives (``.. literalinclude:: target``,
49
+ eagle's dialect) and MyST fenced directives (a code fence whose info string is
50
+ ``{literalinclude} target`` -- three backticks followed by that info string,
51
+ the rest of the family's dialect) with the same scan, so the same call is
52
+ reusable across the family regardless of which markup a repo's docs use.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import re
58
+ import sys
59
+ from dataclasses import dataclass
60
+ from pathlib import Path
61
+
62
+ #: reST: ".. include:: target" / ".. literalinclude:: target" (optionally
63
+ #: indented, e.g. nested inside a ".. dropdown::").
64
+ _RST_DIRECTIVE = re.compile(r"^\s*\.\.\s+(include|literalinclude)::\s+(\S+)\s*$")
65
+
66
+ #: MyST: "```{include} target" / "```{literalinclude} target" (the directive's
67
+ #: argument sits on the fence line itself).
68
+ _MYST_DIRECTIVE = re.compile(r"^\s*`{3,}\{(include|literalinclude)\}\s+(\S+)\s*$")
69
+
70
+ #: Source file suffixes scanned. `.rst` (docutils) and `.md` (MyST) are the
71
+ #: two dialects in use across the family's docs trees today.
72
+ SOURCE_SUFFIXES = (".rst", ".md")
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class BrokenInclude:
77
+ """One `include`/`literalinclude` directive whose target does not exist."""
78
+
79
+ doc: Path
80
+ line: int
81
+ directive: str
82
+ target: str
83
+ resolved: Path
84
+
85
+ def __str__(self) -> str:
86
+ return (
87
+ f"{self.doc}:{self.line}: .. {self.directive}:: {self.target} "
88
+ f"-> missing {self.resolved}"
89
+ )
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class DocsScanReport:
94
+ """One `scan_docs` result: the root scanned, the TOTAL number of
95
+ `include`/`literalinclude` directives found (regardless of whether they
96
+ resolved), and the broken subset.
97
+
98
+ `n_directives` closes a vacuity trap: a scan pointed at an empty or
99
+ mis-rooted `docs_root` reports `broken == []` vacuously -- nothing was
100
+ found to break. A caller must assert `n_directives >= 1` alongside
101
+ `broken == []`, or an empty/wrong root passes silently.
102
+ """
103
+
104
+ docs_root: Path
105
+ n_directives: int
106
+ broken: list[BrokenInclude]
107
+
108
+
109
+ def resolve_target(target: str, doc_path: Path, docs_root: Path) -> Path:
110
+ """Resolve an `include`/`literalinclude` target using Sphinx's own rule.
111
+
112
+ ``target`` starting with ``/`` is rooted at ``docs_root`` (the directory
113
+ ``conf.py`` lives in), never the filesystem root. Otherwise it is
114
+ resolved relative to ``doc_path``'s own directory (the document that
115
+ names it), matching both docutils' `include` directive and Sphinx's
116
+ `literalinclude` extension of it.
117
+ """
118
+ if target.startswith("/"):
119
+ return (docs_root / target[1:]).resolve()
120
+ return (doc_path.parent / target).resolve()
121
+
122
+
123
+ def _iter_directives(text: str):
124
+ for lineno, line in enumerate(text.splitlines(), start=1):
125
+ m = _RST_DIRECTIVE.match(line)
126
+ if m is None:
127
+ m = _MYST_DIRECTIVE.match(line)
128
+ if m is not None:
129
+ yield lineno, m.group(1), m.group(2)
130
+
131
+
132
+ def scan_docs(docs_root) -> DocsScanReport:
133
+ """Scan every ``.rst``/``.md`` file under ``docs_root`` (excluding
134
+ ``_build/``) for `include`/`literalinclude` directives, resolving each
135
+ with :func:`resolve_target`. One filesystem walk produces both the total
136
+ directive count and the broken subset (see :class:`DocsScanReport`).
137
+ """
138
+ docs_root = Path(docs_root).resolve()
139
+ n_directives = 0
140
+ broken: list[BrokenInclude] = []
141
+ for suffix in SOURCE_SUFFIXES:
142
+ for doc_path in sorted(docs_root.rglob(f"*{suffix}")):
143
+ if "_build" in doc_path.relative_to(docs_root).parts:
144
+ continue
145
+ text = doc_path.read_text(encoding="utf-8")
146
+ for lineno, directive, target in _iter_directives(text):
147
+ n_directives += 1
148
+ resolved = resolve_target(target, doc_path, docs_root)
149
+ if not resolved.is_file():
150
+ broken.append(
151
+ BrokenInclude(doc_path, lineno, directive, target, resolved)
152
+ )
153
+ return DocsScanReport(docs_root, n_directives, broken)
154
+
155
+
156
+ def main(argv: list[str] | None = None) -> int:
157
+ """CLI entry point: print the directive count and every broken include
158
+ found under each docs root given."""
159
+ argv = sys.argv[1:] if argv is None else argv
160
+ if not argv:
161
+ print(
162
+ "usage: python -m raptor.conformance.doc_includes <docs_root> "
163
+ "[<docs_root> ...]",
164
+ file=sys.stderr,
165
+ )
166
+ return 2
167
+ red = 0
168
+ for root in argv:
169
+ report = scan_docs(Path(root))
170
+ print(f"{root}: {report.n_directives} directive(s) scanned", file=sys.stderr)
171
+ for broken in report.broken:
172
+ red = 1
173
+ print(broken)
174
+ return red
175
+
176
+
177
+ if __name__ == "__main__":
178
+ raise SystemExit(main())
@@ -0,0 +1,393 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The interop certification declaration and reusable matrix-hygiene scanners.
5
+
6
+ raptor is the framework-free contract owner: this module names every
7
+ framework the family will ever bridge to (:data:`UNIVERSE`), which subset is
8
+ certified today (:data:`CERTIFIED`) vs. declared-but-not-yet-built
9
+ (:data:`ROADMAP`), and the row-level ``ROWS`` catalogue, declared here in
10
+ full ahead of the packages that write the matrix tests — so that
11
+ :func:`assert_rows_complete` has a real gap to catch instead of declared and
12
+ collected always landing together. Framework objects NEVER enter raptor —
13
+ pointers and callables are passed in duck-typed; this module itself imports
14
+ no framework, ever (``find_spec`` never imports).
15
+
16
+ Pure-Python, numpy-free by design — raptor has zero hard dependencies.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import ast
22
+ import importlib.util
23
+ import re
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+ #: every framework the family will ever bridge to, day-0 and roadmap together.
28
+ UNIVERSE = ("numpy", "cupy", "torch", "warp", "jax", "tensorflow")
29
+
30
+ #: frameworks with an executable, certified round-trip today.
31
+ CERTIFIED = ("numpy", "cupy", "torch", "warp")
32
+
33
+ #: frameworks declared absent-by-design — no row certifies them yet.
34
+ ROADMAP = ("jax", "tensorflow")
35
+
36
+ #: the vocabulary a :class:`RowDecl` may declare for how a row moves data.
37
+ #: ``ALIAS`` — the crossing shares the same underlying buffer, no copy (e.g.
38
+ #: DLPack zero-copy into another framework's tensor).
39
+ #: ``COPY`` — the crossing makes an independent copy of the data.
40
+ #: ``STREAM`` — the crossing's ordering relative to other work on the same
41
+ #: stream/queue (not whether data is shared or copied).
42
+ #: ``ENV`` — a precondition on the execution environment/state that gates
43
+ #: whether a crossing is legal (e.g. CUDA graph capture mode, device
44
+ #: placement, a layout self-check) — certifying a loud refusal under the
45
+ #: wrong state as much as acceptance under the right one.
46
+ #: ``INVARIANCE`` — the same plugin body produces equivalent results under
47
+ #: different execution structures (bit-exact for some structures, within a
48
+ #: tolerance band for others) — distinct from ALIAS/COPY (which certify a
49
+ #: framework crossing's pointer semantics) and from STREAM (ordering).
50
+ ROW_KINDS = frozenset({"ALIAS", "COPY", "STREAM", "ENV", "INVARIANCE"})
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class RowDecl:
55
+ """One declared interop-matrix row.
56
+
57
+ ``framework`` must be a member of :data:`CERTIFIED`, OR ``None`` for a row
58
+ that bridges to no array framework at all — an execution-structure
59
+ invariant internal to eagle rather than a numpy/cupy/torch crossing.
60
+ Every prior row, even ``CP-CAPTURE-REJECT``/``T-DEVICE-FAILLOUD``, still
61
+ names the framework whose crossing it guards; widening ``framework`` to
62
+ ``Optional[str]`` — rather than inventing a sixth pseudo-framework entry
63
+ in :data:`CERTIFIED`/:data:`UNIVERSE` — keeps that vocabulary honestly
64
+ scoped to actual array frameworks. ``owner_repo`` is the repo whose test
65
+ suite carries the executable assertion, so the matrix rows live with the
66
+ seam they certify; ``kind`` is one of :data:`ROW_KINDS`.
67
+ """
68
+
69
+ framework: str | None
70
+ owner_repo: str
71
+ kind: str
72
+
73
+ def __post_init__(self) -> None:
74
+ if self.kind not in ROW_KINDS:
75
+ raise ValueError(
76
+ f"RowDecl.kind={self.kind!r} not in {sorted(ROW_KINDS)}"
77
+ )
78
+
79
+
80
+ #: row_id -> RowDecl — the full row catalogue, declared here ahead of the
81
+ #: packages that write the tests. This is deliberate: if declaring and
82
+ #: collecting happened in the same later package, declared and collected
83
+ #: would always agree by construction and :func:`assert_rows_complete` could
84
+ #: never bite. Every row named elsewhere in the family exists below under its
85
+ #: catalogue ID.
86
+ ROWS: dict[str, RowDecl] = {
87
+ # numpy (host) rows.
88
+ # cupy — kernel-level rows and adapter/pipeline rows, split by owner.
89
+ # CP-STREAM-ORDER is a named cross-reference to STREAM-CUPY-PRODUCER-ORDER
90
+ # below, not a second row.
91
+ "CP-CAPTURE-REJECT": RowDecl("cupy", "eagle", "ENV"),
92
+ # torch — adapter+stream rows, kernel-level rows, and bridge+backend
93
+ # rows, split by owner.
94
+ "T-IN-CUDA-ALIAS": RowDecl("torch", "eagle", "ALIAS"),
95
+ "T-OUT-CUDA-ALIAS": RowDecl("torch", "eagle", "ALIAS"),
96
+ "T-IN-CPU-COPY": RowDecl("torch", "eagle", "COPY"),
97
+ # A row separate from T-IN-CPU-COPY immediately above: that row stays
98
+ # owner=eagle and keeps certifying eagle's own adapter crossing. This one
99
+ # certifies a different seam: a native host provider round-trips a
100
+ # conforming (float64, C-contiguous) torch CPU tensor by pointer-alias —
101
+ # no upload, no eagle, no cupy. The declaration here is authoritative, an
102
+ # explicit re-declaration rather than a quiet one.
103
+ # Note eagle's adapter ALSO copies float32 (``eagle/python/eagle/interop.py``);
104
+ # this row certifies the kernel-level crossing, per the grouping above.
105
+ "T-BRIDGE-COPY": RowDecl("torch", "learning_runtime", "COPY"),
106
+ "T-BACKEND-CPU-IN-ALIAS": RowDecl("torch", "learning_runtime", "ALIAS"),
107
+ # Renamed from "T-BACKEND-CUDA": kind=ALIAS is true for the INPUT crossing
108
+ # only — the learning runtime's bridge copies unconditionally on export
109
+ # (any live replay buffer is copied off), so no backend row can be an
110
+ # alias in the OUT direction — that copy is itself certified by
111
+ # T-BRIDGE-COPY. The unqualified name would have published as "the CUDA
112
+ # backend aliases", which is false for outputs; the ID now names its
113
+ # direction, matching its CPU sibling.
114
+ "T-BACKEND-CUDA-IN-ALIAS": RowDecl("torch", "learning_runtime", "ALIAS"),
115
+ "T-DEVICE-FAILLOUD": RowDecl("torch", "learning_runtime", "ENV"),
116
+ # stream rows — owner eagle, GPU-serial.
117
+ "STREAM-TORCH-PRODUCER-ORDER": RowDecl("torch", "eagle", "STREAM"),
118
+ "STREAM-CUPY-PRODUCER-ORDER": RowDecl("cupy", "eagle", "STREAM"),
119
+ "STREAM-TORCH-CONSUMER-ORDER": RowDecl("torch", "eagle", "STREAM"),
120
+ "STREAM-IDENTITY": RowDecl("torch", "eagle", "STREAM"),
121
+ # warp — NVIDIA Warp crosses through eagle's generic zero-copy buffer
122
+ # layer (eagle.interop.import_buffer), not a dedicated Adapter: any
123
+ # __dlpack__ + __dlpack_device__ producer already routes through
124
+ # eagle.interop._DlpackAdapter, so warp needed no new adapter code, only
125
+ # these rows. Warp speaks legacy (pre-1.0) DLPack, so its access flag is
126
+ # always "unknown" — that is part of WP-IN-CUDA-ALIAS, not a separate
127
+ # row (same bundling convention T-IN-CUDA-ALIAS already uses for its own
128
+ # three legs). It joins the other STREAM-* rows.
129
+ "WP-IN-CUDA-ALIAS": RowDecl("warp", "eagle", "ALIAS"),
130
+ "WP-OUT-CUDA-ALIAS": RowDecl("warp", "eagle", "ALIAS"),
131
+ "STREAM-WARP-PRODUCER-ORDER": RowDecl("warp", "eagle", "STREAM"),
132
+ # execution rows: the closed RowDecl framework/kind sets extended (above)
133
+ # so these four can be declared here, ahead of their eagle-owned consumer
134
+ # implementation — the same declare-before-build convention every other
135
+ # eagle-owned row above already uses (e.g. CP-CAPTURE-REJECT): raptor's
136
+ # own suite never calls ``assert_rows_complete("eagle")``, so a
137
+ # declared-but-uncollected eagle row is not a gap this repo's gate can
138
+ # see — that completeness gate is eagle's own, in its own suite. Row ids
139
+ # are given in eagle's own snake_case naming, not this file's
140
+ # UPPER-HYPHEN convention — kept as given rather than renamed to fit.
141
+ "execution_partition_identity": RowDecl(None, "eagle", "INVARIANCE"),
142
+ "execution_host_device_twin": RowDecl(None, "eagle", "INVARIANCE"),
143
+ "execution_illegal_placement_refused": RowDecl(None, "eagle", "ENV"),
144
+ "execution_layout_selfcheck_refused": RowDecl(None, "eagle", "ENV"),
145
+ # HAWK rows: rows that exist only because HAWK exists, declared
146
+ # owner_repo="hawk" — same declare-before-build convention as the
147
+ # execution_* rows above, ahead of HAWK's own consumer implementation.
148
+ # The numpy/cupy/torch crossing rows HAWK will own are declared when its
149
+ # consumer implementation lands, not here. framework=None for all
150
+ # five: none bridges to an array framework (see RowDecl's docstring);
151
+ # every check is deferred to its own consumer implementation. Row ids
152
+ # cite the row they carry in hawk's own documentation, which gives no
153
+ # snake_case name for them (contrast the execution_* rows' eagle-given
154
+ # names above) — this file's own UPPER-HYPHEN convention applies,
155
+ # prefixed HAWK- for the family.
156
+ "HAWK-DIFF-POSITIVE-ROUNDTRIP": RowDecl(None, "hawk", "INVARIANCE"),
157
+ "HAWK-EXEC-DEPLOY-LOADABLE": RowDecl(None, "hawk", "ENV"),
158
+ "HAWK-EXEC-REFERENCE-HOST": RowDecl(None, "hawk", "INVARIANCE"),
159
+ "HAWK-EXEC-REFERENCE-RANKPARTITION": RowDecl(None, "hawk", "INVARIANCE"),
160
+ }
161
+
162
+
163
+ def certified_framework(fw: str) -> None:
164
+ """Assert ``fw`` (a name in :data:`CERTIFIED`) is importable here.
165
+
166
+ Never skips: an absent certified framework is a hard failure of the gate
167
+ environment, not a reason to skip the row (a pytest SKIP is RC=0 and
168
+ certifies nothing). ``pytest`` is imported here, not at module level, so
169
+ the rest of :mod:`raptor.conformance` stays importable without it.
170
+ """
171
+ import pytest
172
+
173
+ if importlib.util.find_spec(fw) is None:
174
+ pytest.fail("certified framework absent from the gate environment")
175
+
176
+
177
+ def roadmap_framework(fw: str) -> None:
178
+ """Assert ``fw`` (a name in :data:`ROADMAP`) is ABSENT here.
179
+
180
+ Certified-absent and roadmap-present are BOTH failures — the declaration
181
+ and reality must agree in both directions, or the matrix certifies
182
+ nothing.
183
+ """
184
+ assert importlib.util.find_spec(fw) is None, (
185
+ "declared ROADMAP (absent-by-declaration) but INSTALLED — certify it "
186
+ "or remove it from the env"
187
+ )
188
+
189
+
190
+ #: row IDs actually collected this pytest session (populated by
191
+ #: :func:`register_row` at collection time).
192
+ _COLLECTED_ROWS: set[str] = set()
193
+
194
+
195
+ def register_row(row_id: str):
196
+ """Decorator marking a matrix test as covering declared row ``row_id``.
197
+
198
+ Raises :class:`KeyError` immediately if ``row_id`` is not in :data:`ROWS`
199
+ (a typo'd row id should fail loud at collection, not silently pass
200
+ :func:`assert_rows_complete`).
201
+ """
202
+ if row_id not in ROWS:
203
+ raise KeyError(f"register_row: {row_id!r} is not a declared row in ROWS")
204
+
205
+ def _decorate(fn):
206
+ _COLLECTED_ROWS.add(row_id)
207
+ return fn
208
+
209
+ return _decorate
210
+
211
+
212
+ def assert_rows_complete(owner_repo: str) -> None:
213
+ """Assert every :data:`ROWS` entry owned by ``owner_repo`` was collected.
214
+
215
+ Declared vs. collected row IDs; any declared row with no collected test
216
+ is RED.
217
+ """
218
+ declared = {rid for rid, decl in ROWS.items() if decl.owner_repo == owner_repo}
219
+ missing = declared - _COLLECTED_ROWS
220
+ assert not missing, (
221
+ f"rows declared for {owner_repo!r} but never collected: {sorted(missing)}"
222
+ )
223
+
224
+
225
+ #: token names that must never appear in a matrix module — skip machinery is
226
+ #: banned from matrix modules, mechanically.
227
+ _SKIP_TOKEN_ATTRS = frozenset({"skip", "skipif", "importorskip"})
228
+
229
+
230
+ def assert_no_skip_tokens(*module_paths) -> None:
231
+ """Assert zero ``pytest.skip``/``skipif``/``importorskip`` tokens.
232
+
233
+ AST-based over each path in ``module_paths`` (a matrix module file):
234
+ catches direct calls (``pytest.skip(...)``, ``pytest.importorskip(...)``)
235
+ and decorator uses (``@pytest.mark.skip``/``@pytest.mark.skipif``) alike,
236
+ since decorators are ordinary ``Attribute``/``Call`` nodes in the AST.
237
+ Absence of a certified framework must be a FAILURE by construction, never
238
+ a silent skip.
239
+ """
240
+ hits: list[str] = []
241
+ for raw_path in module_paths:
242
+ path = Path(raw_path)
243
+ tree = ast.parse(path.read_text(), filename=str(path))
244
+ for node in ast.walk(tree):
245
+ if isinstance(node, ast.Attribute) and node.attr in _SKIP_TOKEN_ATTRS:
246
+ hits.append(f"{path}:{node.lineno}: pytest.{node.attr}")
247
+ assert not hits, "banned skip tokens found in matrix modules:\n" + "\n".join(hits)
248
+
249
+
250
+ #: markers ANY conftest in the family wires into ``pytest_collection_modifyitems``'s
251
+ #: skip-on-absence machinery — the union across every family member's own
252
+ #: availability markers (verified against each repo's own
253
+ #: ``conftest.py``/``pyproject.toml`` directly, not assumed). Checking for a
254
+ #: marker a given repo's conftest doesn't define is always a zero-hit, never
255
+ #: a false negative — the union is safe to use everywhere.
256
+ _AVAILABILITY_MARKERS = frozenset({"gpu", "torch", "torch_gpu", "jax_gpu", "tf_gpu"})
257
+
258
+
259
+ def _calls_register_row(tree: ast.AST) -> bool:
260
+ return any(
261
+ isinstance(node, ast.Name) and node.id == "register_row"
262
+ for node in ast.walk(tree)
263
+ )
264
+
265
+
266
+ def discover_matrix_modules(test_dir) -> list[Path]:
267
+ """Every ``.py`` file under ``test_dir`` that calls :func:`register_row`
268
+ (AST-detected, not textually grepped) — the matrix modules a repo actually
269
+ ships, DISCOVERED rather than hand-passed.
270
+
271
+ A hand-passed module list is exactly how a matrix row can go unscanned by
272
+ accident — a row registered from a file its scanner's caller forgot to
273
+ name still counts as "collected" (``register_row`` fires at DECORATION
274
+ time, independent of whether pytest later skips the test), so
275
+ completeness and skip-freedom silently diverge. Discovery closes that gap
276
+ by construction: there is no second list to forget to update.
277
+ """
278
+ return sorted(p for p in Path(test_dir).rglob("*.py") if _calls_register_row(
279
+ ast.parse(p.read_text(), filename=str(p))
280
+ ))
281
+
282
+
283
+ def _registered_function_defs(tree: ast.AST):
284
+ """Yield ``(row_id, FunctionDef)`` for every function decorated
285
+ ``@register_row("...")`` in ``tree`` — the exact, narrow locus a matrix
286
+ row's own hygiene must hold at (not the whole file: a file mixing matrix
287
+ rows with ordinary, legitimately-marker-gated tests must not have its
288
+ unrelated tests' markers flagged)."""
289
+ for node in ast.walk(tree):
290
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
291
+ continue
292
+ for dec in node.decorator_list:
293
+ if (
294
+ isinstance(dec, ast.Call)
295
+ and isinstance(dec.func, ast.Name)
296
+ and dec.func.id == "register_row"
297
+ ):
298
+ row_id = (
299
+ dec.args[0].value
300
+ if dec.args and isinstance(dec.args[0], ast.Constant)
301
+ else "<unresolved>"
302
+ )
303
+ yield row_id, node
304
+ break
305
+
306
+
307
+ def _is_marker_decorator(dec: ast.expr, banned_markers: frozenset[str]) -> str | None:
308
+ """Return the banned marker name ``dec`` applies (``@pytest.mark.<name>``,
309
+ bare or called), or ``None``."""
310
+ target = dec.func if isinstance(dec, ast.Call) else dec
311
+ if (
312
+ isinstance(target, ast.Attribute)
313
+ and target.attr in banned_markers
314
+ and isinstance(target.value, ast.Attribute)
315
+ and target.value.attr == "mark"
316
+ ):
317
+ return target.attr
318
+ return None
319
+
320
+
321
+ def assert_no_skip_machinery(
322
+ test_dir, *, banned_markers: frozenset[str] = _AVAILABILITY_MARKERS
323
+ ) -> None:
324
+ """Assert every DISCOVERED matrix row (any ``@register_row``-decorated
325
+ function in any ``.py`` under ``test_dir``) is free of skip machinery:
326
+ zero ``pytest.skip``/``skipif``/``importorskip`` tokens in its body, AND
327
+ zero conftest availability-marker decorators (``banned_markers``, default
328
+ the cross-repo union) on the registered function itself.
329
+
330
+ Scoped to the REGISTERED function, not the whole file (see
331
+ :func:`_registered_function_defs`) — a file may legitimately mix matrix
332
+ rows with ordinary marker-gated tests; only the declared rows must be
333
+ unable to skip. Discovery (:func:`discover_matrix_modules`) means there is
334
+ no hand-passed module list to fall out of sync with reality: that is
335
+ exactly how a row once skipped silently while
336
+ :func:`assert_rows_complete` still passed — ``register_row`` collects at
337
+ decoration time, not at execution.
338
+ """
339
+ modules = discover_matrix_modules(test_dir)
340
+ assert modules, f"no register_row-bearing modules found under {test_dir}"
341
+ skip_hits: list[str] = []
342
+ marker_hits: list[str] = []
343
+ for path in modules:
344
+ tree = ast.parse(path.read_text(), filename=str(path))
345
+ for row_id, fn in _registered_function_defs(tree):
346
+ for dec in fn.decorator_list:
347
+ marker = _is_marker_decorator(dec, banned_markers)
348
+ if marker is not None:
349
+ marker_hits.append(
350
+ f"{path}:{fn.lineno}: row {row_id!r} carries "
351
+ f"@pytest.mark.{marker} (skip-capable)"
352
+ )
353
+ for sub in ast.walk(fn):
354
+ if isinstance(sub, ast.Attribute) and sub.attr in _SKIP_TOKEN_ATTRS:
355
+ skip_hits.append(
356
+ f"{path}:{sub.lineno}: row {row_id!r} uses pytest.{sub.attr}"
357
+ )
358
+ assert not skip_hits, "banned skip tokens found on registered rows:\n" + "\n".join(
359
+ skip_hits
360
+ )
361
+ assert not marker_hits, (
362
+ "banned availability-marker decorators found on registered rows:\n"
363
+ + "\n".join(marker_hits)
364
+ )
365
+
366
+
367
+ def assert_doc_rows_match(
368
+ doc_path,
369
+ rows: dict[str, RowDecl],
370
+ *,
371
+ row_id_pattern: str = r"`([A-Za-z][A-Za-z0-9_.:/-]*)`",
372
+ ) -> None:
373
+ """Assert the doc's row IDs and ``rows``'s keys are the same set.
374
+
375
+ Mirrors ``test_roles_vocab.py``'s technique (``eagle/python/tests/
376
+ test_roles_vocab.py``): the document is read as raw TEXT and scanned with
377
+ a regex — no markdown parser, no import of anything the doc describes —
378
+ then compared by set equality against the declaration, in both
379
+ directions. ``row_id_pattern`` defaults to backtick-quoted identifiers
380
+ (the row-id convention a table of ``` `row_id` ``` cells would use);
381
+ override it if the shipped document's convention differs.
382
+ """
383
+ text = Path(doc_path).read_text()
384
+ doc_ids = set(re.findall(row_id_pattern, text))
385
+ declared_ids = set(rows)
386
+ missing_from_doc = declared_ids - doc_ids
387
+ extra_in_doc = doc_ids - declared_ids
388
+ assert not missing_from_doc, (
389
+ f"rows declared but missing from {doc_path}: {sorted(missing_from_doc)}"
390
+ )
391
+ assert not extra_in_doc, (
392
+ f"{doc_path} references undeclared rows: {sorted(extra_in_doc)}"
393
+ )
@@ -0,0 +1,24 @@
1
+ # Copyright 2026 Alessandro Masat
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """raptor.protocols — the family's typing.Protocols.
5
+
6
+ Pure structural contracts; zero hard deps; zero imports of any downstream
7
+ package.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .backend import CAPABILITIES, Backend, CapabilityError, Executable
13
+ from .launcher import KernelLauncher, Marshal
14
+ from .provider import ManifestProvider
15
+
16
+ __all__ = [
17
+ "CAPABILITIES",
18
+ "Backend",
19
+ "CapabilityError",
20
+ "Executable",
21
+ "KernelLauncher",
22
+ "ManifestProvider",
23
+ "Marshal",
24
+ ]