claude-dev-env 2.35.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 (99) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/autoconverge/SKILL.md +19 -15
  3. package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
  4. package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
  5. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
  6. package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
  7. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
  8. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
  9. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
  10. package/.agents/skills/team-advisor/SKILL.md +11 -11
  11. package/_shared/advisor/advisor-protocol.md +51 -51
  12. package/_shared/advisor/reference/advisor-block.md +9 -9
  13. package/_shared/advisor/reference/astra-rung.md +39 -0
  14. package/_shared/advisor/reference/identity.md +4 -4
  15. package/_shared/advisor/reference/lifecycle.md +6 -6
  16. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  17. package/_shared/advisor/reference/third-party-bind.md +11 -11
  18. package/_shared/advisor/reference/warm-up.md +13 -13
  19. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  20. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  21. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  22. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  24. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  25. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  26. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  28. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  29. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  30. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  31. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  32. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  33. package/bin/cde.mjs +175 -0
  34. package/bin/cde.test.mjs +222 -0
  35. package/bin/install-constants.mjs +4 -0
  36. package/bin/install.cursor-rules.test.mjs +233 -2
  37. package/bin/install.mjs +49 -3
  38. package/bin/pstack-model-preferences.codex.json +23 -0
  39. package/bin/select-pstack-models.test.mjs +378 -0
  40. package/docs/references/team-advisor-skill.md +3 -3
  41. package/hooks/hooks_constants/tool_names.py +20 -0
  42. package/hooks/validators/README.md +35 -1
  43. package/hooks/validators/fast_save_validators.py +63 -89
  44. package/hooks/validators/run_all_validators.py +16 -8
  45. package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
  46. package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
  47. package/package.json +3 -2
  48. package/rules/pstack-models.md +119 -0
  49. package/scripts/cde_lint.py +258 -0
  50. package/scripts/check.ps1 +18 -6
  51. package/scripts/policy_lint/__init__.py +41 -0
  52. package/scripts/policy_lint/adapter_configuration.py +91 -0
  53. package/scripts/policy_lint/adapter_detectors.py +368 -0
  54. package/scripts/policy_lint/adapter_pairing.py +145 -0
  55. package/scripts/policy_lint/adapter_support.py +97 -0
  56. package/scripts/policy_lint/adapters.py +376 -0
  57. package/scripts/policy_lint/config/__init__.py +1 -0
  58. package/scripts/policy_lint/config/constants.py +77 -0
  59. package/scripts/policy_lint/engine.py +224 -0
  60. package/scripts/policy_lint/model.py +388 -0
  61. package/scripts/policy_lint/registry.py +171 -0
  62. package/scripts/policy_lint/render.py +91 -0
  63. package/scripts/policy_lint/selection.py +372 -0
  64. package/scripts/policy_lint/selection_git.py +147 -0
  65. package/scripts/repository_checks/__init__.py +1 -0
  66. package/scripts/repository_checks/claude_md.py +108 -0
  67. package/scripts/repository_checks/config/__init__.py +1 -0
  68. package/scripts/repository_checks/config/constants.py +150 -0
  69. package/scripts/repository_checks/env_var_documentation.py +243 -0
  70. package/scripts/repository_checks/hook_modules.py +34 -0
  71. package/scripts/repository_checks/models.py +37 -0
  72. package/scripts/repository_checks/package_inventory.py +118 -0
  73. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  74. package/scripts/repository_checks/reporting.py +39 -0
  75. package/scripts/repository_checks/runner.py +93 -0
  76. package/scripts/repository_checks/tracked_secrets.py +147 -0
  77. package/scripts/repository_checks/tracked_tree.py +28 -0
  78. package/scripts/repository_policy.py +120 -0
  79. package/scripts/select_pstack_models.mjs +304 -0
  80. package/scripts/tests/repository_policy_test_support.py +89 -0
  81. package/scripts/tests/test_cde_lint.py +398 -0
  82. package/scripts/tests/test_claude_md.py +68 -0
  83. package/scripts/tests/test_env_var_documentation.py +133 -0
  84. package/scripts/tests/test_hook_modules.py +17 -0
  85. package/scripts/tests/test_models.py +26 -0
  86. package/scripts/tests/test_package_inventory.py +96 -0
  87. package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
  88. package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
  89. package/scripts/tests/test_policy_lint_selection.py +373 -0
  90. package/scripts/tests/test_pytest_testpaths.py +66 -0
  91. package/scripts/tests/test_reporting.py +54 -0
  92. package/scripts/tests/test_repository_policy.py +71 -0
  93. package/scripts/tests/test_runner.py +59 -0
  94. package/scripts/tests/test_tracked_secrets.py +175 -0
  95. package/scripts/tests/test_tracked_tree.py +28 -0
  96. package/_shared/advisor/reference/sol-rung.md +0 -39
  97. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  98. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  99. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
