claude-dev-env 2.34.0 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.agents/skills/autoconverge/SKILL.md +19 -15
  2. package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
  3. package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
  4. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
  5. package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
  6. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
  7. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
  8. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
  9. package/bin/cde.mjs +175 -0
  10. package/bin/cde.test.mjs +222 -0
  11. package/bin/install-constants.mjs +4 -0
  12. package/bin/install.cursor-rules.test.mjs +233 -2
  13. package/bin/install.mjs +49 -3
  14. package/bin/pstack-model-preferences.codex.json +23 -0
  15. package/bin/select-pstack-models.test.mjs +378 -0
  16. package/hooks/hooks_constants/tool_names.py +20 -0
  17. package/hooks/validators/fast_save_validators.py +63 -89
  18. package/hooks/validators/run_all_validators.py +16 -8
  19. package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
  20. package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
  21. package/package.json +3 -2
  22. package/rules/pstack-models.md +119 -0
  23. package/scripts/audit-hooks-cli.mjs +54 -0
  24. package/scripts/audit-hooks.mjs +397 -0
  25. package/scripts/cde_lint.py +258 -0
  26. package/scripts/policy_lint/__init__.py +41 -0
  27. package/scripts/policy_lint/adapter_configuration.py +91 -0
  28. package/scripts/policy_lint/adapter_detectors.py +328 -0
  29. package/scripts/policy_lint/adapter_pairing.py +145 -0
  30. package/scripts/policy_lint/adapter_support.py +97 -0
  31. package/scripts/policy_lint/adapters.py +352 -0
  32. package/scripts/policy_lint/config/__init__.py +1 -0
  33. package/scripts/policy_lint/config/constants.py +66 -0
  34. package/scripts/policy_lint/engine.py +224 -0
  35. package/scripts/policy_lint/model.py +387 -0
  36. package/scripts/policy_lint/registry.py +162 -0
  37. package/scripts/policy_lint/render.py +91 -0
  38. package/scripts/policy_lint/selection.py +372 -0
  39. package/scripts/policy_lint/selection_git.py +147 -0
  40. package/scripts/select_pstack_models.mjs +304 -0
  41. package/scripts/tests/test-audit-hooks.mjs +379 -0
  42. package/scripts/tests/test_cde_lint.py +374 -0
  43. package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
  44. package/scripts/tests/test_policy_lint_rules_registry.py +252 -0
  45. package/scripts/tests/test_policy_lint_selection.py +373 -0
