claude-dev-env 2.36.0 → 4.0.0

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.
Files changed (66) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +11 -11
  3. package/_shared/advisor/advisor-protocol.md +51 -51
  4. package/_shared/advisor/reference/advisor-block.md +9 -9
  5. package/_shared/advisor/reference/astra-rung.md +39 -0
  6. package/_shared/advisor/reference/identity.md +4 -4
  7. package/_shared/advisor/reference/lifecycle.md +6 -6
  8. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  9. package/_shared/advisor/reference/third-party-bind.md +11 -11
  10. package/_shared/advisor/reference/warm-up.md +13 -13
  11. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  12. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  13. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  17. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  18. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  19. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  20. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  21. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  22. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  23. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  24. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  25. package/docs/references/team-advisor-skill.md +3 -3
  26. package/hooks/validators/README.md +35 -1
  27. package/package.json +1 -1
  28. package/scripts/check.ps1 +18 -6
  29. package/scripts/policy_lint/adapter_detectors.py +42 -2
  30. package/scripts/policy_lint/adapters.py +24 -0
  31. package/scripts/policy_lint/config/constants.py +11 -0
  32. package/scripts/policy_lint/engine.py +1 -1
  33. package/scripts/policy_lint/model.py +1 -0
  34. package/scripts/policy_lint/registry.py +10 -1
  35. package/scripts/repository_checks/__init__.py +1 -0
  36. package/scripts/repository_checks/claude_md.py +108 -0
  37. package/scripts/repository_checks/config/__init__.py +1 -0
  38. package/scripts/repository_checks/config/constants.py +150 -0
  39. package/scripts/repository_checks/env_var_documentation.py +243 -0
  40. package/scripts/repository_checks/hook_modules.py +34 -0
  41. package/scripts/repository_checks/models.py +37 -0
  42. package/scripts/repository_checks/package_inventory.py +118 -0
  43. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  44. package/scripts/repository_checks/reporting.py +39 -0
  45. package/scripts/repository_checks/runner.py +93 -0
  46. package/scripts/repository_checks/tracked_secrets.py +147 -0
  47. package/scripts/repository_checks/tracked_tree.py +28 -0
  48. package/scripts/repository_policy.py +120 -0
  49. package/scripts/tests/repository_policy_test_support.py +89 -0
  50. package/scripts/tests/test_cde_lint.py +25 -1
  51. package/scripts/tests/test_claude_md.py +68 -0
  52. package/scripts/tests/test_env_var_documentation.py +133 -0
  53. package/scripts/tests/test_hook_modules.py +17 -0
  54. package/scripts/tests/test_models.py +26 -0
  55. package/scripts/tests/test_package_inventory.py +96 -0
  56. package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
  57. package/scripts/tests/test_pytest_testpaths.py +66 -0
  58. package/scripts/tests/test_reporting.py +54 -0
  59. package/scripts/tests/test_repository_policy.py +71 -0
  60. package/scripts/tests/test_runner.py +59 -0
  61. package/scripts/tests/test_tracked_secrets.py +175 -0
  62. package/scripts/tests/test_tracked_tree.py +28 -0
  63. package/_shared/advisor/reference/sol-rung.md +0 -39
  64. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  65. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  66. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
