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.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -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/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/README.md +35 -1
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +368 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +376 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +388 -0
- package/scripts/policy_lint/registry.py +171 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- 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/select_pstack_models.mjs +304 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +398 -0
- 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_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -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
|
@@ -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
|