Skip to content
Merged
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/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ and explain why.

<!-- Run `bash scripts/validate.sh` locally. Paste the summary line. -->

- [ ] **1. Lint** — ruff clean on every modified file
- [ ] **1. Lint** — ruff check + format --check clean on every modified file
- [ ] **2. shared.py uniqueness** — no symbol duplicated outside shared.py
- [ ] **3. Encoding sweep** — no non-ASCII in Python source
- [ ] **4. Plugin registry** — every entry has start()/stop()
Expand Down
10 changes: 0 additions & 10 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,16 +353,6 @@ The phases above are not arbitrary. Specifically:
- Phase 5 before Phase 6: ensemble estimation defines what "the profile" is,
and export is downstream of that.

### validate.sh: extend item #1 to include ruff format check

**WHAT:** validate.sh item #1 currently runs `ruff check` only.
CI runs both `ruff check` and `ruff format --check`. Add the latter
to validate.sh so the local gate matches CI.

**WHY:** Drift between local and CI gates means a 10/10 local pass
can still fail CI. Caught during Phase 1.1 rollout — surfaced 9 files
of pre-existing format drift.

### validate.sh: extend item #3 to catch non-ASCII in string literals

**WHAT:** Item #3 currently checks for non-ASCII in Python source but
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Tooling
- Tooling: validate.sh item #1 now runs `ruff format --check` in addition
to `ruff check`, matching .github/workflows/lint.yml exactly. Closes the
local-vs-CI gate parity gap caught during Phase 1.1 and 1.2 rollouts.
New `tests/unit/test_validate_script_lint_parity.py` regression-guards
the invariant going forward. Tags: Tooling, QC.


**Tags:** Feature

Phase 1.2 ships: design diagnostics. Closes BACKLOG 1.2. Surfaces a new
Expand Down
2 changes: 1 addition & 1 deletion PROJECT_KNOWLEDGE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ Purpose:

Run via: bash scripts/validate.sh

1. Lint ruff clean on src/ and tests/
1. Lint ruff check + format --check clean on src/ and tests/
2. shared.py uniqueness no symbol redefined outside shared.py
3. Encoding sweep no non-ASCII in Python source
4. Plugin registry every entry has start()/stop()
Expand Down
6 changes: 2 additions & 4 deletions SESSION_HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,8 @@ know to be efficient:
- **Activate venv first**: a fresh terminal loses the venv. If
`ruff: command not found`, run `source .venv/Scripts/activate`.
- **Validation gate**: never push without `bash scripts/validate.sh`
returning 10/10. NOTE: validate.sh item #1 currently only runs
`ruff check`, not `ruff format --check`. Until that gap is fixed
(Option A above), CI may still reject what locally looks clean.
Run `ruff format src tests` before pushing as a safety belt.
returning 10/10. Item #1 now runs both `ruff check` and
`ruff format --check`, matching CI exactly.
- **Branch protection**: `main` cannot be force-pushed. Bug fixes
that need history rewrites require temporarily relaxing protection.
- **CRLF warnings**: harmless. `.gitattributes` is configured to
Expand Down
15 changes: 12 additions & 3 deletions scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,18 @@ run_check () {
}

# -----------------------------------------------------------------------------
# 1. Lint — every modified Python file passes ruff
# -----------------------------------------------------------------------------
run_check "1. Lint (ruff)" "$VENV_PYTHON" -m ruff check src tests
# 1. Lint — ruff check AND ruff format --check on src/ and tests/
# -----------------------------------------------------------------------------
# CI runs both (.github/workflows/lint.yml). This local gate must mirror
# that exactly. Running only `ruff check` here let format drift ship to
# CI twice during Phase 1.1 + 1.2 rollouts. Both must pass for item #1.
# Regression guarded by tests/unit/test_validate_script_lint_parity.py.
lint_check () {
"$VENV_PYTHON" -m ruff check src tests || return 1
"$VENV_PYTHON" -m ruff format --check src tests || return 1
return 0
}
run_check "1. Lint (ruff check + format)" lint_check

# -----------------------------------------------------------------------------
# 2. Duplication check — no module redefines anything in shared.py
Expand Down
109 changes: 109 additions & 0 deletions tests/unit/test_validate_script_lint_parity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Inherent diagnostic test: validate.sh and CI lint workflow stay in sync.

