Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
if: matrix.run-windows-smoke == false
run: make check-fmt

- name: Run ruff
- name: Run lint and dead-code detection
if: matrix.run-windows-smoke == false
run: make lint

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ target/
.claude/
.memdb/
.grepai/
.skylos/
*.swp
*.swo
*~
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ When implementing changes, adhere to the following testing procedures:

- **Testing:** Passes all relevant unit and behavioural tests according to
the guidelines above (`make test`).
- **Linting:** Passes lint checks (`make lint`).
- **Linting:** Passes the complete `make lint` pipeline: Ruff, the
PyPy-backed Pylint rules, and the blocking Skylos dead-code scan.
Investigate every Skylos finding and remove genuine dead code. After
verifying a false positive, add a reasoned, reviewed exception to the
appropriate `[tool.skylos.dead_code.entrypoints]` or
`[tool.skylos.whitelist.documented]` table in `pyproject.toml`.
- **Formatting:** Adheres to formatting standards (`make check-fmt`,
formatting can be applied by running `make fmt`).
- **Typechecking:** Passes type checking (`make typecheck`).
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ PYLINT_PYPY_SHIM_REF ?= 726d09f968b4d729ee4b29c71fc732e744854f3b
PYLINT_PYPY_SHIM = git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_PYPY_SHIM_REF)
PYLINT_BASELINE_DISABLE = no-else-return,unnecessary-ellipsis,too-many-lines,too-many-arguments,too-many-positional-arguments,subprocess-run-check,use-implicit-booleaness-not-comparison-to-string,unnecessary-dunder-call,use-implicit-booleaness-not-comparison
PYLINT = $(UV_ENV) $(UV) tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy --disable=$(PYLINT_BASELINE_DISABLE)
SKYLOS_VERSION = 4.33.2
SKYLOS = $(UV_ENV) $(UV) tool run --from 'skylos==$(SKYLOS_VERSION)' skylos \
--config-file pyproject.toml
SKYLOS_PRODUCTION_TARGETS ?= cmd_mox
WINDOWS_SMOKE_ARGS = tests/test_windows_environment.py \
tests/test_windows_support_bdd.py \
--log-file=windows-ipc.log \
Expand Down Expand Up @@ -89,6 +93,7 @@ markdownlint-run: ## Run markdownlint-cli2 with pinned fallback
lint: build ## Run linters
$(RUFF) check
$(PYLINT) $(PYLINT_TARGETS)
$(SKYLOS) $(SKYLOS_PRODUCTION_TARGETS) --category dead_code --gate --format concise --no-upload --no-provenance --no-grep-verify
+$(MAKE) spelling

typecheck: build ## Run typechecking
Expand Down
4 changes: 0 additions & 4 deletions cmd_mox/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,6 @@ def _registered_commands(self) -> set[str]:
"""Return all commands registered via doubles."""
return set(self._doubles)

def _expected_commands(self) -> set[str]:
"""Return commands that must be called during replay."""
return {name for name, dbl in self._doubles.items() if dbl.is_expected}

