claude-dev-env 2.36.0 → 4.1.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.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/syncing-submodules/SKILL.md +101 -0
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
- package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
- package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
- package/.agents/skills/team-advisor/SKILL.md +11 -11
- package/_shared/advisor/advisor-protocol.md +51 -51
- package/_shared/advisor/reference/advisor-block.md +9 -9
- package/_shared/advisor/reference/astra-rung.md +39 -0
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +6 -6
- package/_shared/advisor/reference/spawn-walk-log.md +9 -9
- package/_shared/advisor/reference/third-party-bind.md +11 -11
- package/_shared/advisor/reference/warm-up.md +13 -13
- package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
- package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
- package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +30 -0
- package/bin/git_hooks_installer.mjs +0 -1
- package/bin/git_hooks_installer.test.mjs +16 -1
- package/bin/install.mjs +5 -2
- package/bin/install.prune.test.mjs +36 -24
- package/bin/install.test.mjs +7 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/validators/README.md +35 -1
- package/package.json +1 -1
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/adapter_detectors.py +42 -2
- package/scripts/policy_lint/adapters.py +24 -0
- package/scripts/policy_lint/config/constants.py +11 -0
- package/scripts/policy_lint/engine.py +1 -1
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +10 -1
- package/scripts/repository_checks/__init__.py +1 -0
- package/scripts/repository_checks/claude_md.py +108 -0
- package/scripts/repository_checks/config/__init__.py +1 -0
- package/scripts/repository_checks/config/constants.py +150 -0
- package/scripts/repository_checks/env_var_documentation.py +243 -0
- package/scripts/repository_checks/hook_modules.py +34 -0
- package/scripts/repository_checks/models.py +37 -0
- package/scripts/repository_checks/package_inventory.py +118 -0
- package/scripts/repository_checks/pytest_testpaths.py +84 -0
- package/scripts/repository_checks/reporting.py +39 -0
- package/scripts/repository_checks/runner.py +93 -0
- package/scripts/repository_checks/tracked_secrets.py +147 -0
- package/scripts/repository_checks/tracked_tree.py +28 -0
- package/scripts/repository_policy.py +120 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +25 -1
- package/scripts/tests/test_claude_md.py +68 -0
- package/scripts/tests/test_env_var_documentation.py +133 -0
- package/scripts/tests/test_hook_modules.py +17 -0
- package/scripts/tests/test_models.py +26 -0
- package/scripts/tests/test_package_inventory.py +96 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
- package/hooks/git-hooks/post_commit.py +0 -241
- package/hooks/git-hooks/test_post_commit.py +0 -298
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Stable text reports for committed-tree repository checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from repository_checks.config import constants as repository_constants
|
|
6
|
+
from repository_checks.models import RepositoryCheckReport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_report(report: RepositoryCheckReport) -> str:
|
|
10
|
+
"""Render findings and failed checks as newline-terminated text.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
report: Findings and failed check identifiers.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
The report body, or an empty string when the tree is clean.
|
|
17
|
+
"""
|
|
18
|
+
all_lines = [
|
|
19
|
+
repository_constants.FINDING_LINE_TEMPLATE.format(
|
|
20
|
+
check_id=each_finding.check_id,
|
|
21
|
+
relative_path=each_finding.relative_path,
|
|
22
|
+
message=each_finding.message,
|
|
23
|
+
)
|
|
24
|
+
for each_finding in report.all_findings
|
|
25
|
+
]
|
|
26
|
+
all_lines.extend(_failed_check_lines(report))
|
|
27
|
+
if not all_lines:
|
|
28
|
+
return repository_constants.EMPTY_REPORT_TEXT
|
|
29
|
+
return (
|
|
30
|
+
repository_constants.REPORT_LINE_SEPARATOR.join(all_lines)
|
|
31
|
+
+ repository_constants.REPORT_LINE_SEPARATOR
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _failed_check_lines(report: RepositoryCheckReport) -> list[str]:
|
|
36
|
+
return [
|
|
37
|
+
repository_constants.RULE_FAILED_LINE_TEMPLATE.format(check_id=each_check_id)
|
|
38
|
+
for each_check_id in report.all_failed_check_ids
|
|
39
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Fail-closed runner for committed-tree repository checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from repository_checks import tracked_tree
|
|
9
|
+
from repository_checks.claude_md import collect_claude_md_orphan_findings
|
|
10
|
+
from repository_checks.config import constants as repository_constants
|
|
11
|
+
from repository_checks.env_var_documentation import (
|
|
12
|
+
collect_env_var_documentation_findings,
|
|
13
|
+
)
|
|
14
|
+
from repository_checks.models import RepositoryCheckReport, RepositoryFinding
|
|
15
|
+
from repository_checks.package_inventory import collect_package_inventory_findings
|
|
16
|
+
from repository_checks.pytest_testpaths import collect_pytest_testpath_findings
|
|
17
|
+
from repository_checks.tracked_secrets import collect_tracked_secret_findings
|
|
18
|
+
|
|
19
|
+
RepositoryCollector = Callable[[Path, Sequence[str]], list[RepositoryFinding]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_repository_checks(repository_root: Path) -> RepositoryCheckReport:
|
|
23
|
+
"""Run every committed-tree check and fail closed on collector errors.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
repository_root: Git repository root whose tracked files are scanned.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Sorted findings and any check identifiers that failed closed.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
all_tracked_paths = tracked_tree.tracked_relative_paths(repository_root)
|
|
33
|
+
except repository_constants.ALL_FAIL_CLOSED_EXCEPTION_TYPES:
|
|
34
|
+
return RepositoryCheckReport((), repository_constants.ALL_CHECK_IDS)
|
|
35
|
+
all_findings, all_failed_check_ids = _run_collectors(
|
|
36
|
+
repository_root, all_tracked_paths
|
|
37
|
+
)
|
|
38
|
+
return RepositoryCheckReport(
|
|
39
|
+
_sort_findings(all_findings), tuple(all_failed_check_ids)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run_collectors(
|
|
44
|
+
repository_root: Path, all_tracked_paths: Sequence[str]
|
|
45
|
+
) -> tuple[list[RepositoryFinding], list[str]]:
|
|
46
|
+
all_findings: list[RepositoryFinding] = []
|
|
47
|
+
all_failed_check_ids: list[str] = []
|
|
48
|
+
for each_check_id, each_collector in _all_collectors():
|
|
49
|
+
try:
|
|
50
|
+
all_findings.extend(each_collector(repository_root, all_tracked_paths))
|
|
51
|
+
except repository_constants.ALL_FAIL_CLOSED_EXCEPTION_TYPES:
|
|
52
|
+
all_failed_check_ids.append(each_check_id)
|
|
53
|
+
return all_findings, all_failed_check_ids
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _sort_findings(
|
|
57
|
+
all_findings: list[RepositoryFinding],
|
|
58
|
+
) -> tuple[RepositoryFinding, ...]:
|
|
59
|
+
return tuple(
|
|
60
|
+
sorted(
|
|
61
|
+
all_findings,
|
|
62
|
+
key=lambda each_finding: (
|
|
63
|
+
each_finding.check_id,
|
|
64
|
+
each_finding.relative_path,
|
|
65
|
+
each_finding.message,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _all_collectors() -> tuple[tuple[str, RepositoryCollector], ...]:
|
|
72
|
+
return (
|
|
73
|
+
(
|
|
74
|
+
repository_constants.CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
75
|
+
collect_claude_md_orphan_findings,
|
|
76
|
+
),
|
|
77
|
+
(
|
|
78
|
+
repository_constants.CHECK_ID_ENV_VAR_DOCUMENTATION,
|
|
79
|
+
collect_env_var_documentation_findings,
|
|
80
|
+
),
|
|
81
|
+
(
|
|
82
|
+
repository_constants.CHECK_ID_PACKAGE_INVENTORY,
|
|
83
|
+
collect_package_inventory_findings,
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
repository_constants.CHECK_ID_PYTEST_TESTPATHS,
|
|
87
|
+
collect_pytest_testpath_findings,
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
repository_constants.CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
91
|
+
collect_tracked_secret_findings,
|
|
92
|
+
),
|
|
93
|
+
)
|