Why this test exists
--------------------
During Phase 1.1 and 1.2 rollouts, two PRs passed `bash scripts/validate.sh`
locally with 10 / 10 but were rejected by CI because the local gate did not
run `ruff format --check` while CI did. Per ARCHITECTURE_TENETS Tenet 5
("priority QC over speed") and the kAI rollout discipline, the local
validation gate must be a strict superset of what CI enforces, so a clean
local run is sufficient confidence to push.

This test guards the invariant going forward. If somebody removes a ruff
step from `scripts/validate.sh` without removing the matching one in CI
(or vice versa), this test fires with a message explaining the parity
contract.

What this test checks
---------------------
1. `scripts/validate.sh` invokes `ruff check src tests`.
2. `scripts/validate.sh` invokes `ruff format --check src tests`.
3. `.github/workflows/lint.yml` invokes `ruff check src tests`.
4. `.github/workflows/lint.yml` invokes `ruff format --check src tests`.

What this test deliberately does NOT check
------------------------------------------
- The exact Python invocation prefix (e.g. `$VENV_PYTHON -m`). The
validate.sh form differs from CI which uses a system ruff install;
that is intentional and orthogonal to lint parity.
- Other CI workflows (ci.yml, secret-scan.yml). Each workflow that has
a local mirror should grow its own parity test as the project adds
them. Captured in BACKLOG as a generalization opportunity.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from kai.shared import REPO_ROOT

VALIDATE_SH = REPO_ROOT / "scripts" / "validate.sh"
LINT_YML = REPO_ROOT / ".github" / "workflows" / "lint.yml"

REQUIRED_INVOCATIONS = (
"ruff check src tests",
"ruff format --check src tests",
)


def _read(path: Path) -> str:
if not path.exists():
pytest.fail(
f"Expected file does not exist: {path}. "
f"This test asserts the kAI repo layout invariant."
)
return path.read_text(encoding="utf-8")


@pytest.mark.parametrize("invocation", REQUIRED_INVOCATIONS)
def test_validate_sh_runs_ruff_invocation(invocation: str) -> None:
"""validate.sh item #1 must run both ruff check and ruff format --check.

If this fails after a refactor of validate.sh, you almost certainly
want to put the missing invocation back. The local gate is supposed
to be a strict superset of CI - a clean local 10 / 10 must imply
a clean CI lint job.
"""
content = _read(VALIDATE_SH)
assert invocation in content, (
f"scripts/validate.sh is missing required invocation: {invocation!r}. "
f"This invariant exists because CI runs this exact command in "
f".github/workflows/lint.yml. If the local gate stops running it, "
f"local validation no longer protects against CI failure. "
f"If you intentionally removed this from validate.sh, also remove "
f"the matching step from lint.yml AND update this test."
)


@pytest.mark.parametrize("invocation", REQUIRED_INVOCATIONS)
def test_lint_yml_runs_ruff_invocation(invocation: str) -> None:
"""CI lint workflow must run both ruff check and ruff format --check.

The same invariant as the validate.sh test, mirrored on the CI side.
Together the two parametrized tests assert: every required ruff
invocation appears in BOTH places.
"""
content = _read(LINT_YML)
assert invocation in content, (
f".github/workflows/lint.yml is missing required invocation: "
f"{invocation!r}. If you intentionally removed this from CI, also "
f"remove the matching step from scripts/validate.sh AND update "
f"this test."
)


def test_validate_sh_and_lint_yml_target_same_paths() -> None:
"""Both gates must lint the same source paths (`src` and `tests`).

Catches a subtle drift mode where one gate is updated to add or
drop a path (e.g. someone adds `scripts/` to CI but not to local).
The check is intentionally narrow - just confirms both files
mention `src tests` in their ruff invocations.
"""
validate_text = _read(VALIDATE_SH)
lint_text = _read(LINT_YML)
for path_phrase in ("ruff check src tests", "ruff format --check src tests"):
assert path_phrase in validate_text, f"validate.sh missing path phrase: {path_phrase!r}"
assert path_phrase in lint_text, f"lint.yml missing path phrase: {path_phrase!r}"
Loading