@@ -0,0 +1,89 @@
1
+ """Shared fixtures for repository-policy behavior tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
13
+ UTF8_ENCODING = "utf-8"
14
+
15
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
16
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
17
+
18
+ import repository_policy
19
+
20
+
21
+ def initialize_repository(repository_root: Path) -> None:
22
+ repository_root.mkdir(parents=True, exist_ok=True)
23
+ run_git(repository_root, ("init", "--initial-branch=main"))
24
+
25
+
26
+ def commit_tracked_files(repository_root: Path) -> None:
27
+ run_git(repository_root, ("add", "-A"))
28
+ run_git(repository_root, ("commit", "--quiet", "-m", "seed"))
29
+
30
+
31
+ def write_text(file_path: Path, content: str) -> None:
32
+ file_path.parent.mkdir(parents=True, exist_ok=True)
33
+ file_path.write_text(content, encoding=UTF8_ENCODING)
34
+
35
+
36
+ def patch_unreadable_named_file(
37
+ monkeypatch: pytest.MonkeyPatch, filename: str, error_text: str
38
+ ) -> None:
39
+ original_read_text = Path.read_text
40
+
41
+ def refuse_named_file(
42
+ file_path: Path,
43
+ encoding: str | None = None,
44
+ errors: str | None = None,
45
+ ) -> str:
46
+ if file_path.name == filename:
47
+ raise OSError(error_text)
48
+ return original_read_text(file_path, encoding=encoding, errors=errors)
49
+
50
+ monkeypatch.setattr(Path, "read_text", refuse_named_file)
51
+
52
+
53
+ def seed_clean_repository(repository_root: Path) -> Path:
54
+ initialize_repository(repository_root)
55
+ write_text(repository_root / "README.md", "clean tree\n")
56
+ commit_tracked_files(repository_root)
57
+ return repository_root
58
+
59
+
60
+ def run_policy(
61
+ repository_root: Path, all_arguments: list[str] | None = None
62
+ ) -> tuple[int, str, str]:
63
+ stdout = io.StringIO()
64
+ stderr = io.StringIO()
65
+ exit_code = repository_policy.main(
66
+ all_arguments or [],
67
+ repository_root=repository_root,
68
+ stdout=stdout,
69
+ stderr=stderr,
70
+ )
71
+ return exit_code, stdout.getvalue(), stderr.getvalue()
72
+
73
+
74
+ def run_git(repository_root: Path, all_arguments: tuple[str, ...]) -> None:
75
+ subprocess.run(
76
+ (
77
+ "git",
78
+ "-c",
79
+ "user.email=ci@example.com",
80
+ "-c",
81
+ "user.name=CI",
82
+ "-c",
83
+ "core.hooksPath=",
84
+ *all_arguments,
85
+ ),
86
+ cwd=repository_root,
87
+ check=True,
88
+ capture_output=True,
89
+ )
@@ -304,7 +304,9 @@ def test_staged_clean_reports_keep_legacy_advisories_outside_renderer(
304
304
 
305
305
  assert json_exit_code == 0
306
306
  assert json_stderr == ""
307
- assert json.loads(json_stdout)["diagnostics"] == []
307
+ json_report = json.loads(json_stdout)
308
+ assert json_report["diagnostics"] == []
309
+ assert "terminology-sweep" in json_report["executed_rules"]
308
310
  assert text_exit_code == 0
309
311
  assert text_stdout == ""
310
312
  assert text_stderr == ""
@@ -313,6 +315,28 @@ def test_staged_clean_reports_keep_legacy_advisories_outside_renderer(
313
315
  assert editor_stderr == ""
314
316
 
315
317
 
318
+ def test_staged_git_failure_exits_three_with_valid_json(
319
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
320
+ ) -> None:
321
+ _stage_oversized_test_file(tmp_path)
322
+ adapter_module = sys.modules["policy_lint.adapters"]
323
+ terminology_module = adapter_module._pr_loop_script_module("terminology_sweep")
324
+
325
+ def fail_git(
326
+ _repository_root: Path, _all_arguments: object
327
+ ) -> subprocess.CompletedProcess[str]:
328
+ raise RuntimeError("simulated Git failure")
329
+
330
+ monkeypatch.setattr(terminology_module, "_run_strict_git", fail_git)
331
+ exit_code, stdout_text, stderr_text = _run_staged_format("json", tmp_path)
332
+ parsed_report = json.loads(stdout_text)
333
+
334
+ assert exit_code == 3
335
+ assert stderr_text == ""
336
+ assert parsed_report["diagnostics"] == []
337
+ assert parsed_report["failed_rules"] == ["terminology-sweep"]
338
+
339
+
316
340
  def test_should_exit_one_when_diagnostics_exist(tmp_path: Path) -> None:
317
341
  exit_code, stdout_text, _stderr_text = _run_cli(
318
342
  ["--files", "src/app.py"],
@@ -0,0 +1,68 @@
1
+ """Behavior tests for committed CLAUDE.md references."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Iterator
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _TESTS_DIRECTORY = Path(__file__).resolve().parent
12
+ if str(_TESTS_DIRECTORY) not in sys.path:
13
+ sys.path.insert(0, str(_TESTS_DIRECTORY))
14
+
15
+ from repository_checks.config.constants import (
16
+ CHECK_ID_CLAUDE_MD_ORPHANS,
17
+ FAILED_CHECK_EXIT_CODE,
18
+ FINDINGS_EXIT_CODE,
19
+ )
20
+ from repository_policy_test_support import (
21
+ commit_tracked_files,
22
+ initialize_repository,
23
+ run_policy,
24
+ write_text,
25
+ )
26
+
27
+
28
+ def test_should_flag_a_broken_claude_md_file_reference(tmp_path: Path) -> None:
29
+ repository_root = tmp_path / "repo"
30
+ initialize_repository(repository_root)
31
+ write_text(
32
+ repository_root / "notes" / "CLAUDE.md",
33
+ "# notes\n\n"
34
+ "| File | Role |\n"
35
+ "|---|---|\n"
36
+ "| `absent_helper.py` | Missing helper |\n",
37
+ )
38
+ commit_tracked_files(repository_root)
39
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
40
+ assert exit_code == FINDINGS_EXIT_CODE
41
+ assert CHECK_ID_CLAUDE_MD_ORPHANS in stdout_text
42
+ assert "notes/CLAUDE.md" in stdout_text
43
+ assert "absent_helper.py" in stdout_text
44
+
45
+
46
+ def test_should_fail_closed_when_claude_md_walk_fails_after_first_entry(
47
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
48
+ ) -> None:
49
+ repository_root = tmp_path / "repo"
50
+ initialize_repository(repository_root)
51
+ write_text(
52
+ repository_root / "notes" / "CLAUDE.md",
53
+ "# notes\n\n"
54
+ "| File | Role |\n"
55
+ "|---|---|\n"
56
+ "| `absent_helper.py` | Missing helper |\n",
57
+ )
58
+ commit_tracked_files(repository_root)
59
+
60
+ def fail_after_first_entry(_file_path: Path, _pattern: str = "*") -> Iterator[Path]:
61
+ yield repository_root / "notes" / "CLAUDE.md"
62
+ raise OSError("scan root unreadable")
63
+
64
+ monkeypatch.setattr(Path, "rglob", fail_after_first_entry)
65
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
66
+ assert exit_code == FAILED_CHECK_EXIT_CODE
67
+ assert CHECK_ID_CLAUDE_MD_ORPHANS in stdout_text
68
+ assert "error: rule failed:" in stdout_text
@@ -0,0 +1,133 @@
1
+ """Behavior tests for environment-variable documentation checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _TESTS_DIRECTORY = Path(__file__).resolve().parent
11
+ if str(_TESTS_DIRECTORY) not in sys.path:
12
+ sys.path.insert(0, str(_TESTS_DIRECTORY))
13
+
14
+ from repository_checks.config.constants import (
15
+ CHECK_ID_ENV_VAR_DOCUMENTATION,
16
+ FAILED_CHECK_EXIT_CODE,
17
+ FINDINGS_EXIT_CODE,
18
+ SUCCESS_EXIT_CODE,
19
+ )
20
+ from repository_policy_test_support import (
21
+ commit_tracked_files,
22
+ initialize_repository,
23
+ patch_unreadable_named_file,
24
+ run_policy,
25
+ write_text,
26
+ )
27
+
28
+
29
+ def test_should_flag_environment_variable_documentation_drift(tmp_path: Path) -> None:
30
+ repository_root = tmp_path / "repo"
31
+ initialize_repository(repository_root)
32
+ write_text(
33
+ repository_root / "auth" / "google_auth.py", "def load() -> None:\n return\n"
34
+ )
35
+ write_text(
36
+ repository_root / "docs" / "configuration.md",
37
+ "## Summary: Environment Variables\n\n"
38
+ "| Variable | Used By | Purpose |\n"
39
+ "|----------|---------|---------|\n"
40
+ "| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | Path to JSON |\n",
41
+ )
42
+ commit_tracked_files(repository_root)
43
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
44
+ assert exit_code == FINDINGS_EXIT_CODE
45
+ assert CHECK_ID_ENV_VAR_DOCUMENTATION in stdout_text
46
+ assert "docs/configuration.md" in stdout_text
47
+ assert "GOOGLE_APPLICATION_CREDENTIALS -> auth/google_auth.py" in stdout_text
48
+
49
+
50
+ def test_should_ignore_long_code_reference_inside_code_fence(
51
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
52
+ ) -> None:
53
+ repository_root = tmp_path / "repo"
54
+ initialize_repository(repository_root)
55
+ long_code_reference = ("x" * 300) + ".py"
56
+ write_text(
57
+ repository_root / "docs" / "review.md",
58
+ f"# Review\n\n~~~diff\n+ `{long_code_reference}`\n~~~\n",
59
+ )
60
+ commit_tracked_files(repository_root)
61
+ original_is_file = Path.is_file
62
+
63
+ def reject_long_filename(candidate_path: Path) -> bool:
64
+ if len(candidate_path.name) > 255:
65
+ raise OSError(candidate_path)
66
+ return original_is_file(candidate_path)
67
+
68
+ monkeypatch.setattr(Path, "is_file", reject_long_filename)
69
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
70
+ assert exit_code == SUCCESS_EXIT_CODE
71
+ assert CHECK_ID_ENV_VAR_DOCUMENTATION not in stdout_text
72
+
73
+
74
+ def test_should_fail_closed_when_an_env_var_code_file_cannot_be_read(
75
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
76
+ ) -> None:
77
+ repository_root = tmp_path / "repo"
78
+ initialize_repository(repository_root)
79
+ write_text(
80
+ repository_root / "packages" / "service" / "auth" / "google_auth.py",
81
+ "def load() -> None:\n return\n",
82
+ )
83
+ write_text(
84
+ repository_root / "docs" / "configuration.md",
85
+ "## Summary: Environment Variables\n\n"
86
+ "| Variable | Used By | Purpose |\n"
87
+ "|----------|---------|---------|\n"
88
+ "| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | Path to JSON |\n",
89
+ )
90
+ commit_tracked_files(repository_root)
91
+ patch_unreadable_named_file(monkeypatch, "google_auth.py", "code file unreadable")
92
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
93
+ assert exit_code == FAILED_CHECK_EXIT_CODE
94
+ assert CHECK_ID_ENV_VAR_DOCUMENTATION in stdout_text
95
+ assert "error: rule failed:" in stdout_text
96
+
97
+
98
+ def test_should_fail_closed_before_reading_env_var_code_file_outside_repository(
99
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
100
+ ) -> None:
101
+ repository_root = tmp_path / "repo"
102
+ initialize_repository(repository_root)
103
+ outside_path = tmp_path / "outside.py"
104
+ write_text(outside_path, "def load() -> None:\n return\n")
105
+ write_text(
106
+ repository_root / "docs" / "configuration.md",
107
+ "## Summary: Environment Variables\n\n"
108
+ "| Variable | Used By | Purpose |\n"
109
+ "|----------|---------|---------|\n"
110
+ "| `EXTERNAL_TOKEN` | `../outside.py` | External consumer |\n",
111
+ )
112
+ commit_tracked_files(repository_root)
113
+ all_read_paths = record_read_paths(monkeypatch)
114
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
115
+ assert exit_code == FAILED_CHECK_EXIT_CODE
116
+ assert CHECK_ID_ENV_VAR_DOCUMENTATION in stdout_text
117
+ assert outside_path not in all_read_paths
118
+
119
+
120
+ def record_read_paths(monkeypatch: pytest.MonkeyPatch) -> list[Path]:
121
+ original_read_text = Path.read_text
122
+ all_read_paths: list[Path] = []
123
+
124
+ def record_read_path(
125
+ file_path: Path,
126
+ encoding: str | None = None,
127
+ errors: str | None = None,
128
+ ) -> str:
129
+ all_read_paths.append(file_path.resolve())
130
+ return original_read_text(file_path, encoding=encoding, errors=errors)
131
+
132
+ monkeypatch.setattr(Path, "read_text", record_read_path)
133
+ return all_read_paths
@@ -0,0 +1,17 @@
1
+ """Behavior tests for repository-check hook module loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
9
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
10
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
11
+
12
+ from repository_checks.hook_modules import load_hooks_module
13
+
14
+
15
+ def test_should_load_a_hooks_module_by_its_dotted_name() -> None:
16
+ loaded_module = load_hooks_module("hooks_constants.pii_prevention_constants")
17
+ assert loaded_module.__name__ == "hooks_constants.pii_prevention_constants"
@@ -0,0 +1,26 @@
1
+ """Behavior tests for repository-check report models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
9
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
10
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
11
+
12
+ from repository_checks.config.constants import (
13
+ FAILED_CHECK_EXIT_CODE,
14
+ FINDINGS_EXIT_CODE,
15
+ SUCCESS_EXIT_CODE,
16
+ )
17
+ from repository_checks.models import RepositoryCheckReport, RepositoryFinding
18
+
19
+
20
+ def test_should_choose_exit_code_by_report_severity() -> None:
21
+ finding = RepositoryFinding("check", "file.py", "message")
22
+ assert RepositoryCheckReport((), ()).exit_code == SUCCESS_EXIT_CODE
23
+ assert RepositoryCheckReport((finding,), ()).exit_code == FINDINGS_EXIT_CODE
24
+ assert RepositoryCheckReport((finding,), ("check",)).exit_code == (
25
+ FAILED_CHECK_EXIT_CODE
26
+ )
@@ -0,0 +1,96 @@
1
+ """Behavior tests for committed package inventories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _TESTS_DIRECTORY = Path(__file__).resolve().parent
11
+ if str(_TESTS_DIRECTORY) not in sys.path:
12
+ sys.path.insert(0, str(_TESTS_DIRECTORY))
13
+
14
+ from repository_checks.config.constants import (
15
+ CHECK_ID_PACKAGE_INVENTORY,
16
+ FAILED_CHECK_EXIT_CODE,
17
+ FINDINGS_EXIT_CODE,
18
+ )
19
+ from repository_policy_test_support import (
20
+ commit_tracked_files,
21
+ initialize_repository,
22
+ patch_unreadable_named_file,
23
+ run_policy,
24
+ write_text,
25
+ )
26
+
27
+ _SKILL_INVENTORY_MARKDOWN = (
28
+ "# Skill\n\n"
29
+ "| File | Role |\n"
30
+ "|---|---|\n"
31
+ "| `listed_alpha.py` | Alpha helper |\n"
32
+ "| `listed_beta.py` | Beta helper |\n"
33
+ )
34
+
35
+
36
+ def test_should_flag_a_stale_package_inventory(tmp_path: Path) -> None:
37
+ repository_root = tmp_path / "repo"
38
+ initialize_repository(repository_root)
39
+ package_directory = repository_root / "pipeline"
40
+ _write_package_inventory(package_directory)
41
+ commit_tracked_files(repository_root)
42
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
43
+ assert exit_code == FINDINGS_EXIT_CODE
44
+ assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
45
+ assert "pipeline/check_dialer_seam_cli.py" in stdout_text
46
+
47
+
48
+ def test_should_check_active_skills_and_exclude_archived_skills(tmp_path: Path) -> None:
49
+ repository_root = tmp_path / "repo"
50
+ initialize_repository(repository_root)
51
+ _seed_skill_package(repository_root / ".agents" / "skills" / "live-skill")
52
+ _seed_skill_package(repository_root / ".agents" / "skills-archived" / "old-skill")
53
+ commit_tracked_files(repository_root)
54
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
55
+ assert exit_code == FINDINGS_EXIT_CODE
56
+ assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
57
+ assert ".agents/skills/live-skill/scripts/unlisted_gamma.py" in stdout_text
58
+ assert (
59
+ ".agents/skills-archived/old-skill/scripts/unlisted_gamma.py" not in stdout_text
60
+ )
61
+
62
+
63
+ def test_should_fail_closed_when_a_package_inventory_document_cannot_be_read(
64
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
65
+ ) -> None:
66
+ repository_root = tmp_path / "repo"
67
+ initialize_repository(repository_root)
68
+ _write_package_inventory(repository_root / "pipeline")
69
+ commit_tracked_files(repository_root)
70
+ patch_unreadable_named_file(monkeypatch, "README.md", "inventory unreadable")
71
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
72
+ assert exit_code == FAILED_CHECK_EXIT_CODE
73
+ assert CHECK_ID_PACKAGE_INVENTORY in stdout_text
74
+ assert "error: rule failed:" in stdout_text
75
+
76
+
77
+ def _write_package_inventory(package_directory: Path) -> None:
78
+ write_text(
79
+ package_directory / "README.md",
80
+ "# Pipeline\n\n"
81
+ "| Path | Role |\n"
82
+ "|---|---|\n"
83
+ "| `dialer_compose.py` | Composes a dialer strip. |\n"
84
+ "| `compose_dialer_cli.py` | CLI for the dialer strip. |\n",
85
+ )
86
+ write_text(package_directory / "dialer_compose.py", "x = 1\n")
87
+ write_text(package_directory / "compose_dialer_cli.py", "x = 1\n")
88
+ write_text(package_directory / "check_dialer_seam_cli.py", "x = 1\n")
89
+
90
+
91
+ def _seed_skill_package(skill_root: Path) -> None:
92
+ write_text(skill_root / "SKILL.md", _SKILL_INVENTORY_MARKDOWN)
93
+ scripts_directory = skill_root / "scripts"
94
+ write_text(scripts_directory / "listed_alpha.py", "x = 1\n")
95
+ write_text(scripts_directory / "listed_beta.py", "x = 1\n")
96
+ write_text(scripts_directory / "unlisted_gamma.py", "x = 1\n")
@@ -11,6 +11,7 @@ import pytest
11
11
  from policy_lint import adapters, registry