@@ -0,0 +1,352 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import sys
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ from . import adapter_configuration, adapter_detectors, adapter_pairing, adapter_support
9
+ from .config import constants
10
+ from .model import Diagnostic, Document, DocumentSet
11
+
12
+ _document_path = adapter_support._document_path
13
+ _line_number = adapter_support._line_number
14
+ _diagnostics_for_messages = adapter_support._diagnostics_for_messages
15
+ _source_line_for_phrase = adapter_support._source_line_for_phrase
16
+ _diagnostics_for_state_messages = adapter_support._diagnostics_for_state_messages
17
+
18
+
19
+ def _hooks_module(module_name: str) -> ModuleType:
20
+ hooks_directory = str(Path(__file__).resolve().parents[1].parent / "hooks")
21
+ if hooks_directory not in sys.path:
22
+ sys.path.insert(0, hooks_directory)
23
+ return importlib.import_module(module_name)
24
+
25
+
26
+ def validator_diagnostics(
27
+ document: Document, repository_root: Path
28
+ ) -> tuple[Diagnostic, ...]:
29
+ """Run the fast validator set on one document.
30
+
31
+ Args:
32
+ document: Current source text and path.
33
+ repository_root: Request repository root for configuration resolution.
34
+
35
+ Returns:
36
+ Path-normalized diagnostics for failed validators.
37
+ """
38
+ return adapter_detectors.validator_diagnostics(
39
+ document, repository_root, _hooks_module
40
+ )
41
+
42
+
43
+ def code_rule_diagnostics(
44
+ document: Document, repository_root: Path
45
+ ) -> tuple[Diagnostic, ...]:
46
+ """Run the code-rule engine on one document.
47
+
48
+ Args:
49
+ document: Current text and optional prior text.
50
+ repository_root: Request repository root for sibling-file resolution.
51
+
52
+ Returns:
53
+ Diagnostics from the code-rule engine.
54
+ """
55
+ return adapter_detectors.code_rule_diagnostics(
56
+ document, repository_root, _hooks_module
57
+ )
58
+
59
+
60
+ def state_description_diagnostics(
61
+ document: Document, repository_root: Path
62
+ ) -> tuple[Diagnostic, ...]:
63
+ """Run the state-description detector on one document.
64
+
65
+ Args:
66
+ document: Current document text.
67
+ repository_root: Request repository root for document-path resolution.
68
+
69
+ Returns:
70
+ State-description diagnostics.
71
+ """
72
+ return adapter_detectors.state_description_diagnostics(
73
+ document, repository_root, _hooks_module
74
+ )
75
+
76
+
77
+ def plain_language_diagnostics(
78
+ document: Document, repository_root: Path
79
+ ) -> tuple[Diagnostic, ...]:
80
+ """Run the stored-prose detector on one document.
81
+
82
+ Args:
83
+ document: Current document text.
84
+ repository_root: Request repository root for document-path resolution.
85
+
86
+ Returns:
87
+ Plain-language diagnostics.
88
+ """
89
+ return adapter_detectors.plain_language_diagnostics(
90
+ document, repository_root, _hooks_module
91
+ )
92
+
93
+
94
+ def docstring_gate_diagnostics(
95
+ document: Document, repository_root: Path
96
+ ) -> tuple[Diagnostic, ...]:
97
+ """Run the docstring gate-count detector on one document.
98
+
99
+ Args:
100
+ document: Current document text.
101
+ repository_root: Request repository root for document-path resolution.
102
+
103
+ Returns:
104
+ Docstring gate diagnostics.
105
+ """
106
+ return adapter_detectors.docstring_gate_diagnostics(
107
+ document, repository_root, _hooks_module
108
+ )
109
+
110
+
111
+ def workflow_substitution_diagnostics(
112
+ document: Document, repository_root: Path
113
+ ) -> tuple[Diagnostic, ...]:
114
+ """Run the workflow substitution detector on one document.
115
+
116
+ Args:
117
+ document: Current document text.
118
+ repository_root: Request repository root for document-path resolution.
119
+
120
+ Returns:
121
+ Workflow substitution diagnostics.
122
+ """
123
+ return adapter_detectors.workflow_substitution_diagnostics(
124
+ document, repository_root, _hooks_module
125
+ )
126
+
127
+
128
+ def rmtree_diagnostics(
129
+ document: Document, repository_root: Path
130
+ ) -> tuple[Diagnostic, ...]:
131
+ """Run the Windows cleanup detectors on one document.
132
+
133
+ Args:
134
+ document: Current text.
135
+ repository_root: Request repository root for document-path resolution.
136
+
137
+ Returns:
138
+ Cleanup safety diagnostics.
139
+ """
140
+ return adapter_detectors.rmtree_diagnostics(
141
+ document, repository_root, _hooks_module
142
+ )
143
+
144
+
145
+ def subprocess_budget_diagnostics(
146
+ document: Document, repository_root: Path
147
+ ) -> tuple[Diagnostic, ...]:
148
+ """Run the subprocess budget detector on one document.
149
+
150
+ Args:
151
+ document: Current text.
152
+ repository_root: Request repository root for document-path resolution.
153
+
154
+ Returns:
155
+ Subprocess budget diagnostics.
156
+ """
157
+ return adapter_detectors.subprocess_budget_diagnostics(
158
+ document, repository_root, _hooks_module
159
+ )
160
+
161
+
162
+ def hook_prose_diagnostics(
163
+ document: Document, repository_root: Path
164
+ ) -> tuple[Diagnostic, ...]:
165
+ """Run the hook-prose consistency detector on one document.
166
+
167
+ Args:
168
+ document: Current text.
169
+ repository_root: Request repository root for document-path resolution.
170
+
171
+ Returns:
172
+ Hook-prose diagnostics.
173
+ """
174
+ return adapter_detectors.hook_prose_diagnostics(
175
+ document, repository_root, _hooks_module
176
+ )
177
+
178
+
179
+ def open_question_diagnostics(
180
+ document: Document, repository_root: Path
181
+ ) -> tuple[Diagnostic, ...]:
182
+ """Run the open-question plan detector on one document.
183
+
184
+ Args:
185
+ document: Current plan text.
186
+ repository_root: Request repository root for document-path resolution.
187
+
188
+ Returns:
189
+ Plan question diagnostics.
190
+ """
191
+ return adapter_detectors.open_question_diagnostics(
192
+ document, repository_root, _hooks_module
193
+ )
194
+
195
+
196
+ def hook_configuration_diagnostics(
197
+ document: Document, repository_root: Path
198
+ ) -> tuple[Diagnostic, ...]:
199
+ """Check hook configuration for policy decision fields.
200
+
201
+ Args:
202
+ document: Current hook configuration text.
203
+ repository_root: Request repository root for document-path resolution.
204
+
205
+ Returns:
206
+ Hook configuration diagnostics.
207
+ """
208
+ return adapter_configuration.hook_configuration_diagnostics(
209
+ document, repository_root
210
+ )
211
+
212
+
213
+ def test_pairing_diagnostics(document_set: DocumentSet) -> tuple[Diagnostic, ...]:
214
+ """Report changed production files without a changed matching test.
215
+
216
+ Args:
217
+ document_set: Staged or base change selection.
218
+
219
+ Returns:
220
+ Test-pairing diagnostics for unmatched production files.
221
+ """
222
+ return adapter_pairing.test_pairing_diagnostics(document_set, _hooks_module)
223
+
224
+
225
+ def hook_format_diagnostics(
226
+ document: Document, repository_root: Path
227
+ ) -> tuple[Diagnostic, ...]:
228
+ """Check one settings document for a nonportable hook command.
229
+
230
+ Args:
231
+ document: Current settings text and path.
232
+ repository_root: Request repository root for document-path resolution.
233
+
234
+ Returns:
235
+ A diagnostic when the command uses a home-relative Python script path.
236
+ """
237
+ return adapter_detectors.hook_format_diagnostics(
238
+ document, repository_root, _hooks_module
239
+ )
240
+
241
+
242
+ def accepts_python(document: Document) -> bool:
243
+ return document.path.suffix.lower() == constants.PYTHON_SUFFIX
244
+
245
+
246
+ def accepts_code(document: Document) -> bool:
247
+ """Return whether the document contains supported source code.
248
+
249
+ Args:
250
+ document: Candidate document.
251
+
252
+ Returns:
253
+ True for a supported source extension.
254
+ """
255
+ return document.path.suffix.lower() in constants.ALL_CODE_SUFFIXES
256
+
257
+
258
+ def accepts_markdown(document: Document) -> bool:
259
+ return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
260
+
261
+
262
+ def accepts_source_or_markdown(document: Document) -> bool:
263
+ """Return whether a state-description rule can inspect the document.
264
+
265
+ Args:
266
+ document: Candidate document.
267
+
268
+ Returns:
269
+ True for supported source or Markdown.
270
+ """
271
+ return accepts_code(document) or accepts_markdown(document)
272
+
273
+
274
+ def accepts_stored_prompt(document: Document) -> bool:
275
+ """Return whether the document is stored prompt prose.
276
+
277
+ Args:
278
+ document: Candidate document.
279
+
280
+ Returns:
281
+ True for Markdown in an instruction directory.
282
+ """
283
+ normalized_path = f"/{document.path.as_posix().lower()}"
284
+ return accepts_markdown(document) and any(
285
+ each_segment in normalized_path
286
+ for each_segment in constants.ALL_STORED_PROMPT_SEGMENTS
287
+ )
288
+
289
+
290
+ def accepts_hook_configuration(document: Document) -> bool:
291
+ return document.path.name == "hooks.json"
292
+
293
+
294
+ def accepts_hook_format(document: Document) -> bool:
295
+ """Return whether the document is Claude settings JSON.
296
+
297
+ Args:
298
+ document: Candidate document.
299
+
300
+ Returns:
301
+ True for ``settings.json`` under a ``.claude`` directory.
302
+ """
303
+ settings_file_name = "settings.json"
304
+ claude_directory_segment = "/.claude/"
305
+ normalized_path = f"/{document.path.as_posix().lower()}"
306
+ return (
307
+ document.path.name.lower() == settings_file_name
308
+ and claude_directory_segment in normalized_path
309
+ )
310
+
311
+
312
+ def accepts_workflow(document: Document) -> bool:
313
+ """Return whether the document is a workflow substitution template.
314
+
315
+ Args:
316
+ document: Candidate document.
317
+
318
+ Returns:
319
+ True for a ``.workflow.js`` path.
320
+ """
321
+ workflow_constants = _hooks_module(
322
+ "hooks_constants.workflow_substitution_slot_blocker_constants"
323
+ )
324
+ return document.path.as_posix().lower().endswith(
325
+ workflow_constants.WORKFLOW_FILE_SUFFIX
326
+ )
327
+
328
+
329
+ def accepts_plans(document: Document) -> bool:
330
+ """Return whether the document lives in a plan directory.
331
+
332
+ Args:
333
+ document: Candidate document.
334
+
335
+ Returns:
336
+ True for repository ``docs/plans`` or ``.claude/plans`` paths.
337
+ """
338
+ plans_constants = _hooks_module(
339
+ "hooks_constants.open_questions_in_plans_blocker_constants"
340
+ )
341
+ normalized_path = f"/{document.path.as_posix().lower()}"
342
+ relative_path = document.path.as_posix().lower()
343
+ return (
344
+ plans_constants.PLANS_PATH_SEGMENT in normalized_path
345
+ or plans_constants.DOCS_PLANS_PATH_SEGMENT in normalized_path
346
+ or relative_path.startswith(
347
+ (
348
+ plans_constants.PLANS_PATH_PREFIX,
349
+ plans_constants.DOCS_PLANS_PATH_PREFIX,
350
+ )
351
+ )
352
+ )
@@ -0,0 +1 @@
1
+ """Configuration values for the policy linter."""
@@ -0,0 +1,66 @@
1
+ INCOMPLETE_EXIT_CODE = 3
2
+ INVALID_INPUT_EXIT_CODE = 2
3
+ GIT_EXECUTABLE = "git"
4
+ UTF8_ENCODING = "utf-8"
5
+ PATH_SEPARATOR = "/"
6
+ NUL_BYTE = b"\0"
7
+ ALL_GIT_ROOT_ARGUMENTS = ("rev-parse", "--show-toplevel")
8
+ ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "--verify", "HEAD")
9
+ ALL_UNBORN_HEAD_ERROR_FRAGMENTS = ("needed a single revision",)
10
+ GIT_MERGE_BASE_ARGUMENT = "merge-base"
11
+ GIT_HEAD_REFERENCE = "HEAD"
12
+ GIT_INDEX_REFERENCE_PREFIX = ""
13
+ GIT_ENVIRONMENT_VARIABLE_PREFIX = "GIT_"
14
+ ALL_GIT_BLOB_ARGUMENTS = ("cat-file", "blob")
15
+ ALL_MISSING_BLOB_ERROR_FRAGMENTS = (
16
+ "does not exist in",
17
+ "does not exist (at stage",
18
+ "exists on disk, but not in",
19
+ )
20
+ GIT_DIFF_ARGUMENT = "diff"
21
+ GIT_CACHED_FLAG = "--cached"
22
+ GIT_NAME_STATUS_FLAG = "--name-status"
23
+ GIT_FIND_RENAMES_FLAG = "--find-renames"
24
+ GIT_ZERO_TERMINATED_FLAG = "-z"
25
+ GIT_FILES_ARGUMENT = "ls-files"
26
+ GIT_SEPARATOR = "--"
27
+ ALL_DIFF_CHANGED_TAGS = frozenset({"replace", "insert"})
28
+ NEW_DOCUMENT_ORIGIN_LINE = 1
29
+ RENAME_PATH_COUNT = 2
30
+ PYTHON_SUFFIX = ".py"
31
+ ALL_CODE_SUFFIXES = frozenset({".py", ".js", ".jsx", ".ts", ".tsx", ".mjs"})
32
+ ALL_MARKDOWN_SUFFIXES = frozenset({".md", ".mdx"})
33
+ ALL_STORED_PROMPT_SEGMENTS = (
34
+ "/.agents/",
35
+ "/commands/",
36
+ "/rules/",
37
+ "/system-prompts/",
38
+ )
39
+ ALL_HOOK_REGISTRATION_KEYS = frozenset({"command", "path", "script", "entrypoint"})
40
+ ALL_DECISION_REGISTRATION_KEYS = frozenset(
41
+ {"permissionDecision", "permission_decision", "decision"}
42
+ )
43
+ ALL_ACTION_BOUNDARY_SEGMENTS = frozenset(
44
+ {
45
+ "deny",
46
+ "block",
47
+ "ask",
48
+ "blocking",
49
+ "blocker",
50
+ "permissiondecision",
51
+ "permission_decision",
52
+ "pre_tool_use_dispatcher",
53
+ }
54
+ )
55
+ ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
56
+ ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
57
+ ALL_TEST_FILE_PREFIXES = ("test_",)
58
+ ALL_TEST_FILE_SUFFIXES = ("_test.py",)
59
+ POLICY_LINT_DIRECTORY_NAME = "policy_lint"
60
+ POLICY_LINT_RULES_TEST_PREFIX = "test_policy_lint_rules"
61
+ POLICY_LINT_SELECTION_TEST_PREFIX = "test_policy_lint_selection"
62
+ RUN_ALL_VALIDATORS_STEM = "run_all_validators"
63
+ FAST_SAVE_VALIDATORS_STEM = "fast_save_validators"
64
+ ALL_OVERLAPPING_VALIDATOR_NAMES = frozenset(
65
+ {"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
66
+ )
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Sequence
4
+ from pathlib import Path
5
+
6
+ from .model import (
7
+ ChangeSetRule,
8
+ Diagnostic,
9
+ Document,
10
+ DocumentRule,
11
+ DocumentSet,
12
+ LintReport,
13
+ LintRequest,
14
+ RepositoryRule,
15
+ Rule,
16
+ SelectionKind,
17
+ )
18
+ from .registry import default_registry, selected_rules
19
+ from .selection import select_documents
20
+
21
+
22
+ def lint(
23
+ request: LintRequest,
24
+ all_registry: Sequence[Rule] | None = None,
25
+ ) -> LintReport:
26
+ """Run the selected rules on one source selection.
27
+
28
+ Args:
29
+ request: Repository root, source, and rule sets.
30
+ all_registry: Optional registry replacement.
31
+
32
+ Returns:
33
+ A report of diagnostics and rule execution state.
34
+ """
35
+ document_set = select_documents(request)
36
+ candidate_rules = tuple(all_registry) if all_registry is not None else default_registry()
37
+ selected = selected_rules(candidate_rules, request.rule_sets)
38
+ all_diagnostics, executed, failed, skipped = _run_selected_rules(selected, document_set)
39
+ return LintReport(
40
+ 1,
41
+ tuple(sorted(all_diagnostics, key=_diagnostic_sort_key)),
42
+ tuple(each_document.path for each_document in document_set.documents),
43
+ executed,
44
+ failed,
45
+ skipped,
46
+ )
47
+
48
+
49
+ def _run_selected_rules(
50
+ all_rules: tuple[Rule, ...], document_set: DocumentSet
51
+ ) -> tuple[list[Diagnostic], tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
52
+ resolved_repository_root = document_set.repository_root.resolve()
53
+ all_diagnostics: list[Diagnostic] = []
54
+ all_executed_rules: list[str] = []
55
+ all_failed_rules: list[str] = []
56
+ all_skipped_rules: list[str] = []
57
+ for each_rule in all_rules:
58
+ _run_rule(
59
+ each_rule,
60
+ document_set,
61
+ resolved_repository_root,
62
+ all_diagnostics,
63
+ all_executed_rules,
64
+ all_failed_rules,
65
+ all_skipped_rules,
66
+ )
67
+ return (
68
+ all_diagnostics,
69
+ tuple(all_executed_rules),
70
+ tuple(all_failed_rules),
71
+ tuple(all_skipped_rules),
72
+ )
73
+
74
+
75
+ def _run_rule(
76
+ rule: Rule,
77
+ document_set: DocumentSet,
78
+ resolved_repository_root: Path,
79
+ all_diagnostics: list[Diagnostic],
80
+ all_executed_rules: list[str],
81
+ all_failed_rules: list[str],
82
+ all_skipped_rules: list[str],
83
+ ) -> None:
84
+ pending_diagnostics: list[Diagnostic] = []
85
+ all_skipped_before = len(all_skipped_rules)
86
+ if _rule_execution_failed(
87
+ rule,
88
+ document_set,
89
+ resolved_repository_root,
90
+ pending_diagnostics,
91
+ all_skipped_rules,
92
+ ):
93
+ all_failed_rules.append(rule.rule_id)
94
+ return
95
+ if len(all_skipped_rules) != all_skipped_before:
96
+ return
97
+ all_diagnostics.extend(pending_diagnostics)
98
+ all_executed_rules.append(rule.rule_id)
99
+
100
+
101
+ def _rule_execution_failed(
102
+ rule: Rule,
103
+ document_set: DocumentSet,
104
+ resolved_repository_root: Path,
105
+ all_diagnostics: list[Diagnostic],
106
+ all_skipped_rules: list[str],
107
+ ) -> bool:
108
+ try:
109
+ _execute_rule(
110
+ rule,
111
+ document_set,
112
+ resolved_repository_root,
113
+ all_diagnostics,
114
+ all_skipped_rules,
115
+ )
116
+ except (
117
+ AttributeError,
118
+ ImportError,
119
+ KeyError,
120
+ OSError,
121
+ RuntimeError,
122
+ SyntaxError,
123
+ TypeError,
124
+ UnicodeError,
125
+ ValueError,
126
+ ):
127
+ return True
128
+ return False
129
+
130
+
131
+ def _execute_rule(
132
+ rule: Rule,
133
+ document_set: DocumentSet,
134
+ resolved_repository_root: Path,
135
+ all_diagnostics: list[Diagnostic],
136
+ all_skipped_rules: list[str],
137
+ ) -> None:
138
+ if isinstance(rule, DocumentRule):
139
+ _run_document_rule(
140
+ rule,
141
+ document_set,
142
+ resolved_repository_root,
143
+ all_diagnostics,
144
+ all_skipped_rules,
145
+ )
146
+ return
147
+ if isinstance(rule, ChangeSetRule):
148
+ _run_changeset_rule(rule, document_set, all_diagnostics, all_skipped_rules)
149
+ return
150
+ _run_repository_rule(rule, document_set, all_diagnostics, all_skipped_rules)
151
+
152
+
153
+ def _run_document_rule(
154
+ rule: DocumentRule,
155
+ document_set: DocumentSet,
156
+ resolved_repository_root: Path,
157
+ all_diagnostics: list[Diagnostic],
158
+ all_skipped_rules: list[str],
159
+ ) -> None:
160
+ all_documents = tuple(
161
+ each_document
162
+ for each_document in document_set.documents
163
+ if rule.accepts(each_document)
164
+ )
165
+ if not all_documents:
166
+ all_skipped_rules.append(rule.rule_id)
167
+ return
168
+ for each_document in all_documents:
169
+ all_diagnostics.extend(
170
+ _scoped_diagnostics(
171
+ rule.check(each_document, resolved_repository_root),
172
+ each_document,
173
+ )
174
+ )
175
+
176
+
177
+ def _run_changeset_rule(
178
+ rule: ChangeSetRule,
179
+ document_set: DocumentSet,
180
+ all_diagnostics: list[Diagnostic],
181
+ all_skipped_rules: list[str],
182
+ ) -> None:
183
+ if document_set.selection not in {SelectionKind.STAGED, SelectionKind.BASE}:
184
+ all_skipped_rules.append(rule.rule_id)
185
+ return
186
+ all_diagnostics.extend(rule.check(document_set))
187
+
188
+
189
+ def _run_repository_rule(
190
+ rule: RepositoryRule,
191
+ document_set: DocumentSet,
192
+ all_diagnostics: list[Diagnostic],
193
+ all_skipped_rules: list[str],
194
+ ) -> None:
195
+ if document_set.selection is not SelectionKind.REPOSITORY:
196
+ all_skipped_rules.append(rule.rule_id)
197
+ return
198
+ all_diagnostics.extend(rule.check(document_set))
199
+
200
+
201
+ def _scoped_diagnostics(
202
+ all_diagnostics: Iterable[Diagnostic],
203
+ document: Document,
204
+ ) -> tuple[Diagnostic, ...]:
205
+ if document.changed_lines is None or not document.changed_lines:
206
+ return tuple(all_diagnostics)
207
+ return tuple(
208
+ each_diagnostic
209
+ for each_diagnostic in all_diagnostics
210
+ if each_diagnostic.location is None
211
+ or each_diagnostic.location.start_line in document.changed_lines
212
+ )
213
+
214
+
215
+ def _diagnostic_sort_key(diagnostic: Diagnostic) -> tuple[str, int, int, str, str]:
216
+ if diagnostic.location is None:
217
+ return ("", 0, 0, diagnostic.rule_id, diagnostic.message)
218
+ return (
219
+ diagnostic.location.path.as_posix(),
220
+ diagnostic.location.start_line,
221
+ diagnostic.location.start_column,
222
+ diagnostic.rule_id,
223
+ diagnostic.message,
224
+ )