# ------------------------------------------------------------------
# Context manager protocol
# ------------------------------------------------------------------
Expand Down
4 changes: 0 additions & 4 deletions cmd_mox/verifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,6 @@ def _validate_expectations_order(
self._check_order_violations(
ordered_seq,
relevant_invocations,
expected_descriptions,
actual_descriptions,
)
self._check_extra_invocations(
ordered_seq,
Expand Down Expand Up @@ -364,8 +362,6 @@ def _check_order_violations(
self,
ordered_seq: list[Expectation],
relevant_invocations: list[Invocation],
expected_descriptions: list[str],
actual_descriptions: list[str],
) -> None:
for index, (exp, actual_inv) in enumerate(
zip(ordered_seq, relevant_invocations, strict=False)
Expand Down
55 changes: 42 additions & 13 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ where lint policy is configured.

## Linting

CmdMox uses a two-tier linting pipeline. Run it with:
CmdMox uses a three-stage linting pipeline. Run it with:

```bash
make lint
```

The `lint` target first builds the development environment through
`make build`. It then runs the two lint tiers in order:
`make build`. It then runs the checks in order:

1. `ruff check`
2. PyPy-backed Pylint through `pylint-pypy-shim`
3. a blocking Skylos dead-code scan of the production package

Ruff is the fast first tier. It enforces import order, pycodestyle and Pyflakes
rules, pathlib usage, docstring rules, pytest rules, selected Ruff preview
Expand All @@ -29,21 +30,29 @@ footguns, and module or function shape limits. The Pylint tier is intentionally
focused: `pyproject.toml` disables all Pylint messages by default and then
enables only the selected messages that complement Ruff.

Skylos is the final production-liveness check. It is separately provisioned at
an exact release, scans `cmd_mox` without treating test references as live
callers, and fails the local gate and Linux CI when it reports unexplained dead
code. The scan uses only local static analysis: uploads, provenance collection,
and grep verification are disabled.

## Makefile lint variables

The `Makefile` exposes the lint runner through variables so developers and
Continuous Integration (CI) jobs can override the runtime without editing
project files.

| Variable | Default | Purpose |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `RUFF` | `$(UV_ENV) $(UV) run ruff` | Runs the Ruff command inside the `uv` environment. |
| `PYLINT_PYTHON` | `pypy` | Selects the Python interpreter used by `uv tool run` for Pylint. |
| `PYLINT_TARGETS` | `cmd_mox conftest.py examples tests` | Lists the directories and files linted by Pylint. |
| `PYLINT_PYPY_SHIM_REF` | `726d09f968b4d729ee4b29c71fc732e744854f3b` | Pins the shim repository revision. |
| `PYLINT_PYPY_SHIM` | `git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_PYPY_SHIM_REF)` | Identifies the shim package used by `uv tool run`. |
| `PYLINT_BASELINE_DISABLE` | Existing cmd-mox baseline | Temporarily disables legacy Pylint findings while keeping the second tier active. |
| `PYLINT` | `$(UV_ENV) $(UV) tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy --disable=$(PYLINT_BASELINE_DISABLE)` | Builds the full PyPy-backed Pylint command. |
| Variable | Default | Purpose |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `RUFF` | `$(UV_ENV) $(UV) run ruff` | Runs the Ruff command inside the `uv` environment. |
| `PYLINT_PYTHON` | `pypy` | Selects the Python interpreter used by `uv tool run` for Pylint. |
| `PYLINT_TARGETS` | `cmd_mox conftest.py examples tests` | Lists the directories and files linted by Pylint. |
| `PYLINT_PYPY_SHIM_REF` | `726d09f968b4d729ee4b29c71fc732e744854f3b` | Pins the shim repository revision. |
| `PYLINT_PYPY_SHIM` | `git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_PYPY_SHIM_REF)` | Identifies the shim package used by `uv tool run`. |
| `PYLINT_BASELINE_DISABLE` | Existing cmd-mox baseline | Temporarily disables legacy Pylint findings while keeping the second tier active. |
| `PYLINT` | `$(UV_ENV) $(UV) tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy --disable=$(PYLINT_BASELINE_DISABLE)` | Builds the full PyPy-backed Pylint command. |
| `SKYLOS_VERSION` | `4.33.2` | Pins the separately provisioned dead-code analyser. |
| `SKYLOS_PRODUCTION_TARGETS` | `cmd_mox` | Limits dead-code liveness analysis to production sources. |

_Table 1: Makefile variables for the lint pipeline._

Expand All @@ -55,8 +64,20 @@ make lint PYLINT_TARGETS=cmd_mox/ipc PYLINT_PYTHON=pypy
```

Do not bypass `make lint` for normal validation. Running the target keeps the
Ruff and Pylint tiers ordered consistently with CI and preserves the shared
`uv` cache configuration.
Ruff, Pylint, and Skylos checks ordered consistently with CI and preserves the
shared `uv` cache configuration.

## Skylos dead-code policy

Treat a Skylos report as genuine dead code until a runtime caller has been
verified. Remove confirmed dead code. For a confirmed false positive that
cannot be represented as an ordinary static reference, add a precise typed
entry-point rule to `[tool.skylos.dead_code.entrypoints]` or a named exception
to `[tool.skylos.whitelist.documented]` in `pyproject.toml`. Every exception
must name its verified runtime caller in its reason. Do not add unexplained
exceptions or use the allow list to avoid a removal. The `--no-grep-verify`
configuration is intentional: test references must not keep production symbols
live in the blocking scan.

## Spelling policy

Expand All @@ -80,6 +101,7 @@ goals:
- use focused Pylint checks for problems that Ruff does not cover as well; and
- run Pylint under PyPy through the shared
[pylint-pypy-shim](https://github.com/leynos/pylint-pypy-shim) approach.
- detect unused production symbols with a local, blocking Skylos scan.

The policy is adapted for CmdMox rather than copied blindly. CmdMox targets
Python 3.12 in `pyproject.toml`, while Episodic targets a newer interpreter.
Expand Down Expand Up @@ -131,6 +153,13 @@ The `Makefile` currently supplies `PYLINT_BASELINE_DISABLE` in addition to the
the desired selected Pylint policy, while the `Makefile` carries the temporary
project baseline required to keep the new tier actionable.

### Skylos tables

- `[tool.skylos.gate]` enables strict failure for unexplained dead-code
findings.
- `[tool.skylos.whitelist.documented]` stores only reasoned false positives;
each entry must identify the verified runtime caller.

## Updating lint policy

When changing lint policy:
Expand Down
45 changes: 45 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,51 @@ also_copy = ["conftest.py", "features/", "docs/"]
[tool.uv]
package = true

[tool.skylos.gate]
strict = true

[[tool.skylos.dead_code.entrypoints]]
type = "method"
full_name = [
"cmd_mox.ipc.server._BaseIPCServer._export_environment",
"cmd_mox.ipc.server.IPCServer._prepare_backend_start",
"cmd_mox.ipc.server.IPCServer._wait_until_ready",
"cmd_mox.ipc.server.IPCServer._stop_backend",
"cmd_mox.ipc.server.IPCServer._post_stop_cleanup",
"cmd_mox.ipc.server.NamedPipeServer._prepare_backend_start",
"cmd_mox.ipc.server.NamedPipeServer._wait_until_ready",
"cmd_mox.ipc.server.NamedPipeServer._stop_backend",
]
reason = "The shared IPC lifecycle invokes transport hooks through dynamically dispatched self calls."

[[tool.skylos.dead_code.entrypoints]]
type = "method"
full_name = [
"cmd_mox.ipc.server.ParsedRequest.validate",
"cmd_mox.ipc.server._NamedPipeState.stop",
"cmd_mox.ipc.server._NamedPipeState._poke_pipe",
]
reason = "The IPC request and named-pipe lifecycle invoke these methods through runtime transport state."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

[[tool.skylos.dead_code.entrypoints]]
type = "variable"
full_name = [
"cmd_mox.environment._Win32Function.argtypes",
"cmd_mox.environment._Win32Function.restype",
]
reason = "ctypes function pointers expose these runtime-configured attributes through the typed protocol."

[[tool.skylos.dead_code.entrypoints]]
type = "parameter"
full_name = ["cmd_mox.ipc.server._ServerLifecycle._stop_backend.server"]
reason = "The abstract lifecycle contract receives the backend instance from its start and stop orchestration."

[tool.skylos.whitelist]
names = ["bootstrap_shim_path"]

[tool.skylos.whitelist.documented]
bootstrap_shim_path = "cmd_mox.shim re-imports this helper after bootstrap before main invokes it."


[build-system]
requires = ["setuptools>=61.0", "wheel"]
Expand Down
93 changes: 93 additions & 0 deletions tests/test_skylos_lint_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Contract tests for the blocking Skylos dead-code lint gate."""

import shutil
import subprocess
import tomllib
import typing as typ
from pathlib import Path

REPOSITORY_ROOT = Path(__file__).resolve().parents[1]


def _pyproject() -> dict[str, object]:
"""Load the repository's Python project configuration."""
return tomllib.loads(
(REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8")
)


def test_skylos_is_a_pinned_external_tool() -> None:
"""Keep Skylos out of the project environment and pin its tool release."""
config = _pyproject()
dependency_groups = typ.cast("dict[str, list[str]]", config["dependency-groups"])

dependencies = dependency_groups["dev"]
assert not any(dependency.startswith("skylos") for dependency in dependencies), (
"Expected Skylos to be separately provisioned from the development "
"dependency group."
)
makefile = (REPOSITORY_ROOT / "Makefile").read_text(encoding="utf-8")
assert "SKYLOS_VERSION = 4.33.2" in makefile, (
"Expected the separately provisioned Skylos tool version to be exact."
)
assert "--from 'skylos==$(SKYLOS_VERSION)' skylos" in makefile, (
"Expected Skylos to run from its separately provisioned tool environment."
)


def test_skylos_configuration_is_strict_and_reasoned() -> None:
"""Require reasons for every configured Skylos exception."""
config = _pyproject()
tool_config = typ.cast("dict[str, object]", config["tool"])
skylos = typ.cast("dict[str, object]", tool_config["skylos"])
whitelist = typ.cast("dict[str, object]", skylos["whitelist"])
documented = typ.cast("dict[str, str]", whitelist["documented"])
assert all(reason.strip() for reason in documented.values()), (
"Expected every documented Skylos whitelist entry to have a reason."
)

dead_code = typ.cast("dict[str, object]", skylos["dead_code"])
entrypoints = typ.cast("list[dict[str, object]]", dead_code["entrypoints"])
assert entrypoints, "Expected verified runtime entry points to be documented."
assert all(
isinstance(reason := entrypoint.get("reason"), str) and reason.strip()
for entrypoint in entrypoints
), "Expected every Skylos dead-code entry point to have a reason."

gate = typ.cast("dict[str, object]", skylos["gate"])
assert gate["strict"] is True, "Expected the Skylos gate to run in strict mode."


def test_make_lint_runs_local_blocking_dead_code_scan() -> None:
"""Keep the Skylos invocation deterministic and production-scoped."""
make_executable = shutil.which("make")
assert make_executable is not None, "Expected make to be available for the test."

result = subprocess.run( # noqa: S603 - test executes make without a shell
[make_executable, "--no-print-directory", "--dry-run", "lint"],
cwd=REPOSITORY_ROOT,
check=False,
capture_output=True,
text=True,
)

assert result.returncode == 0, "Expected make lint dry run to succeed."
skylos_commands = [
line
for line in result.stdout.splitlines()
if "skylos --config-file pyproject.toml" in line
]
assert len(skylos_commands) == 1, (
"Expected make lint to expand exactly one blocking Skylos command."
)
skylos_command = skylos_commands[0]
assert "cmd_mox --category" in skylos_command, (
"Expected the blocking Skylos command to use production-only targets."
)
assert " tests" not in skylos_command, (
"Expected tests to be excluded from the production Skylos graph."
)
assert (
"--category dead_code --gate --format concise --no-upload "
"--no-provenance --no-grep-verify" in skylos_command
), "Expected the blocking Skylos command to retain its gate flags."
2 changes: 1 addition & 1 deletion typos.local.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ schema = 1
stems = []

[words]
accepted = ["Flavored"] # Formal GitHub Flavored Markdown product name.
accepted = ["Flavored", "color"] # Formal product name and literal US API spelling.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

[words.corrections]

Expand Down
Loading
Loading