12
12
  from policy_lint.engine import lint
13
13
  from policy_lint.model import (
14
+ ChangeSetRule,
14
15
  Document,
15
16
  DocumentRule,
16
17
  DocumentSet,
@@ -41,6 +42,60 @@ def _ensure_git_repository(repository_root: Path) -> None:
41
42
  )
42
43
 
43
44
 
45
+ def _commit_repository(repository_root: Path, message: str) -> None:
46
+ subprocess.run(
47
+ ("git", "config", "user.email", "test@example.com"),
48
+ cwd=repository_root,
49
+ check=True,
50
+ capture_output=True,
51
+ )
52
+ subprocess.run(
53
+ ("git", "config", "user.name", "Test"),
54
+ cwd=repository_root,
55
+ check=True,
56
+ capture_output=True,
57
+ )
58
+ subprocess.run(
59
+ ("git", "add", "-A"), cwd=repository_root, check=True, capture_output=True
60
+ )
61
+ subprocess.run(
62
+ ("git", "commit", "-m", message),
63
+ cwd=repository_root,
64
+ check=True,
65
+ capture_output=True,
66
+ )
67
+
68
+
69
+ def _stage_terminology_change(
70
+ repository_root: Path,
71
+ base_source: str,
72
+ current_source: str,
73
+ documentation_text: str,
74
+ ) -> None:
75
+ _ensure_git_repository(repository_root)
76
+ source_path = repository_root / "src" / "quota.py"
77
+ source_path.parent.mkdir()
78
+ source_path.write_text(base_source, encoding="utf-8")
79
+ _commit_repository(repository_root, "base")
80
+ source_path.write_text(current_source, encoding="utf-8")
81
+ documentation_path = repository_root / "docs" / "README.md"
82
+ documentation_path.parent.mkdir()
83
+ documentation_path.write_text(documentation_text, encoding="utf-8")
84
+ subprocess.run(
85
+ ("git", "add", "-A"), cwd=repository_root, check=True, capture_output=True
86
+ )
87
+
88
+
89
+ def _terminology_rule() -> ChangeSetRule:
90
+ terminology_rule = next(
91
+ each_rule
92
+ for each_rule in registry.default_registry()
93
+ if each_rule.rule_id == "terminology-sweep"
94
+ )
95
+ assert isinstance(terminology_rule, ChangeSetRule)
96
+ return terminology_rule
97
+
98
+
44
99
  def test_hook_format_accepts_claude_settings() -> None:
