Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
*~
14 changes: 13 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,19 @@ 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`. Keep
each reason caller-specific; group symbols only when the same runtime
caller or lifecycle reaches all of them.
For a verified false positive, `make skylos-allow NAME=<name>` invokes
Skylos's name-only whitelist subcommand. It accepts no reason, so the
same reviewed change must add or retain the matching
`[tool.skylos.whitelist.documented]` entry with its caller-specific
reason 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
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@ 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_COMMAND = $(UV_ENV) $(UV) tool run --from 'skylos==$(SKYLOS_VERSION)' skylos
SKYLOS = $(SKYLOS_COMMAND) --config-file pyproject.toml
SKYLOS_WHITELIST = $(SKYLOS_COMMAND) whitelist
SKYLOS_PRODUCTION_TARGETS ?= cmd_mox
WINDOWS_SMOKE_ARGS = tests/test_windows_environment.py \
tests/test_windows_support_bdd.py \
--log-file=windows-ipc.log \
--log-file-level=DEBUG \
--log-file-format="%(asctime)s %(levelname)s [%(name)s] %(message)s"

.PHONY: help all clean build build-release lint fmt check-fmt
.PHONY: markdownlint markdownlint-run nixie spelling test typecheck
.PHONY: markdownlint markdownlint-run nixie spelling skylos-allow test typecheck
.PHONY: $(TOOLS) $(VENV_TOOLS)

.DEFAULT_GOAL := all
Expand Down Expand Up @@ -89,8 +94,14 @@ 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

skylos-allow: export SKYLOS_NAME = $(value NAME)
skylos-allow: ## Add one named Skylos whitelist exception
@test -n "$${SKYLOS_NAME}" || { printf "Error: NAME is required for a named whitelist exception\\n" >&2; exit 2; }
$(SKYLOS_WHITELIST) "$${SKYLOS_NAME}"

typecheck: build ## Run typechecking
$(TY) --version
$(TY) check
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
46 changes: 42 additions & 4 deletions docs/adr-001-linting-architecture.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Architectural decision record (ADR) 001: Two-tier linting architecture
# Architectural decision record (ADR) 001: Three-stage linting architecture

## Status

Accepted. CmdMox uses Ruff as the first lint tier and PyPy-backed Pylint as the
second lint tier.
Accepted. CmdMox uses Ruff as the first lint tier, PyPy-backed Pylint as the
second lint tier, and a strict Skylos dead-code scan as the final production
liveness check.

## Date

Expand All @@ -21,6 +22,12 @@ Pylint also needs to run in a way that matches the Episodic approach: through
the `pylint-pypy-shim` repository and under PyPy as a second-tier lint action
after Ruff.

Ruff and Pylint do not model cross-module symbol liveness. CmdMox therefore
also needs a deterministic dead-code check that identifies unused production
symbols without letting test-only references keep them alive. Framework-style
dispatch, ctypes metadata, and the shim bootstrap include verified runtime
surfaces that require narrowly reasoned exceptions rather than a broad baseline.

## Decision drivers

- Keep `make lint` as the single developer entrypoint for lint validation.
Expand All @@ -31,6 +38,9 @@ after Ruff.
- Keep existing CmdMox lint debt visible without forcing unrelated refactors
into the lint architecture change.
- Pin the shim revision so lint execution is reproducible.
- Detect genuine unused production symbols in the standard local and CI gate.
- Keep dead-code exceptions explicit, typed where possible, and reviewable in
`pyproject.toml`.

## Options considered

Expand Down Expand Up @@ -65,18 +75,41 @@ _Table 1: Linting architecture options._

## Decision outcome

CmdMox adopts the Ruff plus PyPy-backed Pylint architecture.
CmdMox adopts a three-stage Ruff, PyPy-backed Pylint, and Skylos architecture.

The `lint` target runs `ruff check` first and then runs Pylint through
`pylint-pypy-shim`. Ruff and Pylint policy are configured in `pyproject.toml`,
while the Makefile defines the executable composition and the temporary
CmdMox-specific Pylint baseline.

The final stage provisions Skylos 4.33.2 separately from the project
environment. It scans only `cmd_mox` with
`--category dead_code --gate
--format concise --no-upload --no-provenance --no-grep-verify`.
The existing Linux CI `make lint` step therefore enforces the same local,
non-interactive production scan. Strict mode fails the gate for unexplained
findings.

Verified runtime entry points are recorded with symbol type, fully qualified
name, and reason under `[tool.skylos.dead_code.entrypoints]`. Exceptions that
cannot describe an entry point are stored in both
`[tool.skylos.whitelist].names` and `[tool.skylos.whitelist.documented]`, with
a caller-specific reason. Symbols are grouped only when the same runtime caller
or lifecycle reaches all of them; separate records describe different callers.
This preserves a narrow, auditable distinction between real dead code and
static-analysis limits.

## Consequences

- Developers continue to run one command: `make lint`.
- Ruff remains the fastest feedback path and blocks before Pylint starts.
- Pylint adds second-tier checks without becoming a separate manual workflow.
- Skylos removes confirmed dead production code and blocks new unexplained
findings locally and in Linux CI.
- Test references do not influence the dead-code graph, and the scan does not
upload code, collect provenance, or invoke cloud analysis.
- Runtime false positives require reasoned, version-controlled entry-point or
whitelist configuration; the contract test protects the reviewed symbols.
- The project carries an explicit baseline for existing findings. This makes
future clean-up incremental rather than hiding the stricter policy.
- The managed PyPy runtime may lag the syntax used by CmdMox. The Pylint
Expand All @@ -90,3 +123,8 @@ CmdMox-specific Pylint baseline.
- Revisit unsupported Ruff selectors when the pinned Ruff version changes.
- Keep `docs/developers-guide.md` synchronized with Makefile and
`pyproject.toml` lint policy changes.
- Review Skylos exceptions whenever the runtime lifecycle, ctypes protocol, or
bootstrap behaviour changes, and remove obsolete entries with the code that
made them unnecessary.
- Update the pinned Skylos release only with a clean production scan and the
complete lint contract test.
3 changes: 2 additions & 1 deletion docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
- [Fake Capabilities Design](./cmd-mox-fake-capabilities-design.md):
Durable fixture writes and reusable helpers for stateful command fakes.
- [Roadmap](./roadmap.md): Planned features and progression.
- [ADR 001](./adr-001-linting-architecture.md): Two-tier linting architecture.
- [ADR 001](./adr-001-linting-architecture.md): Three-stage linting
architecture.
62 changes: 49 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,27 @@ 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 a caller-specific reason. Group
symbols only when the same caller or lifecycle reaches all of them; otherwise,
use separate entries. 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.

For a verified false positive, use `make skylos-allow NAME=<name>` to invoke
Skylos's name-only whitelist subcommand. The subcommand accepts the symbol name
but no reason. Treat its output as a candidate only: in the same reviewed
change, add or retain the matching `[tool.skylos.whitelist.documented]` entry
in `pyproject.toml` with a caller-specific reason.

## Spelling policy

Expand All @@ -80,6 +108,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 +160,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
Loading
Loading