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
30 changes: 28 additions & 2 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Pre-commit hook: L0-L4 validation + BOM stripping.
"""Pre-commit hook: local validation + BOM stripping.

This hook ensures that:
1. UTF-8 BOM is stripped from staged files (PowerShell cleanup)
Expand Down Expand Up @@ -143,7 +143,7 @@ def main() -> int:
print("")
return 1

# Run validation orchestrator (L1a → L1b → L2 → L3L4) on staged
# Run validation orchestrator (L1a → L1b → L2 → L4L4h) on staged
# content files only. This matches CI's scope (regions/*.md changed in
# the diff) so legacy files in untouched paths don't block unrelated
# commits while migrations are in flight.
Expand All @@ -167,6 +167,32 @@ def main() -> int:
print("")
return 1

# Local parity for link integrity: run the same in-repo link test on
# the staged markdown scope before push.
result = subprocess.run(
[
sys.executable,
"-m",
"pytest",
"-q",
"tests/test_links.py",
"--khai-roots=engine",
f"--khai-files={' '.join(staged_content)}",
],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print(">>> LINK VALIDATION FAILED")
print("")
if result.stdout.strip():
print(result.stdout.strip())
if result.stderr.strip():
print(result.stderr.strip())
print("")
return 1

# On culture branches: also run Hofstede validation
# Culture content must maintain declared dimension alignment (±10 gap tolerance)
if branch_kind == "culture":
Expand Down
2 changes: 1 addition & 1 deletion .validation-stamp
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d529a122b99e675163f3f89b085f0e4436257649
4c386089d3105e47b69726597d521f5c911c5deb
1 change: 1 addition & 0 deletions tests/branch_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@

GOVERNANCE_GLOB_PATTERNS = (
"tests/branch_scope.py",
"tests/conftest.py",
"tests/test_*.py",
"tests/validate_*.py",
"tests/requirements.txt",
Expand Down
48 changes: 48 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Shared pytest hooks for local reproducibility hints.

This keeps local repro guidance centralized instead of embedding custom
messages in individual test cases.
"""
from __future__ import annotations

from pathlib import Path


def _nodeid_to_local_cmd(nodeid: str) -> str:
"""Build a generic local repro command from a pytest nodeid."""
parts = nodeid.split("::")
rel_file = parts[0]
test_name = parts[1] if len(parts) > 1 else ""
# Drop parametrization suffix for a stable -k expression.
test_name = test_name.split("[", 1)[0]

if test_name:
return f'python -m pytest -q {rel_file} -k "{test_name}"'
return f"python -m pytest -q {rel_file}"


def pytest_terminal_summary(terminalreporter, exitstatus, config): # type: ignore[no-untyped-def]
"""Emit one local repro command per failed test at the end of the run."""
failed = terminalreporter.stats.get("failed", [])
if not failed:
return

tr = terminalreporter
tr.write_sep("-", "Local Repro Hints")

seen: set[str] = set()
for report in failed:
nodeid = getattr(report, "nodeid", "")
if not nodeid or nodeid in seen:
continue
seen.add(nodeid)

cmd = _nodeid_to_local_cmd(nodeid)
tr.write_line(f"{nodeid}")
tr.write_line(f" Local repro: {cmd}")

# PR-scoped orphan checks require --khai-files context.
if "tests/test_links.py::test_no_orphans" in nodeid:
tr.write_line(
' Note: add --khai-files="<space-separated regions/*.md paths>"'
)
1 change: 1 addition & 0 deletions tests/test_branch_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ def test_governance_dir_prefixes_locked():
def test_governance_glob_patterns_locked():
assert GOVERNANCE_GLOB_PATTERNS == (
"tests/branch_scope.py",
"tests/conftest.py",
"tests/test_*.py",
"tests/validate_*.py",
"tests/requirements.txt",
Expand Down
Loading