claude-dev-env 7.4.1 → 7.6.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/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +10 -16
- package/bin/install.policy-lint.test.mjs +1 -2
- package/bin/install.test.mjs +0 -1
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
- package/hooks/advisory/pr_done_reminder.py +14 -13
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
- package/hooks/advisory/test_pr_done_reminder.py +22 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
- package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
- package/hooks/hooks_constants/post_tool_use_context.py +34 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
- package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
- package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
- package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/observability/test_failure_recorder.py +3 -1
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +3 -3
- package/rules/code-standards.md +3 -2
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/destructive-commands.md +19 -19
- package/rules/doc-inventory-integrity.md +4 -4
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +4 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +34 -0
- package/rules/long-horizon-autonomy.md +2 -0
- package/rules/nas-ssh-invocation.md +3 -3
- package/rules/no-cross-skill-duplicate-helpers.md +6 -6
- package/rules/orphan-css-class.md +5 -5
- package/rules/paired-test-coverage.md +6 -6
- package/rules/plain-illustrative-docstrings.md +3 -3
- package/rules/pstack-models.md +4 -3
- package/rules/re-stage-before-commit.md +7 -7
- package/rules/retired-hook-prose.md +16 -0
- package/rules/shell-invocation.md +17 -1
- package/rules/testing.md +3 -5
- package/rules/verify-runtime-state.md +1 -1
- package/rules/windows-filesystem-safe.md +2 -2
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/durable_post_lint_config/config/constants.py +0 -3
- package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_configuration.py +24 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
- package/scripts/policy_lint/adapters.py +82 -5
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/registry.py +6 -1
- package/scripts/policy_lint/selection.py +37 -18
- package/scripts/policy_lint/selection_git.py +17 -2
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/test_durable_post_lint.py +42 -9
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
- package/scripts/tests/test_policy_lint_selection.py +141 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
- package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
- package/hooks/hooks_constants/test_text_stripping.py +0 -39
- package/hooks/hooks_constants/text_stripping.py +0 -36
- package/scripts/automatic_advisory/window_flags.py +0 -41
- package/scripts/repository_checks/config/email_exemptions.py +0 -10
|
@@ -8,7 +8,13 @@ from types import ModuleType
|
|
|
8
8
|
|
|
9
9
|
from shared_tree_paths import resolve_shared_scripts_directory
|
|
10
10
|
|
|
11
|
-
from . import
|
|
11
|
+
from . import (
|
|
12
|
+
adapter_configuration,
|
|
13
|
+
adapter_detectors,
|
|
14
|
+
adapter_pairing,
|
|
15
|
+
adapter_retired_hook_prose,
|
|
16
|
+
adapter_support,
|
|
17
|
+
)
|
|
12
18
|
from .config import constants
|
|
13
19
|
from .model import Diagnostic, Document, DocumentSet
|
|
14
20
|
|
|
@@ -27,9 +33,13 @@ def _hooks_module(module_name: str) -> ModuleType:
|
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
def _pr_loop_script_module(module_name: str) -> ModuleType:
|
|
36
|
+
marker_relative_path = (
|
|
37
|
+
module_name.replace(".", constants.POSIX_PATH_SEPARATOR)
|
|
38
|
+
+ constants.PYTHON_SUFFIX
|
|
39
|
+
)
|
|
30
40
|
scripts_directory = str(resolve_shared_scripts_directory(
|
|
31
41
|
__file__, os.environ, constants.PR_LOOP_DIRECTORY_NAME,
|
|
32
|
-
|
|
42
|
+
marker_relative_path, constants.SHARED_ROOT_PARENT_INDEX
|
|
33
43
|
))
|
|
34
44
|
if scripts_directory not in sys.path:
|
|
35
45
|
sys.path.insert(0, scripts_directory)
|
|
@@ -66,7 +76,7 @@ def code_rule_diagnostics(
|
|
|
66
76
|
Diagnostics from the code-rule engine.
|
|
67
77
|
"""
|
|
68
78
|
return adapter_detectors.code_rule_diagnostics(
|
|
69
|
-
document, repository_root, _hooks_module
|
|
79
|
+
document, repository_root, _hooks_module, _pr_loop_script_module
|
|
70
80
|
)
|
|
71
81
|
|
|
72
82
|
|
|
@@ -270,6 +280,25 @@ def accepts_python(document: Document) -> bool:
|
|
|
270
280
|
return document.path.suffix.lower() == constants.PYTHON_SUFFIX
|
|
271
281
|
|
|
272
282
|
|
|
283
|
+
def accepts_production_python(document: Document) -> bool:
|
|
284
|
+
"""Return whether the document is Python outside the test tree.
|
|
285
|
+
|
|
286
|
+
The hook lane exempts a test module so a test can stage an undercounting
|
|
287
|
+
fixture freely. This gate reuses the hook lane's own ``is_test_file``
|
|
288
|
+
predicate, so the two lanes cannot drift on what counts as a test file.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
document: Candidate document.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
True for a Python document whose path is not a test path.
|
|
295
|
+
"""
|
|
296
|
+
if not accepts_python(document):
|
|
297
|
+
return False
|
|
298
|
+
shared_module = _hooks_module("blocking.code_rules_shared")
|
|
299
|
+
return not shared_module.is_test_file(f"/{document.path.as_posix()}")
|
|
300
|
+
|
|
301
|
+
|
|
273
302
|
def accepts_code(document: Document) -> bool:
|
|
274
303
|
"""Return whether the document contains supported source code.
|
|
275
304
|
|
|
@@ -286,6 +315,21 @@ def accepts_markdown(document: Document) -> bool:
|
|
|
286
315
|
return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
|
|
287
316
|
|
|
288
317
|
|
|
318
|
+
def _is_generated_document(document: Document) -> bool:
|
|
319
|
+
"""Return whether a release tool writes the document from commit history.
|
|
320
|
+
|
|
321
|
+
A changelog is rebuilt from merged commit subjects, so its wording belongs
|
|
322
|
+
to the commits rather than to an author. Prose rules skip it.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
document: Candidate document.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
True for a generated document name.
|
|
329
|
+
"""
|
|
330
|
+
return document.path.name.lower() in constants.ALL_GENERATED_DOCUMENT_NAMES
|
|
331
|
+
|
|
332
|
+
|
|
289
333
|
def accepts_source_or_markdown(document: Document) -> bool:
|
|
290
334
|
"""Return whether a state-description rule can inspect the document.
|
|
291
335
|
|
|
@@ -293,8 +337,10 @@ def accepts_source_or_markdown(document: Document) -> bool:
|
|
|
293
337
|
document: Candidate document.
|
|
294
338
|
|
|
295
339
|
Returns:
|
|
296
|
-
True for
|
|
340
|
+
True for authored source or Markdown, and False for a generated document.
|
|
297
341
|
"""
|
|
342
|
+
if _is_generated_document(document):
|
|
343
|
+
return False
|
|
298
344
|
return accepts_code(document) or accepts_markdown(document)
|
|
299
345
|
|
|
300
346
|
|
|
@@ -305,8 +351,10 @@ def accepts_stored_prompt(document: Document) -> bool:
|
|
|
305
351
|
document: Candidate document.
|
|
306
352
|
|
|
307
353
|
Returns:
|
|
308
|
-
True for Markdown in an instruction directory.
|
|
354
|
+
True for authored Markdown in an instruction directory.
|
|
309
355
|
"""
|
|
356
|
+
if _is_generated_document(document):
|
|
357
|
+
return False
|
|
310
358
|
normalized_path = f"/{document.path.as_posix().lower()}"
|
|
311
359
|
return accepts_markdown(document) and any(
|
|
312
360
|
each_segment in normalized_path
|
|
@@ -377,3 +425,32 @@ def accepts_plans(document: Document) -> bool:
|
|
|
377
425
|
)
|
|
378
426
|
)
|
|
379
427
|
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def accepts_rules_markdown(document: Document) -> bool:
|
|
431
|
+
"""Return whether the document is Markdown in a rules directory.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
document: Candidate document.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
True for a Markdown file directly under a ``rules`` directory.
|
|
438
|
+
"""
|
|
439
|
+
return adapter_retired_hook_prose.accepts_rules_markdown(document)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def retired_hook_prose_diagnostics(
|
|
443
|
+
document: Document, repository_root: Path
|
|
444
|
+
) -> tuple[Diagnostic, ...]:
|
|
445
|
+
"""Report rules prose that credits an unregistered hook with a live action.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
document: Current rules Markdown text and path.
|
|
449
|
+
repository_root: Request repository root for package resolution.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Retired-hook prose diagnostics.
|
|
453
|
+
"""
|
|
454
|
+
return adapter_retired_hook_prose.retired_hook_prose_diagnostics(
|
|
455
|
+
document, repository_root
|
|
456
|
+
)
|
|
@@ -5,6 +5,31 @@ from pathlib import PurePosixPath
|
|
|
5
5
|
APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
|
|
6
6
|
PurePosixPath, frozenset[PurePosixPath]
|
|
7
7
|
] = {
|
|
8
|
+
PurePosixPath("packages/claude-dev-env/bin/install-constants.mjs"): frozenset(
|
|
9
|
+
{
|
|
10
|
+
PurePosixPath("packages/claude-dev-env/bin/install.cursor-rules.test.mjs"),
|
|
11
|
+
}
|
|
12
|
+
),
|
|
13
|
+
PurePosixPath("packages/claude-dev-env/bin/install.mjs"): frozenset(
|
|
14
|
+
{
|
|
15
|
+
PurePosixPath("packages/claude-dev-env/bin/install.cursor-rules.test.mjs"),
|
|
16
|
+
}
|
|
17
|
+
),
|
|
18
|
+
PurePosixPath(
|
|
19
|
+
"packages/claude-dev-env/hooks/blocking/code_rules_comments.py"
|
|
20
|
+
): frozenset(
|
|
21
|
+
{
|
|
22
|
+
PurePosixPath(
|
|
23
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py"
|
|
24
|
+
),
|
|
25
|
+
PurePosixPath(
|
|
26
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py"
|
|
27
|
+
),
|
|
28
|
+
PurePosixPath(
|
|
29
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_javascript_comments.py"
|
|
30
|
+
),
|
|
31
|
+
}
|
|
32
|
+
),
|
|
8
33
|
PurePosixPath(
|
|
9
34
|
"packages/claude-dev-env/hooks/git-hooks/verification_notice_context.py"
|
|
10
35
|
): frozenset(
|
|
@@ -152,15 +177,6 @@ APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
|
|
|
152
177
|
),
|
|
153
178
|
}
|
|
154
179
|
),
|
|
155
|
-
PurePosixPath(
|
|
156
|
-
"packages/claude-dev-env/scripts/automatic_advisory/window_flags.py"
|
|
157
|
-
): frozenset(
|
|
158
|
-
{
|
|
159
|
-
PurePosixPath(
|
|
160
|
-
"packages/claude-dev-env/scripts/tests/test_automatic_advisory_window_flags.py"
|
|
161
|
-
),
|
|
162
|
-
}
|
|
163
|
-
),
|
|
164
180
|
PurePosixPath(
|
|
165
181
|
"packages/claude-dev-env/scripts/automatic_advisory/windows_job.py"
|
|
166
182
|
): frozenset(
|
|
@@ -34,6 +34,7 @@ RENAME_PATH_COUNT = 2
|
|
|
34
34
|
PYTHON_SUFFIX = ".py"
|
|
35
35
|
ALL_CODE_SUFFIXES = frozenset({".py", ".js", ".jsx", ".ts", ".tsx", ".mjs"})
|
|
36
36
|
ALL_MARKDOWN_SUFFIXES = frozenset({".md", ".mdx"})
|
|
37
|
+
ALL_GENERATED_DOCUMENT_NAMES = frozenset({"changelog.md"})
|
|
37
38
|
ALL_STORED_PROMPT_SEGMENTS = (
|
|
38
39
|
"/.agents/",
|
|
39
40
|
"/commands/",
|
|
@@ -57,6 +58,9 @@ ALL_ACTION_BOUNDARY_SEGMENTS = frozenset(
|
|
|
57
58
|
}
|
|
58
59
|
)
|
|
59
60
|
ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
|
|
61
|
+
ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS = (
|
|
62
|
+
"blocking/bash_pre_tool_use_dispatcher.py",
|
|
63
|
+
)
|
|
60
64
|
ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
|
|
61
65
|
ALL_TEST_FILE_PREFIXES = ("test_",)
|
|
62
66
|
ALL_TEST_FILE_SUFFIXES = ("_test.py",)
|
|
@@ -80,3 +84,76 @@ TERMINOLOGY_MESSAGE_GROUP = "message"
|
|
|
80
84
|
|
|
81
85
|
PR_LOOP_DIRECTORY_NAME = "pr-loop"
|
|
82
86
|
SHARED_ROOT_PARENT_INDEX = 2
|
|
87
|
+
POSIX_PATH_SEPARATOR = "/"
|
|
88
|
+
VIOLATION_SCOPING_MODULE_NAME = "code_rules_gate_parts.violation_scoping"
|
|
89
|
+
|
|
90
|
+
RETIRED_HOOK_PROSE_RULE_ID = "retired-hook-prose"
|
|
91
|
+
RULES_DIRECTORY_NAME = "rules"
|
|
92
|
+
HOOKS_DIRECTORY_NAME = "hooks"
|
|
93
|
+
HOOKS_CONFIGURATION_FILE_NAME = "hooks.json"
|
|
94
|
+
HOOK_CONSTANTS_DIRECTORY_NAME = "hooks_constants"
|
|
95
|
+
DISPATCHER_CONSTANTS_GLOB = "*dispatcher_constants.py"
|
|
96
|
+
ALL_INSTALLER_PATH_SEGMENTS = ("bin", "install.mjs")
|
|
97
|
+
PYTHON_GLOB = "*.py"
|
|
98
|
+
REPLACEMENT_CHARACTER_ERRORS = "replace"
|
|
99
|
+
ALL_HOOK_MODULE_NAME_SUFFIXES = (
|
|
100
|
+
"_advisor",
|
|
101
|
+
"_blocker",
|
|
102
|
+
"_dispatcher",
|
|
103
|
+
"_enforcer",
|
|
104
|
+
"_gate",
|
|
105
|
+
"_guard",
|
|
106
|
+
"_recorder",
|
|
107
|
+
"_rewriter",
|
|
108
|
+
"_tracker",
|
|
109
|
+
"_validator",
|
|
110
|
+
)
|
|
111
|
+
ALL_LIVE_CLAIM_VERBS = frozenset(
|
|
112
|
+
{
|
|
113
|
+
"allows",
|
|
114
|
+
"applies",
|
|
115
|
+
"blocks",
|
|
116
|
+
"catches",
|
|
117
|
+
"checks",
|
|
118
|
+
"denies",
|
|
119
|
+
"enforces",
|
|
120
|
+
"fires",
|
|
121
|
+
"flags",
|
|
122
|
+
"gates",
|
|
123
|
+
"hosts",
|
|
124
|
+
"matches",
|
|
125
|
+
"prevents",
|
|
126
|
+
"reads",
|
|
127
|
+
"records",
|
|
128
|
+
"refuses",
|
|
129
|
+
"rejects",
|
|
130
|
+
"reports",
|
|
131
|
+
"requires",
|
|
132
|
+
"runs",
|
|
133
|
+
"scans",
|
|
134
|
+
"stops",
|
|
135
|
+
"watches",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
ALL_REPLACEMENT_DELIVERY_PHRASES = (
|
|
139
|
+
"staged policy lint",
|
|
140
|
+
"policy lint",
|
|
141
|
+
"cde_lint",
|
|
142
|
+
"repository_checks/",
|
|
143
|
+
"repository_policy.py",
|
|
144
|
+
"repository check",
|
|
145
|
+
)
|
|
146
|
+
INLINE_CODE_PATTERN = r"`([^`\n]+)`"
|
|
147
|
+
HOOK_MODULE_NAME_PATTERN = r"[a-z_][a-z0-9_]*"
|
|
148
|
+
PROSE_WORD_PATTERN = r"[a-z]+"
|
|
149
|
+
RETIRED_ROSTER_PATTERN = (
|
|
150
|
+
r"(?s)RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS\s*=\s*new Set\(\[(.*?)\]\)"
|
|
151
|
+
)
|
|
152
|
+
QUOTED_HOOK_PATH_PATTERN = r"['\"]([a-z_]+/[a-z_0-9]+\.py)['\"]"
|
|
153
|
+
HOOKS_JSON_PATH_PATTERN = r"hooks/[a-z_]+/([a-z_0-9]+)\.py"
|
|
154
|
+
SENTENCE_BOUNDARY_PATTERN = r"[.!?](?:\s|$)"
|
|
155
|
+
RETIRED_HOOK_PROSE_MESSAGE = (
|
|
156
|
+
"{hook_name} is named by no hooks.json entry and no dispatcher roster, so it "
|
|
157
|
+
"reaches no tool call. Say what the reader faces now, or drop the "
|
|
158
|
+
"present-tense action."
|
|
159
|
+
)
|
|
@@ -73,7 +73,7 @@ def _python_document_rules() -> tuple[model.Rule, ...]:
|
|
|
73
73
|
),
|
|
74
74
|
build_document_rule(
|
|
75
75
|
"subprocess-budget",
|
|
76
|
-
adapters.
|
|
76
|
+
adapters.accepts_production_python,
|
|
77
77
|
adapters.subprocess_budget_diagnostics,
|
|
78
78
|
),
|
|
79
79
|
build_document_rule(
|
|
@@ -104,6 +104,11 @@ def _text_document_rules() -> tuple[model.Rule, ...]:
|
|
|
104
104
|
adapters.accepts_workflow,
|
|
105
105
|
adapters.workflow_substitution_diagnostics,
|
|
106
106
|
),
|
|
107
|
+
build_document_rule(
|
|
108
|
+
"retired-hook-prose",
|
|
109
|
+
adapters.accepts_rules_markdown,
|
|
110
|
+
adapters.retired_hook_prose_diagnostics,
|
|
111
|
+
),
|
|
107
112
|
)
|
|
108
113
|
|
|
109
114
|
|
|
@@ -18,6 +18,7 @@ from .model import (
|
|
|
18
18
|
TextDocument,
|
|
19
19
|
)
|
|
20
20
|
from .selection_git import (
|
|
21
|
+
GitBlobItemBlocked,
|
|
21
22
|
GitSelectionError,
|
|
22
23
|
git_bytes_for,
|
|
23
24
|
head_revision,
|
|
@@ -30,6 +31,10 @@ class SelectionRunFatal(ValueError):
|
|
|
30
31
|
"""Raised when a source selection cannot be resolved safely."""
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
class SelectionItemBlocked(ValueError):
|
|
35
|
+
"""Raised when one file holds bytes that are not a policy document."""
|
|
36
|
+
|
|
37
|
+
|
|
33
38
|
SelectionError = SelectionRunFatal
|
|
34
39
|
|
|
35
40
|
|
|
@@ -54,9 +59,7 @@ def select_documents(request: LintRequest) -> DocumentSet:
|
|
|
54
59
|
|
|
55
60
|
def _select_source(repository_root: Path, source: object) -> DocumentSet:
|
|
56
61
|
if isinstance(source, ExplicitFiles):
|
|
57
|
-
all_documents =
|
|
58
|
-
_worktree_document(repository_root, each_path) for each_path in source.paths
|
|
59
|
-
)
|
|
62
|
+
all_documents = _explicit_documents(repository_root, source.paths)
|
|
60
63
|
return DocumentSet(all_documents, SelectionKind.FILES, repository_root)
|
|
61
64
|
if isinstance(source, StagedChanges):
|
|
62
65
|
return _select_changes(repository_root, True, None)
|
|
@@ -89,13 +92,28 @@ def _validated_repository_root(repository_root: Path) -> Path:
|
|
|
89
92
|
return resolved_root
|
|
90
93
|
|
|
91
94
|
|
|
95
|
+
def _explicit_documents(
|
|
96
|
+
repository_root: Path, all_paths: tuple[Path, ...]
|
|
97
|
+
) -> tuple[Document, ...]:
|
|
98
|
+
all_documents: list[Document] = []
|
|
99
|
+
for each_path in all_paths:
|
|
100
|
+
try:
|
|
101
|
+
all_documents.append(_worktree_document(repository_root, each_path))
|
|
102
|
+
except SelectionItemBlocked:
|
|
103
|
+
continue
|
|
104
|
+
return tuple(all_documents)
|
|
105
|
+
|
|
106
|
+
|
|
92
107
|
def _select_repository_tree(repository_root: Path) -> DocumentSet:
|
|
93
|
-
all_documents =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
all_documents: list[Document] = []
|
|
109
|
+
for each_path in _tracked_paths(repository_root):
|
|
110
|
+
if not (repository_root / each_path).is_file():
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
all_documents.append(_tracked_worktree_document(repository_root, each_path))
|
|
114
|
+
except SelectionItemBlocked:
|
|
115
|
+
continue
|
|
116
|
+
return DocumentSet(tuple(all_documents), SelectionKind.REPOSITORY, repository_root)
|
|
99
117
|
|
|
100
118
|
|
|
101
119
|
def _tracked_worktree_document(repository_root: Path, raw_path: str) -> Document:
|
|
@@ -189,13 +207,16 @@ def _append_change(
|
|
|
189
207
|
old_path, new_path = all_paths if status[:1] in {"R", "C"} else (all_paths[0], all_paths[0])
|
|
190
208
|
normalized_old_path = _normalize_path(repository_root, old_path)
|
|
191
209
|
normalized_new_path = _normalize_path(repository_root, new_path)
|
|
192
|
-
|
|
193
|
-
repository_root, old_path, new_path, normalized_old_path,
|
|
194
|
-
normalized_new_path, is_staged, base_revision, status,
|
|
195
|
-
)
|
|
196
|
-
if document is None:
|
|
210
|
+
if status == "D":
|
|
197
211
|
all_deleted_paths.append(normalized_old_path)
|
|
198
212
|
return
|
|
213
|
+
try:
|
|
214
|
+
document = _change_document(
|
|
215
|
+
repository_root, old_path, new_path, normalized_old_path,
|
|
216
|
+
normalized_new_path, is_staged, base_revision, status,
|
|
217
|
+
)
|
|
218
|
+
except (SelectionItemBlocked, GitBlobItemBlocked):
|
|
219
|
+
return
|
|
199
220
|
all_documents.append(document)
|
|
200
221
|
if document.prior_path is not None:
|
|
201
222
|
all_renamed_paths.append((normalized_old_path, normalized_new_path))
|
|
@@ -210,9 +231,7 @@ def _change_document(
|
|
|
210
231
|
is_staged: bool,
|
|
211
232
|
base_revision: str | None,
|
|
212
233
|
status: str,
|
|
213
|
-
) -> Document
|
|
214
|
-
if status == "D":
|
|
215
|
-
return None
|
|
234
|
+
) -> Document:
|
|
216
235
|
is_copy = status[:1] == "C"
|
|
217
236
|
is_rename = status[:1] == "R"
|
|
218
237
|
prior_text = _change_prior_text(
|
|
@@ -274,7 +293,7 @@ def _read_worktree_text(file_path: Path) -> str:
|
|
|
274
293
|
try:
|
|
275
294
|
return file_path.read_bytes().decode(constants.UTF8_ENCODING)
|
|
276
295
|
except UnicodeDecodeError as error:
|
|
277
|
-
raise
|
|
296
|
+
raise SelectionItemBlocked(f"File is not UTF-8: {file_path}") from error
|
|
278
297
|
except OSError as error:
|
|
279
298
|
raise SelectionRunFatal(f"File cannot be read: {file_path}") from error
|
|
280
299
|
|
|
@@ -16,11 +16,23 @@ from .config.constants import (
|
|
|
16
16
|
UTF8_ENCODING,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
_subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
|
|
22
|
+
if _subprocess_window_hooks_directory not in sys.path:
|
|
23
|
+
sys.path.append(_subprocess_window_hooks_directory)
|
|
24
|
+
|
|
25
|
+
from hooks_constants.subprocess_window import hidden_window_creation_flags
|
|
26
|
+
|
|
19
27
|
|
|
20
28
|
class GitSelectionError(ValueError):
|
|
21
29
|
"""Raised when Git cannot provide the requested source bytes."""
|
|
22
30
|
|
|
23
31
|
|
|
32
|
+
class GitBlobItemBlocked(GitSelectionError):
|
|
33
|
+
"""Raised when one Git blob holds bytes that are not UTF-8 text."""
|
|
34
|
+
|
|
35
|
+
|
|
24
36
|
def git_bytes_for(repository_root: Path, all_arguments: tuple[str, ...]) -> bytes:
|
|
25
37
|
"""Run a fixed Git argument vector and return raw stdout bytes.
|
|
26
38
|
|
|
@@ -40,6 +52,7 @@ def git_bytes_for(repository_root: Path, all_arguments: tuple[str, ...]) -> byte
|
|
|
40
52
|
capture_output=True,
|
|
41
53
|
check=False,
|
|
42
54
|
env=_git_subprocess_environment(),
|
|
55
|
+
creationflags=hidden_window_creation_flags(),
|
|
43
56
|
)
|
|
44
57
|
if completed.returncode != 0:
|
|
45
58
|
error_text = completed.stderr.decode(UTF8_ENCODING, errors="replace").strip()
|
|
@@ -65,6 +78,7 @@ def head_revision(repository_root: Path) -> str | None:
|
|
|
65
78
|
capture_output=True,
|
|
66
79
|
check=False,
|
|
67
80
|
env=_git_subprocess_environment(),
|
|
81
|
+
creationflags=hidden_window_creation_flags(),
|
|
68
82
|
)
|
|
69
83
|
if completed.returncode != 0:
|
|
70
84
|
error_text = completed.stderr.decode(UTF8_ENCODING, errors="replace").strip()
|
|
@@ -89,7 +103,7 @@ def read_blob(repository_root: Path, revision: str, relative_path: str) -> str |
|
|
|
89
103
|
Decoded blob text, or None when the blob is absent.
|
|
90
104
|
|
|
91
105
|
Raises:
|
|
92
|
-
|
|
106
|
+
GitBlobItemBlocked: If the blob is not UTF-8.
|
|
93
107
|
"""
|
|
94
108
|
blob_bytes = _read_blob_bytes(repository_root, revision, relative_path)
|
|
95
109
|
if blob_bytes is None:
|
|
@@ -97,7 +111,7 @@ def read_blob(repository_root: Path, revision: str, relative_path: str) -> str |
|
|
|
97
111
|
try:
|
|
98
112
|
return blob_bytes.decode(UTF8_ENCODING)
|
|
99
113
|
except UnicodeDecodeError as error:
|
|
100
|
-
raise
|
|
114
|
+
raise GitBlobItemBlocked(f"Git blob is not UTF-8: {relative_path}") from error
|
|
101
115
|
|
|
102
116
|
|
|
103
117
|
def _git_subprocess_environment() -> dict[str, str]:
|
|
@@ -121,6 +135,7 @@ def _read_blob_bytes(repository_root: Path, revision: str, relative_path: str) -
|
|
|
121
135
|
capture_output=True,
|
|
122
136
|
check=False,
|
|
123
137
|
env=_git_subprocess_environment(),
|
|
138
|
+
creationflags=hidden_window_creation_flags(),
|
|
124
139
|
)
|
|
125
140
|
if completed.returncode == 0:
|
|
126
141
|
return completed.stdout
|
|
@@ -131,5 +131,10 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
|
|
|
131
131
|
"home-path",
|
|
132
132
|
"5d956228802fbd4a65af8fa7fb4183f5a9f8793dbe218877aefe887b905f726d",
|
|
133
133
|
),
|
|
134
|
+
(
|
|
135
|
+
"shared_utils/theme_assets/contracts/evidence/theme_editor_asset_resolution_master.oneui9.v1.json",
|
|
136
|
+
"home-path",
|
|
137
|
+
"ec69fe88e2f9d3f1ed3504532c7484120591cbec7ae6fda017b0f9ca57a3fc25",
|
|
138
|
+
),
|
|
134
139
|
)
|
|
135
140
|
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Repository policy document and exception-entry schema."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
CONFIG_RELATIVE_PATH = "config/repository-policy.json"
|
|
6
|
+
DIGEST_PATTERN = re.compile(r"[0-9a-f]{64}")
|
|
7
|
+
FORBIDDEN_PATH_CHARACTERS = frozenset("\\:*?[]")
|
|
8
|
+
ALL_ENTRY_FIELDS = frozenset({"path", "sha256", "reason"})
|
|
9
|
+
ALL_FORBIDDEN_PATH_SEGMENTS = frozenset({"", ".", ".."})
|
|
10
|
+
FIRST_PRINTABLE_CODEPOINT = 32
|
|
11
|
+
VERSION_FIELD = "version"
|
|
12
|
+
SUPPORTED_DOCUMENT_VERSION = 1
|
|
13
|
+
EMAIL_EXEMPTIONS_FIELD = "email_exemptions"
|
|
14
|
+
PATH_EXEMPTIONS_FIELD = "path_exemptions"
|
|
15
|
+
ALL_EXEMPTION_FIELDS = frozenset({EMAIL_EXEMPTIONS_FIELD, PATH_EXEMPTIONS_FIELD})
|
|
16
|
+
ALL_DOCUMENT_FIELDS = ALL_EXEMPTION_FIELDS | {VERSION_FIELD}
|
|
17
|
+
EMAIL_EXEMPTION_SUBJECT = "Email exemption"
|
|
18
|
+
PATH_EXEMPTION_SUBJECT = "Path exemption"
|
|
19
|
+
EMAIL_CATEGORY = "email"
|
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
|
-
from repository_checks.config.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
from repository_checks.config.policy_document import (
|
|
8
|
+
EMAIL_CATEGORY,
|
|
9
|
+
EMAIL_EXEMPTION_SUBJECT,
|
|
10
|
+
EMAIL_EXEMPTIONS_FIELD,
|
|
11
|
+
)
|
|
12
|
+
from repository_checks.policy_document import (
|
|
13
|
+
PolicyConfigurationRunFatal,
|
|
14
|
+
entry_path_and_digest,
|
|
15
|
+
read_policy_entries,
|
|
15
16
|
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class PolicyConfigurationRunFatal(ValueError):
|
|
19
|
-
"""Reject the entire policy run when exception configuration is invalid."""
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
def load_email_exemptions(repository_root: Path) -> frozenset[tuple[str, str, str]]:
|
|
@@ -31,66 +28,12 @@ def load_email_exemptions(repository_root: Path) -> frozenset[tuple[str, str, st
|
|
|
31
28
|
OSError: Configuration cannot be read.
|
|
32
29
|
"""
|
|
33
30
|
exemptions: set[tuple[str, str, str]] = set()
|
|
34
|
-
for each_entry in
|
|
35
|
-
|
|
31
|
+
for each_entry in read_policy_entries(repository_root, EMAIL_EXEMPTIONS_FIELD):
|
|
32
|
+
relative_path, digest = entry_path_and_digest(
|
|
33
|
+
each_entry, EMAIL_EXEMPTION_SUBJECT
|
|
34
|
+
)
|
|
35
|
+
identity = (relative_path, EMAIL_CATEGORY, digest)
|
|
36
36
|
if identity in exemptions:
|
|
37
37
|
raise PolicyConfigurationRunFatal("Duplicate repository email exemption")
|
|
38
38
|
exemptions.add(identity)
|
|
39
39
|
return frozenset(exemptions)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _read_entries(repository_root: Path) -> list[object]:
|
|
43
|
-
config_path = repository_root / CONFIG_RELATIVE_PATH
|
|
44
|
-
if not config_path.exists() and not config_path.is_symlink():
|
|
45
|
-
return []
|
|
46
|
-
if not config_path.resolve().is_relative_to(repository_root.resolve()):
|
|
47
|
-
raise ValueError(
|
|
48
|
-
"Repository policy configuration must remain inside the repository"
|
|
49
|
-
)
|
|
50
|
-
document = json.loads(
|
|
51
|
-
config_path.read_text(encoding="utf-8"), object_pairs_hook=_unique_fields
|
|
52
|
-
)
|
|
53
|
-
if (
|
|
54
|
-
not isinstance(document, dict)
|
|
55
|
-
or set(document) != {"version", "email_exemptions"}
|
|
56
|
-
or type(document["version"]) is not int
|
|
57
|
-
or document["version"] != 1
|
|
58
|
-
or not isinstance(document["email_exemptions"], list)
|
|
59
|
-
):
|
|
60
|
-
raise ValueError("Invalid repository policy configuration schema")
|
|
61
|
-
return document["email_exemptions"]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _unique_fields(all_pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
65
|
-
fields: dict[str, object] = {}
|
|
66
|
-
for each_key, each_content in all_pairs:
|
|
67
|
-
if each_key in fields:
|
|
68
|
-
raise PolicyConfigurationRunFatal("Duplicate repository policy field")
|
|
69
|
-
fields[each_key] = each_content
|
|
70
|
-
return fields
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _entry_identity(entry: object) -> tuple[str, str, str]:
|
|
74
|
-
if not isinstance(entry, dict) or set(entry) != ALL_ENTRY_FIELDS:
|
|
75
|
-
raise ValueError("Email exemptions require path, sha256, and reason")
|
|
76
|
-
relative_path, digest, reason = entry["path"], entry["sha256"], entry["reason"]
|
|
77
|
-
if not isinstance(relative_path, str) or not relative_path:
|
|
78
|
-
raise ValueError("Email exemption path must name one repository file")
|
|
79
|
-
if (
|
|
80
|
-
any(character in FORBIDDEN_PATH_CHARACTERS for character in relative_path)
|
|
81
|
-
or any(
|
|
82
|
-
segment in ALL_FORBIDDEN_PATH_SEGMENTS
|
|
83
|
-
for segment in relative_path.split("/")
|
|
84
|
-
)
|
|
85
|
-
or any(
|
|
86
|
-
ord(character) < FIRST_PRINTABLE_CODEPOINT for character in relative_path
|
|
87
|
-
)
|
|
88
|
-
):
|
|
89
|
-
raise ValueError("Email exemption path must be a literal relative file path")
|
|
90
|
-
if not isinstance(digest, str) or DIGEST_PATTERN.fullmatch(digest) is None:
|
|
91
|
-
raise ValueError("Email exemption sha256 must contain 64 lowercase hex digits")
|
|
92
|
-
if not isinstance(reason, str) or not reason.strip():
|
|
93
|
-
raise ValueError(
|
|
94
|
-
"Email exemption reason must explain its public contact purpose"
|
|
95
|
-
)
|
|
96
|
-
return relative_path, "email", digest
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Load frozen-file path exceptions owned by a repository."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from repository_checks.config.policy_document import (
|
|
8
|
+
PATH_EXEMPTION_SUBJECT,
|
|
9
|
+
PATH_EXEMPTIONS_FIELD,
|
|
10
|
+
)
|
|
11
|
+
from repository_checks.policy_document import (
|
|
12
|
+
PolicyConfigurationRunFatal,
|
|
13
|
+
entry_path_and_digest,
|
|
14
|
+
read_policy_entries,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_path_exemptions(repository_root: Path) -> frozenset[tuple[str, str]]:
|
|
19
|
+
"""Read validated frozen-file path exceptions.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
repository_root: Repository containing the optional policy configuration.
|
|
23
|
+
Returns:
|
|
24
|
+
Exact path and recorded file-content digest identities.
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: Configuration contains invalid JSON or schema fields.
|
|
27
|
+
OSError: Configuration cannot be read.
|
|
28
|
+
"""
|
|
29
|
+
exemptions: set[tuple[str, str]] = set()
|
|
30
|
+
for each_entry in read_policy_entries(repository_root, PATH_EXEMPTIONS_FIELD):
|
|
31
|
+
identity = entry_path_and_digest(each_entry, PATH_EXEMPTION_SUBJECT)
|
|
32
|
+
if identity in exemptions:
|
|
33
|
+
raise PolicyConfigurationRunFatal("Duplicate repository path exemption")
|
|
34
|
+
exemptions.add(identity)
|
|
35
|
+
return frozenset(exemptions)
|