45
100
  settings_document = Document.from_text(".claude/settings.json", "{}")
46
101
  hooks_document = Document.from_text("hooks/hooks.json", "{}")
@@ -233,6 +288,68 @@ def test_registry_registers_unique_python_validator_rule() -> None:
233
288
  assert not validator_rule.accepts(Document.from_text("module.ts", _FUNCTION_SOURCE))
234
289
 
235
290
 
291
+ def test_registry_registers_terminology_change_set_rule() -> None:
292
+ terminology_rule = _terminology_rule()
293
+ assert terminology_rule.rule_sets == _CHANGED_RULE_SETS
294
+ assert terminology_rule.selections == frozenset({SelectionKind.STAGED})
295
+
296
+
297
+ def test_registry_runs_test_pairing_for_staged_and_base_changes() -> None:
298
+ test_pairing_rule = next(
299
+ each_rule
300
+ for each_rule in registry.default_registry()
301
+ if each_rule.rule_id == "test-pairing"
302
+ )
303
+ assert isinstance(test_pairing_rule, ChangeSetRule)
304
+ assert test_pairing_rule.selections == frozenset(
305
+ {SelectionKind.STAGED, SelectionKind.BASE}
306
+ )
307
+
308
+
309
+ def test_terminology_rule_reports_cross_surface_path_and_line(
310
+ tmp_path: Path,
311
+ ) -> None:
312
+ _stage_terminology_change(
313
+ tmp_path,
314
+ "baseline = 1\n",
315
+ "premium_request_interactions = 5\n",
316
+ "Quota fields use one name.\n"
317
+ "The premium-request-budget field gates the run.\n",
318
+ )
319
+ lint_report = lint(LintRequest.staged(tmp_path), all_registry=(_terminology_rule(),))
320
+
321
+ assert len(lint_report.diagnostics) == 1
322
+ diagnostic = lint_report.diagnostics[0]
323
+ assert diagnostic.rule_id == "terminology-sweep"
324
+ assert diagnostic.location is not None
325
+ assert diagnostic.location.path == PurePosixPath("docs/README.md")
326
+ assert diagnostic.location.start_line == 2
327
+
328
+
329
+ def test_terminology_rule_suppresses_identifier_present_in_prior_tree(
330
+ tmp_path: Path,
331
+ ) -> None:
332
+ _stage_terminology_change(
333
+ tmp_path,
334
+ "premium_request_interactions = 5\n",
335
+ "premium_request_interactions = 5\n"
336
+ "premium_request_interactions = 6\n",
337
+ "The premium-request-budget field gates the run.\n",
338
+ )
339
+ lint_report = lint(LintRequest.staged(tmp_path), all_registry=(_terminology_rule(),))
340
+
341
+ assert lint_report.diagnostics == ()
342
+
343
+
344
+ def test_terminology_rule_skips_file_selection(tmp_path: Path) -> None:
345
+ document_set = DocumentSet(
346
+ (Document.from_text("docs/README.md", "text\n"),),
347
+ SelectionKind.FILES,
348
+ tmp_path,
349
+ )
350
+ assert adapters.terminology_diagnostics(document_set) == ()
351
+
352
+
236
353
  def test_lint_keeps_skipped_rules_out_of_executed_rules(tmp_path: Path) -> None:
237
354
  skipped_rule = DocumentRule(
238
355
  "skipped",
@@ -0,0 +1,66 @@
1
+ """Behavior tests for committed pytest testpaths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _TESTS_DIRECTORY = Path(__file__).resolve().parent
11
+ if str(_TESTS_DIRECTORY) not in sys.path:
12
+ sys.path.insert(0, str(_TESTS_DIRECTORY))
13
+
14
+ from repository_checks.config.constants import (
15
+ CHECK_ID_PYTEST_TESTPATHS,
16
+ FAILED_CHECK_EXIT_CODE,
17
+ FINDINGS_EXIT_CODE,
18
+ )
19
+ from repository_policy_test_support import (
20
+ commit_tracked_files,
21
+ initialize_repository,
22
+ patch_unreadable_named_file,
23
+ run_policy,
24
+ write_text,
25
+ )
26
+
27
+
28
+ def test_should_flag_a_pytest_testpath_discovery_miss(tmp_path: Path) -> None:
29
+ repository_root = tmp_path / "repo"
30
+ initialize_repository(repository_root)
31
+ package_root = repository_root / "shared_utils"
32
+ _write_pytest_configuration(package_root)
33
+ write_text(
34
+ package_root / "theme_assets" / "tests" / "test_palette.py",
35
+ "def test_palette() -> None:\n assert True\n",
36
+ )
37
+ commit_tracked_files(repository_root)
38
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
39
+ assert exit_code == FINDINGS_EXIT_CODE
40
+ assert CHECK_ID_PYTEST_TESTPATHS in stdout_text
41
+ assert "shared_utils/theme_assets/tests/test_palette.py" in stdout_text
42
+
43
+
44
+ def test_should_fail_closed_when_pytest_config_cannot_be_read(
45
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
46
+ ) -> None:
47
+ repository_root = tmp_path / "repo"
48
+ initialize_repository(repository_root)
49
+ _write_pytest_configuration(repository_root / "shared_utils")
50
+ commit_tracked_files(repository_root)
51
+ patch_unreadable_named_file(monkeypatch, "pyproject.toml", "pyproject unreadable")
52
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
53
+ assert exit_code == FAILED_CHECK_EXIT_CODE
54
+ assert CHECK_ID_PYTEST_TESTPATHS in stdout_text
55
+ assert "error: rule failed:" in stdout_text
56
+
57
+
58
+ def _write_pytest_configuration(package_root: Path) -> None:
59
+ write_text(
60
+ package_root / "pyproject.toml",
61
+ '[tool.pytest.ini_options]\ntestpaths = ["tests"]\n',
62
+ )
63
+ write_text(
64
+ package_root / "tests" / "test_root_behavior.py",
65
+ "def test_ok() -> None:\n assert True\n",
66
+ )