@@ -0,0 +1,150 @@
1
+ """Stable identifiers, report tokens, and committed-tree exemption tables."""
2
+
3
+ from policy_lint.config.constants import (
4
+ GIT_FILES_ARGUMENT,
5
+ GIT_ZERO_TERMINATED_FLAG,
6
+ INCOMPLETE_EXIT_CODE,
7
+ INVALID_INPUT_EXIT_CODE,
8
+ )
9
+
10
+ CHECK_ID_CLAUDE_MD_ORPHANS = "claude-md-orphans"
11
+ CHECK_ID_ENV_VAR_DOCUMENTATION = "env-var-documentation"
12
+ CHECK_ID_PACKAGE_INVENTORY = "package-inventory"
13
+ CHECK_ID_PYTEST_TESTPATHS = "pytest-testpaths"
14
+ CHECK_ID_TRACKED_PERSONAL_DATA = "tracked-secrets"
15
+
16
+ ALL_CHECK_IDS = (
17
+ CHECK_ID_CLAUDE_MD_ORPHANS,
18
+ CHECK_ID_ENV_VAR_DOCUMENTATION,
19
+ CHECK_ID_PACKAGE_INVENTORY,
20
+ CHECK_ID_PYTEST_TESTPATHS,
21
+ CHECK_ID_TRACKED_PERSONAL_DATA,
22
+ )
23
+
24
+ SUCCESS_EXIT_CODE = 0
25
+ FINDINGS_EXIT_CODE = 1
26
+ FAILED_CHECK_EXIT_CODE = INCOMPLETE_EXIT_CODE
27
+ USAGE_EXIT_CODE = INVALID_INPUT_EXIT_CODE
28
+
29
+ REPOSITORY_ROOT_FLAG = "--repository-root"
30
+ FINDING_LINE_TEMPLATE = "{check_id}: {relative_path}: {message}"
31
+ RULE_FAILED_LINE_TEMPLATE = "error: rule failed: {check_id}"
32
+ REPORT_LINE_SEPARATOR = "\n"
33
+ EMPTY_REPORT_TEXT = ""
34
+ TRACKED_MATCH_MESSAGE_TEMPLATE = "[{category}] {preview}"
35
+ CLAUDE_MD_MISSING_FILE_MESSAGE_TEMPLATE = "references missing file {filename}"
36
+ PACKAGE_INVENTORY_MESSAGE_TEMPLATE = "production file is absent from package inventory"
37
+ PYTEST_TESTPATH_MESSAGE_TEMPLATE = (
38
+ "test file is outside the package testpaths allowlist"
39
+ )
40
+ ALL_GIT_LS_FILES_ARGUMENTS = (GIT_FILES_ARGUMENT, GIT_ZERO_TERMINATED_FLAG)
41
+ PYPROJECT_FILENAME = "pyproject.toml"
42
+ WINDOWS_PATH_SEPARATOR = "\\"
43
+
44
+ CLAUDE_MD_SCAN_MODULE_NAME = "blocking.claude_md_orphan_file_blocker_parts.subtree_scan"
45
+ CLAUDE_MD_CONSTANTS_MODULE_NAME = (
46
+ "hooks_constants.claude_md_orphan_file_blocker_constants"
47
+ )
48
+ ENV_VAR_DRIFT_MODULE_NAME = "blocking.env_var_table_code_drift_blocker"
49
+ ENV_VAR_DRIFT_CONSTANTS_MODULE_NAME = (
50
+ "hooks_constants.env_var_table_code_drift_constants"
51
+ )
52
+ PACKAGE_INVENTORY_DETECTION_MODULE_NAME = (
53
+ "blocking.package_inventory_stale_blocker_parts.inventory_detection"
54
+ )
55
+ PACKAGE_INVENTORY_CONSTANTS_MODULE_NAME = (
56
+ "hooks_constants.package_inventory_stale_blocker_constants"
57
+ )
58
+ PYTEST_TESTPATHS_MODULE_NAME = "blocking.pytest_testpaths_orphan_blocker"
59
+ PII_SCANNER_MODULE_NAME = "blocking.pii_scanner"
60
+ REPOSITORY_EXEMPTION_MODULE_NAME = (
61
+ "blocking.pii_prevention_blocker_parts.repository_exemption"
62
+ )
63
+
64
+ ALL_FAIL_CLOSED_EXCEPTION_TYPES = (
65
+ AttributeError,
66
+ ImportError,
67
+ KeyError,
68
+ OSError,
69
+ RuntimeError,
70
+ SyntaxError,
71
+ TypeError,
72
+ UnicodeError,
73
+ ValueError,
74
+ )
75
+
76
+ ALL_ARCHIVED_SKILL_DIRECTORY_SEGMENTS = (".agents", "skills-archived")
77
+ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset(
78
+ (
79
+ (
80
+ "packages/claude-dev-env/.agents/skills-archived/pr-converge/reference/per-tick.md",
81
+ "email",
82
+ "3de132cd98be7bf26b6f08e81c31c799a891bc32046b61f0b6fe3671ca2e44b5",
83
+ ),
84
+ (
85
+ "packages/claude-dev-env/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml",
86
+ "home-path",
87
+ "805e3271caeec55a94438e221726e9349e46fa4970592bd03ffd3adac5e0ea8a",
88
+ ),
89
+ (
90
+ "packages/claude-dev-env/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py",
91
+ "home-path",
92
+ "a4f1f978f88b601288424144a5f5066cdc9154868cf2aaab6e0869a103be72a5",
93
+ ),
94
+ (
95
+ "packages/claude-dev-env/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py",
96
+ "home-path",
97
+ "665e48f1b198dd2a6526e25700fbcb86566d1f8b47d4475a6cc318b1634a4546",
98
+ ),
99
+ (
100
+ "packages/claude-dev-env/_shared/pr-loop/scripts/_claude_permissions_common.py",
101
+ "home-path",
102
+ "9c48917645524d03ea0a4f4d8755aa8f0d59f3108e1aa976202a02e613c1444d",
103
+ ),
104
+ (
105
+ "packages/claude-dev-env/_shared/pr-loop/scripts/_claude_permissions_common.py",
106
+ "home-path",
107
+ "07ba214cb23325675bc68e5513c030e968eccf2760cb5917d932ae5da29d5af1",
108
+ ),
109
+ (
110
+ "packages/claude-dev-env/_shared/pr-loop/scripts/_claude_permissions_common.py",
111
+ "home-path",
112
+ "d41ce8d0f21de2d5320bdd8cd4e8693a2f3d6ea59db6df5ea102a7d2be4b2c22",
113
+ ),
114
+ (
115
+ "packages/claude-dev-env/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md",
116
+ "home-path",
117
+ "550c7b16a2a04e1d05a8a6b4f5b764b8d4fdef65e9d79d09e5dbb3a15a855812",
118
+ ),
119
+ (
120
+ "packages/claude-dev-env/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md",
121
+ "home-path",
122
+ "b8bbdd1a7ebdad16f4e51377a3f21d05dd35992d89e35971b28b05f53245c69d",
123
+ ),
124
+ (
125
+ "packages/claude-dev-env/bin/expand_home_directory_tokens.mjs",
126
+ "home-path",
127
+ "10d5571e7126d2018ee7fb06aa29c72a9d503cac79a4e52d6848602ee10e8832",
128
+ ),
129
+ (
130
+ "packages/claude-dev-env/bin/expand_home_directory_tokens.mjs",
131
+ "home-path",
132
+ "09f83c224a02c370d3d63de5c330e1a830fc92866330fc7963b5b5fd20ffcb91",
133
+ ),
134
+ (
135
+ "packages/claude-dev-env/hooks/blocking/volatile_path_in_post_blocker.py",
136
+ "home-path",
137
+ "ed9203c8078e4c6b832600085f28f24656d679aa8dca82ecc2557afc3df25036",
138
+ ),
139
+ (
140
+ "packages/claude-dev-env/hooks/hooks_constants/hardcoded_user_path_constants.py",
141
+ "home-path",
142
+ "0967160658d782c558722232cad7d47dc19267943d4667b41d3eff1e605be2f4",
143
+ ),
144
+ (
145
+ "packages/claude-dev-env/hooks/hooks_constants/hardcoded_user_path_constants.py",
146
+ "home-path",
147
+ "a541acfac2e649581bca0cd03e4c3dcf306344915017d9cd6be4ad57a8fd7379",
148
+ ),
149
+ )
150
+ )
@@ -0,0 +1,243 @@
1
+ """Environment-variable documentation checks for committed trees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Sequence
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+
9
+ from policy_lint.config.constants import PATH_SEPARATOR, UTF8_ENCODING
10
+
11
+ from repository_checks.config.constants import (
12
+ CHECK_ID_ENV_VAR_DOCUMENTATION,
13
+ ENV_VAR_DRIFT_CONSTANTS_MODULE_NAME,
14
+ ENV_VAR_DRIFT_MODULE_NAME,
15
+ WINDOWS_PATH_SEPARATOR,
16
+ )
17
+ from repository_checks.hook_modules import load_hooks_module
18
+ from repository_checks.models import RepositoryFinding
19
+
20
+
21
+ def collect_env_var_documentation_findings(
22
+ repository_root: Path, all_tracked_paths: Sequence[str]
23
+ ) -> list[RepositoryFinding]:
24
+ """Return drifted environment-variable rows in tracked documentation.
25
+
26
+ Args:
27
+ repository_root: Git repository root.
28
+ all_tracked_paths: Repository-relative tracked paths.
29
+
30
+ Returns:
31
+ Findings for table rows whose code file does not read the variable.
32
+ """
33
+ drift_module = load_hooks_module(ENV_VAR_DRIFT_MODULE_NAME)
34
+ drift_constants = load_hooks_module(ENV_VAR_DRIFT_CONSTANTS_MODULE_NAME)
35
+ all_findings: list[RepositoryFinding] = []
36
+ for each_relative_path in all_tracked_paths:
37
+ all_findings.extend(
38
+ find_documentation_findings_for_path(
39
+ repository_root,
40
+ each_relative_path,
41
+ drift_module,
42
+ drift_constants,
43
+ )
44
+ )
45
+ return all_findings
46
+
47
+
48
+ def find_documentation_findings_for_path(
49
+ repository_root: Path,
50
+ relative_path: str,
51
+ drift_module: ModuleType,
52
+ drift_constants: ModuleType,
53
+ ) -> list[RepositoryFinding]:
54
+ """Return documentation rows whose code file does not read the variable.
55
+
56
+ Args:
57
+ repository_root: Git repository root.
58
+ relative_path: Repository-relative documentation path.
59
+ drift_module: Documentation drift detector module.
60
+ drift_constants: Documentation drift constants module.
61
+
62
+ Returns:
63
+ Findings for table rows whose code file does not read the variable.
64
+ """
65
+ if not drift_module.is_markdown_file(relative_path):
66
+ return []
67
+ absolute_path = repository_root / relative_path
68
+ if not absolute_path.is_file():
69
+ return []
70
+ return _find_documentation_file_findings(
71
+ repository_root,
72
+ relative_path,
73
+ absolute_path,
74
+ drift_module,
75
+ drift_constants,
76
+ )
77
+
78
+
79
+ def _find_documentation_file_findings(
80
+ repository_root: Path,
81
+ relative_path: str,
82
+ absolute_path: Path,
83
+ drift_module: ModuleType,
84
+ drift_constants: ModuleType,
85
+ ) -> list[RepositoryFinding]:
86
+ content = absolute_path.read_text(encoding=UTF8_ENCODING)
87
+ _require_readable_code_files(
88
+ content,
89
+ repository_root,
90
+ drift_module,
91
+ drift_constants,
92
+ )
93
+ all_drift_rows = drift_module.find_drift_rows(content, absolute_path.parent)
94
+ return _build_findings(relative_path, all_drift_rows)
95
+
96
+
97
+ def _build_findings(
98
+ relative_path: str, all_drift_rows: Sequence[str]
99
+ ) -> list[RepositoryFinding]:
100
+ return [
101
+ RepositoryFinding(
102
+ CHECK_ID_ENV_VAR_DOCUMENTATION,
103
+ relative_path.replace(WINDOWS_PATH_SEPARATOR, PATH_SEPARATOR),
104
+ each_drift_row,
105
+ )
106
+ for each_drift_row in all_drift_rows
107
+ ]
108
+
109
+
110
+ def _require_readable_code_files(
111
+ content: str,
112
+ repository_root: Path,
113
+ drift_module: ModuleType,
114
+ drift_constants: ModuleType,
115
+ ) -> None:
116
+ resolved_repository_root = repository_root.resolve(strict=True)
117
+ for each_code_reference in _iter_env_var_code_references(
118
+ content,
119
+ drift_module,
120
+ drift_constants,
121
+ ):
122
+ maybe_code_file = _resolve_code_file(
123
+ repository_root,
124
+ each_code_reference,
125
+ drift_constants,
126
+ )
127
+ if maybe_code_file is None:
128
+ continue
129
+ resolved_code_file = _resolve_inside_repository(
130
+ resolved_repository_root,
131
+ maybe_code_file,
132
+ )
133
+ resolved_code_file.read_text(encoding=UTF8_ENCODING)
134
+
135
+
136
+ def _iter_env_var_code_references(
137
+ content: str,
138
+ drift_module: ModuleType,
139
+ drift_constants: ModuleType,
140
+ ) -> Iterator[str]:
141
+ is_inside_code_fence = False
142
+ for each_line in content.splitlines():
143
+ if drift_constants.CODE_FENCE_PATTERN.match(each_line) is not None:
144
+ is_inside_code_fence = not is_inside_code_fence
145
+ continue
146
+ if is_inside_code_fence:
147
+ continue
148
+ if drift_constants.TABLE_ROW_PATTERN.match(each_line) is None:
149
+ continue
150
+ maybe_code_reference = _code_reference_for_env_var_row(
151
+ each_line,
152
+ drift_module,
153
+ drift_constants,
154
+ )
155
+ if maybe_code_reference is not None:
156
+ yield maybe_code_reference
157
+
158
+
159
+ def _code_reference_for_env_var_row(
160
+ table_line: str,
161
+ drift_module: ModuleType,
162
+ drift_constants: ModuleType,
163
+ ) -> str | None:
164
+ all_cells = drift_module._row_cells(table_line)
165
+ if len(all_cells) < drift_constants.MINIMUM_ENV_VAR_ROW_CELL_COUNT:
166
+ return None
167
+ if drift_module._is_separator_row(all_cells):
168
+ return None
169
+ if drift_module._env_var_name_in_cell(all_cells[0]) is None:
170
+ return None
171
+ for each_cell in all_cells[1:]:
172
+ maybe_code_reference = drift_module._code_file_reference_in_cell(each_cell)
173
+ if maybe_code_reference is not None:
174
+ return maybe_code_reference
175
+ return None
176
+
177
+
178
+ def _resolve_inside_repository(
179
+ resolved_repository_root: Path,
180
+ candidate_path: Path,
181
+ ) -> Path:
182
+ resolved_candidate_path = candidate_path.resolve(strict=True)
183
+ try:
184
+ resolved_candidate_path.relative_to(resolved_repository_root)
185
+ except ValueError as e:
186
+ raise OSError(candidate_path) from e
187
+ return resolved_candidate_path
188
+
189
+
190
+ def _resolve_code_file(
191
+ repository_root: Path,
192
+ code_reference: str,
193
+ drift_constants: ModuleType,
194
+ ) -> Path | None:
195
+ normalized_reference = code_reference.replace(
196
+ WINDOWS_PATH_SEPARATOR, PATH_SEPARATOR
197
+ ).lstrip(PATH_SEPARATOR)
198
+ direct_path = repository_root / normalized_reference
199
+ if direct_path.is_file():
200
+ return direct_path
201
+ reference_basename = Path(normalized_reference).name
202
+ if not reference_basename:
203
+ return None
204
+ return _find_suffix_match(
205
+ repository_root,
206
+ normalized_reference,
207
+ reference_basename,
208
+ drift_constants,
209
+ )
210
+
211
+
212
+ def _find_suffix_match(
213
+ repository_root: Path,
214
+ normalized_reference: str,
215
+ reference_basename: str,
216
+ drift_constants: ModuleType,
217
+ ) -> Path | None:
218
+ suffix_marker = PATH_SEPARATOR + normalized_reference
219
+ scanned_count = 0
220
+ for each_match in repository_root.rglob(reference_basename):
221
+ if _is_under_noise_directory(repository_root, each_match, drift_constants):
222
+ continue
223
+ scanned_count += 1
224
+ if scanned_count > drift_constants.MAX_SUBTREE_FILES_SCANNED:
225
+ return None
226
+ if not each_match.is_file():
227
+ continue
228
+ match_text = PATH_SEPARATOR + each_match.as_posix()
229
+ if match_text.endswith(suffix_marker):
230
+ return each_match
231
+ return None
232
+
233
+
234
+ def _is_under_noise_directory(
235
+ repository_root: Path,
236
+ candidate_path: Path,
237
+ drift_constants: ModuleType,
238
+ ) -> bool:
239
+ all_relative_segments = candidate_path.relative_to(repository_root).parts
240
+ return any(
241
+ each_segment in drift_constants.ALL_NOISE_DIRECTORY_NAMES
242
+ for each_segment in all_relative_segments
243
+ )
@@ -0,0 +1,34 @@
1
+ """Import a hooks module after putting the hook package paths on sys.path."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+
10
+
11
+ def load_hooks_module(module_name: str) -> ModuleType:
12
+ """Import a hooks module after placing ``hooks/`` and ``blocking/`` on ``sys.path``.
13
+
14
+ ::
15
+
16
+ load_hooks_module("blocking.pii_scanner")
17
+ ok: hooks/ is on sys.path, blocking.pii_scanner imports
18
+ flag: importing a hook main() and printing a deny payload
19
+
20
+ Args:
21
+ module_name: Dotted module name under ``hooks/``, such as
22
+ ``blocking.pii_scanner``.
23
+
24
+ Returns:
25
+ The imported module.
26
+ """
27
+ package_root = Path(__file__).resolve().parent.parent.parent
28
+ hooks_directory = str(package_root / "hooks")
29
+ blocking_directory = str(package_root / "hooks" / "blocking")
30
+ if blocking_directory not in sys.path:
31
+ sys.path.insert(0, blocking_directory)
32
+ if hooks_directory not in sys.path:
33
+ sys.path.insert(0, hooks_directory)
34
+ return importlib.import_module(module_name)
@@ -0,0 +1,37 @@
1
+ """Shared report types for committed-tree repository checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from repository_checks.config.constants import (
8
+ FAILED_CHECK_EXIT_CODE,
9
+ FINDINGS_EXIT_CODE,
10
+ SUCCESS_EXIT_CODE,
11
+ )
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RepositoryFinding:
16
+ """One committed-tree finding with a stable check id and relative path."""
17
+
18
+ check_id: str
19
+ relative_path: str
20
+ message: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class RepositoryCheckReport:
25
+ """Sorted findings and the check ids that failed while they ran."""
26
+
27
+ all_findings: tuple[RepositoryFinding, ...]
28
+ all_failed_check_ids: tuple[str, ...]
29
+
30
+ @property
31
+ def exit_code(self) -> int:
32
+ """Return the process status for this report."""
33
+ if self.all_failed_check_ids:
34
+ return FAILED_CHECK_EXIT_CODE
35
+ if self.all_findings:
36
+ return FINDINGS_EXIT_CODE
37
+ return SUCCESS_EXIT_CODE
@@ -0,0 +1,118 @@
1
+ """Package inventory checks for committed trees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+
9
+ from policy_lint.config.constants import PATH_SEPARATOR, UTF8_ENCODING
10
+
11
+ from repository_checks.config.constants import (
12
+ ALL_ARCHIVED_SKILL_DIRECTORY_SEGMENTS,
13
+ CHECK_ID_PACKAGE_INVENTORY,
14
+ PACKAGE_INVENTORY_CONSTANTS_MODULE_NAME,
15
+ PACKAGE_INVENTORY_DETECTION_MODULE_NAME,
16
+ PACKAGE_INVENTORY_MESSAGE_TEMPLATE,
17
+ WINDOWS_PATH_SEPARATOR,
18
+ )
19
+ from repository_checks.hook_modules import load_hooks_module
20
+ from repository_checks.models import RepositoryFinding
21
+
22
+
23
+ def collect_package_inventory_findings(
24
+ repository_root: Path, all_tracked_paths: Sequence[str]
25
+ ) -> list[RepositoryFinding]:
26
+ """Return tracked production files missing from package inventories.
27
+
28
+ Args:
29
+ repository_root: Git repository root.
30
+ all_tracked_paths: Repository-relative tracked paths.
31
+
32
+ Returns:
33
+ Findings for production files absent from their inventories.
34
+ """
35
+ detection = load_hooks_module(PACKAGE_INVENTORY_DETECTION_MODULE_NAME)
36
+ inventory_constants = load_hooks_module(PACKAGE_INVENTORY_CONSTANTS_MODULE_NAME)
37
+ all_findings: list[RepositoryFinding] = []
38
+ for each_relative_path in all_tracked_paths:
39
+ maybe_finding = find_inventory_finding_for_path(
40
+ repository_root,
41
+ each_relative_path,
42
+ detection,
43
+ inventory_constants,
44
+ )
45
+ if maybe_finding is not None:
46
+ all_findings.append(maybe_finding)
47
+ return all_findings
48
+
49
+
50
+ def find_inventory_finding_for_path(
51
+ repository_root: Path,
52
+ relative_path: str,
53
+ detection: ModuleType,
54
+ inventory_constants: ModuleType,
55
+ ) -> RepositoryFinding | None:
56
+ """Return an inventory finding for one tracked production file.
57
+
58
+ Args:
59
+ repository_root: Git repository root.
60
+ relative_path: Repository-relative production path.
61
+ detection: Existing inventory detector module.
62
+ inventory_constants: Existing detector constants module.
63
+
64
+ Returns:
65
+ The finding when the file is missing from its inventory.
66
+ """
67
+ if _is_archived_skill_path(relative_path):
68
+ return None
69
+ absolute_path = repository_root / relative_path
70
+ if not absolute_path.is_file():
71
+ return None
72
+ if not detection.is_inventoried_production_file(str(absolute_path)):
73
+ return None
74
+ _require_readable_inventory_documents(absolute_path.parent, inventory_constants)
75
+ if detection.find_stale_inventory(str(absolute_path)) is None:
76
+ return None
77
+ return _build_finding(relative_path)
78
+
79
+
80
+ def _build_finding(relative_path: str) -> RepositoryFinding:
81
+ return RepositoryFinding(
82
+ CHECK_ID_PACKAGE_INVENTORY,
83
+ relative_path.replace(WINDOWS_PATH_SEPARATOR, PATH_SEPARATOR),
84
+ PACKAGE_INVENTORY_MESSAGE_TEMPLATE,
85
+ )
86
+
87
+
88
+ def _is_archived_skill_path(relative_path: str) -> bool:
89
+ all_path_segments = relative_path.replace(
90
+ WINDOWS_PATH_SEPARATOR, PATH_SEPARATOR
91
+ ).split(PATH_SEPARATOR)
92
+ segment_count = len(ALL_ARCHIVED_SKILL_DIRECTORY_SEGMENTS)
93
+ last_start_index = len(all_path_segments) - segment_count
94
+ for each_start_index in range(max(last_start_index + 1, 0)):
95
+ compared_segments = tuple(
96
+ all_path_segments[each_start_index : each_start_index + segment_count]
97
+ )
98
+ if compared_segments == ALL_ARCHIVED_SKILL_DIRECTORY_SEGMENTS:
99
+ return True
100
+ return False
101
+
102
+
103
+ def _require_readable_inventory_documents(
104
+ package_directory: Path, inventory_constants: ModuleType
105
+ ) -> None:
106
+ for each_inventory_name in inventory_constants.ALL_INVENTORY_DOCUMENT_NAMES:
107
+ inventory_path = package_directory / each_inventory_name
108
+ if not inventory_path.is_file():
109
+ continue
110
+ inventory_path.read_text(encoding=UTF8_ENCODING)
111
+ if package_directory.name != inventory_constants.SCRIPTS_SUBDIRECTORY_NAME:
112
+ return
113
+ skill_inventory_path = (
114
+ package_directory.parent / inventory_constants.SKILL_INVENTORY_DOCUMENT_NAME
115
+ )
116
+ if not skill_inventory_path.is_file():
117
+ return
118
+ skill_inventory_path.read_text(encoding=UTF8_ENCODING)
@@ -0,0 +1,84 @@
1
+ """Pytest testpath checks for committed trees."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+
9
+ import tomllib
10
+ from policy_lint.config import constants as policy_constants
11
+
12
+ from repository_checks.config import constants as repository_constants
13
+ from repository_checks.hook_modules import load_hooks_module
14
+ from repository_checks.models import RepositoryFinding
15
+
16
+
17
+ def collect_pytest_testpath_findings(
18
+ repository_root: Path, all_tracked_paths: Sequence[str]
19
+ ) -> list[RepositoryFinding]:
20
+ """Return tracked tests outside explicit package testpaths.
21
+
22
+ Args:
23
+ repository_root: Git repository root.
24
+ all_tracked_paths: Repository-relative tracked paths.
25
+
26
+ Returns:
27
+ Findings for tests excluded by their package testpaths.
28
+ """
29
+ pytest_module = load_hooks_module(repository_constants.PYTEST_TESTPATHS_MODULE_NAME)
30
+ all_findings: list[RepositoryFinding] = []
31
+ for each_relative_path in all_tracked_paths:
32
+ maybe_finding = _find_testpath_finding(
33
+ repository_root, each_relative_path, pytest_module
34
+ )
35
+ if maybe_finding is not None:
36
+ all_findings.append(maybe_finding)
37
+ return all_findings
38
+
39
+
40
+ def _find_testpath_finding(
41
+ repository_root: Path,
42
+ relative_path: str,
43
+ pytest_module: ModuleType,
44
+ ) -> RepositoryFinding | None:
45
+ if not pytest_module.is_test_file(relative_path):
46
+ return None
47
+ absolute_path = repository_root / relative_path
48
+ if not absolute_path.is_file():
49
+ return None
50
+ _require_readable_inputs(absolute_path, repository_root)
51
+ if pytest_module.find_unregistered_test_directory(str(absolute_path)) is None:
52
+ return None
53
+ return RepositoryFinding(
54
+ repository_constants.CHECK_ID_PYTEST_TESTPATHS,
55
+ _to_posix_path(relative_path),
56
+ repository_constants.PYTEST_TESTPATH_MESSAGE_TEMPLATE,
57
+ )
58
+
59
+
60
+ def _to_posix_path(relative_path: str) -> str:
61
+ return relative_path.replace(
62
+ repository_constants.WINDOWS_PATH_SEPARATOR,
63
+ policy_constants.PATH_SEPARATOR,
64
+ )
65
+
66
+
67
+ def _require_readable_inputs(test_file: Path, repository_root: Path) -> None:
68
+ test_file.read_bytes()
69
+ resolved_repository_root = repository_root.resolve()
70
+ for each_directory in test_file.parents:
71
+ if not _is_inside_repository(each_directory, resolved_repository_root):
72
+ break
73
+ pyproject_path = each_directory / repository_constants.PYPROJECT_FILENAME
74
+ if not pyproject_path.is_file():
75
+ continue
76
+ tomllib.loads(pyproject_path.read_text(encoding=policy_constants.UTF8_ENCODING))
77
+
78
+
79
+ def _is_inside_repository(directory: Path, resolved_repository_root: Path) -> bool:
80
+ try:
81
+ directory.resolve().relative_to(resolved_repository_root)
82
+ except ValueError:
83
+ return False
84
+ return True