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.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- 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/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
|
@@ -133,7 +133,16 @@ def default_registry() -> tuple[model.Rule, ...]:
|
|
|
133
133
|
*_text_document_rules(),
|
|
134
134
|
*_configuration_document_rules(),
|
|
135
135
|
model.ChangeSetRule(
|
|
136
|
-
"test-pairing",
|
|
136
|
+
"test-pairing",
|
|
137
|
+
frozenset({"changed"}),
|
|
138
|
+
frozenset({model.SelectionKind.STAGED, model.SelectionKind.BASE}),
|
|
139
|
+
adapters.test_pairing_diagnostics,
|
|
140
|
+
),
|
|
141
|
+
model.ChangeSetRule(
|
|
142
|
+
"terminology-sweep",
|
|
143
|
+
frozenset({"changed"}),
|
|
144
|
+
frozenset({model.SelectionKind.STAGED}),
|
|
145
|
+
adapters.terminology_diagnostics,
|
|
137
146
|
),
|
|
138
147
|
model.RepositoryRule(
|
|
139
148
|
"repository-path-collision",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Committed-tree repository checks used by the repository_policy command."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""CLAUDE.md reference checks for committed trees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
|
|
10
|
+
from policy_lint.config.constants import PATH_SEPARATOR, UTF8_ENCODING
|
|
11
|
+
|
|
12
|
+
from repository_checks.config.constants import (
|
|
13
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
14
|
+
CLAUDE_MD_CONSTANTS_MODULE_NAME,
|
|
15
|
+
CLAUDE_MD_MISSING_FILE_MESSAGE_TEMPLATE,
|
|
16
|
+
CLAUDE_MD_SCAN_MODULE_NAME,
|
|
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_claude_md_orphan_findings(
|
|
24
|
+
repository_root: Path, all_tracked_paths: Sequence[str]
|
|
25
|
+
) -> list[RepositoryFinding]:
|
|
26
|
+
"""Return missing CLAUDE.md references in the tracked tree.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
repository_root: Git repository root.
|
|
30
|
+
all_tracked_paths: Repository-relative tracked paths.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Findings for references whose files are absent.
|
|
34
|
+
"""
|
|
35
|
+
subtree_scan = load_hooks_module(CLAUDE_MD_SCAN_MODULE_NAME)
|
|
36
|
+
claude_md_constants = load_hooks_module(CLAUDE_MD_CONSTANTS_MODULE_NAME)
|
|
37
|
+
all_findings: list[RepositoryFinding] = []
|
|
38
|
+
for each_relative_path in all_tracked_paths:
|
|
39
|
+
all_findings.extend(
|
|
40
|
+
find_orphan_findings_for_path(
|
|
41
|
+
repository_root,
|
|
42
|
+
each_relative_path,
|
|
43
|
+
subtree_scan,
|
|
44
|
+
claude_md_constants,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return all_findings
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_orphan_findings_for_path(
|
|
51
|
+
repository_root: Path,
|
|
52
|
+
relative_path: str,
|
|
53
|
+
subtree_scan: ModuleType,
|
|
54
|
+
claude_md_constants: ModuleType,
|
|
55
|
+
) -> list[RepositoryFinding]:
|
|
56
|
+
"""Return missing files named by one CLAUDE.md file.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
repository_root: Git repository root.
|
|
60
|
+
relative_path: Repository-relative path to inspect.
|
|
61
|
+
subtree_scan: Existing reference detector module.
|
|
62
|
+
claude_md_constants: Existing detector constants module.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Findings for referenced files that are absent from the scan root.
|
|
66
|
+
"""
|
|
67
|
+
if Path(relative_path).name != claude_md_constants.CLAUDE_MD_FILENAME:
|
|
68
|
+
return []
|
|
69
|
+
absolute_path = repository_root / relative_path
|
|
70
|
+
if not absolute_path.is_file():
|
|
71
|
+
return []
|
|
72
|
+
return _find_orphans_in_claude_md(absolute_path, relative_path, subtree_scan)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _find_orphans_in_claude_md(
|
|
76
|
+
absolute_path: Path,
|
|
77
|
+
relative_path: str,
|
|
78
|
+
subtree_scan: ModuleType,
|
|
79
|
+
) -> list[RepositoryFinding]:
|
|
80
|
+
content = absolute_path.read_text(encoding=UTF8_ENCODING)
|
|
81
|
+
claude_md_directory = absolute_path.parent
|
|
82
|
+
subtree_root = _subtree_root_for(claude_md_directory)
|
|
83
|
+
subtree_root.stat()
|
|
84
|
+
deque(subtree_root.rglob("*"), maxlen=0)
|
|
85
|
+
all_missing_filenames = subtree_scan.find_missing_filenames(
|
|
86
|
+
content, claude_md_directory
|
|
87
|
+
)
|
|
88
|
+
return _build_findings(relative_path, all_missing_filenames)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _subtree_root_for(claude_md_directory: Path) -> Path:
|
|
92
|
+
parent_directory = claude_md_directory.parent
|
|
93
|
+
if parent_directory == claude_md_directory:
|
|
94
|
+
return claude_md_directory
|
|
95
|
+
return parent_directory
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _build_findings(
|
|
99
|
+
relative_path: str, all_missing_filenames: Sequence[str]
|
|
100
|
+
) -> list[RepositoryFinding]:
|
|
101
|
+
return [
|
|
102
|
+
RepositoryFinding(
|
|
103
|
+
CHECK_ID_CLAUDE_MD_ORPHANS,
|
|
104
|
+
relative_path.replace(WINDOWS_PATH_SEPARATOR, PATH_SEPARATOR),
|
|
105
|
+
CLAUDE_MD_MISSING_FILE_MESSAGE_TEMPLATE.format(filename=each_filename),
|
|
106
|
+
)
|
|
107
|
+
for each_filename in all_missing_filenames
|
|
108
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration for the committed-tree repository checker."""
|
|
@@ -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
|