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.
Files changed (99) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/autoconverge/SKILL.md +19 -15
  3. package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
  4. package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
  5. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
  6. package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
  7. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
  8. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
  9. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
  10. package/.agents/skills/team-advisor/SKILL.md +11 -11
  11. package/_shared/advisor/advisor-protocol.md +51 -51
  12. package/_shared/advisor/reference/advisor-block.md +9 -9
  13. package/_shared/advisor/reference/astra-rung.md +39 -0
  14. package/_shared/advisor/reference/identity.md +4 -4
  15. package/_shared/advisor/reference/lifecycle.md +6 -6
  16. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  17. package/_shared/advisor/reference/third-party-bind.md +11 -11
  18. package/_shared/advisor/reference/warm-up.md +13 -13
  19. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  20. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  21. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  22. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  24. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  25. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  26. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  28. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  29. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  30. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  31. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  32. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  33. package/bin/cde.mjs +175 -0
  34. package/bin/cde.test.mjs +222 -0
  35. package/bin/install-constants.mjs +4 -0
  36. package/bin/install.cursor-rules.test.mjs +233 -2
  37. package/bin/install.mjs +49 -3
  38. package/bin/pstack-model-preferences.codex.json +23 -0
  39. package/bin/select-pstack-models.test.mjs +378 -0
  40. package/docs/references/team-advisor-skill.md +3 -3
  41. package/hooks/hooks_constants/tool_names.py +20 -0
  42. package/hooks/validators/README.md +35 -1
  43. package/hooks/validators/fast_save_validators.py +63 -89
  44. package/hooks/validators/run_all_validators.py +16 -8
  45. package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
  46. package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
  47. package/package.json +3 -2
  48. package/rules/pstack-models.md +119 -0
  49. package/scripts/cde_lint.py +258 -0
  50. package/scripts/check.ps1 +18 -6
  51. package/scripts/policy_lint/__init__.py +41 -0
  52. package/scripts/policy_lint/adapter_configuration.py +91 -0
  53. package/scripts/policy_lint/adapter_detectors.py +368 -0
  54. package/scripts/policy_lint/adapter_pairing.py +145 -0
  55. package/scripts/policy_lint/adapter_support.py +97 -0
  56. package/scripts/policy_lint/adapters.py +376 -0
  57. package/scripts/policy_lint/config/__init__.py +1 -0
  58. package/scripts/policy_lint/config/constants.py +77 -0
  59. package/scripts/policy_lint/engine.py +224 -0
  60. package/scripts/policy_lint/model.py +388 -0
  61. package/scripts/policy_lint/registry.py +171 -0
  62. package/scripts/policy_lint/render.py +91 -0
  63. package/scripts/policy_lint/selection.py +372 -0
  64. package/scripts/policy_lint/selection_git.py +147 -0
  65. package/scripts/repository_checks/__init__.py +1 -0
  66. package/scripts/repository_checks/claude_md.py +108 -0
  67. package/scripts/repository_checks/config/__init__.py +1 -0
  68. package/scripts/repository_checks/config/constants.py +150 -0
  69. package/scripts/repository_checks/env_var_documentation.py +243 -0
  70. package/scripts/repository_checks/hook_modules.py +34 -0
  71. package/scripts/repository_checks/models.py +37 -0
  72. package/scripts/repository_checks/package_inventory.py +118 -0
  73. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  74. package/scripts/repository_checks/reporting.py +39 -0
  75. package/scripts/repository_checks/runner.py +93 -0
  76. package/scripts/repository_checks/tracked_secrets.py +147 -0
  77. package/scripts/repository_checks/tracked_tree.py +28 -0
  78. package/scripts/repository_policy.py +120 -0
  79. package/scripts/select_pstack_models.mjs +304 -0
  80. package/scripts/tests/repository_policy_test_support.py +89 -0
  81. package/scripts/tests/test_cde_lint.py +398 -0
  82. package/scripts/tests/test_claude_md.py +68 -0
  83. package/scripts/tests/test_env_var_documentation.py +133 -0
  84. package/scripts/tests/test_hook_modules.py +17 -0
  85. package/scripts/tests/test_models.py +26 -0
  86. package/scripts/tests/test_package_inventory.py +96 -0
  87. package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
  88. package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
  89. package/scripts/tests/test_policy_lint_selection.py +373 -0
  90. package/scripts/tests/test_pytest_testpaths.py +66 -0
  91. package/scripts/tests/test_reporting.py +54 -0
  92. package/scripts/tests/test_repository_policy.py +71 -0
  93. package/scripts/tests/test_runner.py +59 -0
  94. package/scripts/tests/test_tracked_secrets.py +175 -0
  95. package/scripts/tests/test_tracked_tree.py +28 -0
  96. package/_shared/advisor/reference/sol-rung.md +0 -39
  97. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  98. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  99. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
@@ -0,0 +1,376 @@
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 _pr_loop_script_module(module_name: str) -> ModuleType:
27
+ package_directory = Path(__file__).resolve().parent.parent.parent
28
+ scripts_directory = str(
29
+ package_directory.joinpath(*constants.ALL_PR_LOOP_SCRIPTS_PATH_SEGMENTS)
30
+ )
31
+ if scripts_directory not in sys.path:
32
+ sys.path.insert(0, scripts_directory)
33
+ return importlib.import_module(module_name)
34
+
35
+
36
+ def validator_diagnostics(
37
+ document: Document, repository_root: Path
38
+ ) -> tuple[Diagnostic, ...]:
39
+ """Run the fast validator set on one document.
40
+
41
+ Args:
42
+ document: Current source text and path.
43
+ repository_root: Request repository root for configuration resolution.
44
+
45
+ Returns:
46
+ Path-normalized diagnostics for failed validators.
47
+ """
48
+ return adapter_detectors.validator_diagnostics(
49
+ document, repository_root, _hooks_module
50
+ )
51
+
52
+
53
+ def code_rule_diagnostics(
54
+ document: Document, repository_root: Path
55
+ ) -> tuple[Diagnostic, ...]:
56
+ """Run the code-rule engine on one document.
57
+
58
+ Args:
59
+ document: Current text and optional prior text.
60
+ repository_root: Request repository root for sibling-file resolution.
61
+
62
+ Returns:
63
+ Diagnostics from the code-rule engine.
64
+ """
65
+ return adapter_detectors.code_rule_diagnostics(
66
+ document, repository_root, _hooks_module
67
+ )
68
+
69
+
70
+ def state_description_diagnostics(
71
+ document: Document, repository_root: Path
72
+ ) -> tuple[Diagnostic, ...]:
73
+ """Run the state-description detector on one document.
74
+
75
+ Args:
76
+ document: Current document text.
77
+ repository_root: Request repository root for document-path resolution.
78
+
79
+ Returns:
80
+ State-description diagnostics.
81
+ """
82
+ return adapter_detectors.state_description_diagnostics(
83
+ document, repository_root, _hooks_module
84
+ )
85
+
86
+
87
+ def plain_language_diagnostics(
88
+ document: Document, repository_root: Path
89
+ ) -> tuple[Diagnostic, ...]:
90
+ """Run the stored-prose detector on one document.
91
+
92
+ Args:
93
+ document: Current document text.
94
+ repository_root: Request repository root for document-path resolution.
95
+
96
+ Returns:
97
+ Plain-language diagnostics.
98
+ """
99
+ return adapter_detectors.plain_language_diagnostics(
100
+ document, repository_root, _hooks_module
101
+ )
102
+
103
+
104
+ def docstring_gate_diagnostics(
105
+ document: Document, repository_root: Path
106
+ ) -> tuple[Diagnostic, ...]:
107
+ """Run the docstring gate-count detector on one document.
108
+
109
+ Args:
110
+ document: Current document text.
111
+ repository_root: Request repository root for document-path resolution.
112
+
113
+ Returns:
114
+ Docstring gate diagnostics.
115
+ """
116
+ return adapter_detectors.docstring_gate_diagnostics(
117
+ document, repository_root, _hooks_module
118
+ )
119
+
120
+
121
+ def workflow_substitution_diagnostics(
122
+ document: Document, repository_root: Path
123
+ ) -> tuple[Diagnostic, ...]:
124
+ """Run the workflow substitution detector on one document.
125
+
126
+ Args:
127
+ document: Current document text.
128
+ repository_root: Request repository root for document-path resolution.
129
+
130
+ Returns:
131
+ Workflow substitution diagnostics.
132
+ """
133
+ return adapter_detectors.workflow_substitution_diagnostics(
134
+ document, repository_root, _hooks_module
135
+ )
136
+
137
+
138
+ def rmtree_diagnostics(
139
+ document: Document, repository_root: Path
140
+ ) -> tuple[Diagnostic, ...]:
141
+ """Run the Windows cleanup detectors on one document.
142
+
143
+ Args:
144
+ document: Current text.
145
+ repository_root: Request repository root for document-path resolution.
146
+
147
+ Returns:
148
+ Cleanup safety diagnostics.
149
+ """
150
+ return adapter_detectors.rmtree_diagnostics(
151
+ document, repository_root, _hooks_module
152
+ )
153
+
154
+
155
+ def subprocess_budget_diagnostics(
156
+ document: Document, repository_root: Path
157
+ ) -> tuple[Diagnostic, ...]:
158
+ """Run the subprocess budget detector on one document.
159
+
160
+ Args:
161
+ document: Current text.
162
+ repository_root: Request repository root for document-path resolution.
163
+
164
+ Returns:
165
+ Subprocess budget diagnostics.
166
+ """
167
+ return adapter_detectors.subprocess_budget_diagnostics(
168
+ document, repository_root, _hooks_module
169
+ )
170
+
171
+
172
+ def hook_prose_diagnostics(
173
+ document: Document, repository_root: Path
174
+ ) -> tuple[Diagnostic, ...]:
175
+ """Run the hook-prose consistency detector on one document.
176
+
177
+ Args:
178
+ document: Current text.
179
+ repository_root: Request repository root for document-path resolution.
180
+
181
+ Returns:
182
+ Hook-prose diagnostics.
183
+ """
184
+ return adapter_detectors.hook_prose_diagnostics(
185
+ document, repository_root, _hooks_module
186
+ )
187
+
188
+
189
+ def open_question_diagnostics(
190
+ document: Document, repository_root: Path
191
+ ) -> tuple[Diagnostic, ...]:
192
+ """Run the open-question plan detector on one document.
193
+
194
+ Args:
195
+ document: Current plan text.
196
+ repository_root: Request repository root for document-path resolution.
197
+
198
+ Returns:
199
+ Plan question diagnostics.
200
+ """
201
+ return adapter_detectors.open_question_diagnostics(
202
+ document, repository_root, _hooks_module
203
+ )
204
+
205
+
206
+ def hook_configuration_diagnostics(
207
+ document: Document, repository_root: Path
208
+ ) -> tuple[Diagnostic, ...]:
209
+ """Check hook configuration for policy decision fields.
210
+
211
+ Args:
212
+ document: Current hook configuration text.
213
+ repository_root: Request repository root for document-path resolution.
214
+
215
+ Returns:
216
+ Hook configuration diagnostics.
217
+ """
218
+ return adapter_configuration.hook_configuration_diagnostics(
219
+ document, repository_root
220
+ )
221
+
222
+
223
+ def test_pairing_diagnostics(document_set: DocumentSet) -> tuple[Diagnostic, ...]:
224
+ """Report changed production files without a changed matching test.
225
+
226
+ Args:
227
+ document_set: Staged or base change selection.
228
+
229
+ Returns:
230
+ Test-pairing diagnostics for unmatched production files.
231
+ """
232
+ return adapter_pairing.test_pairing_diagnostics(document_set, _hooks_module)
233
+
234
+
235
+ def terminology_diagnostics(document_set: DocumentSet) -> tuple[Diagnostic, ...]:
236
+ """Report staged prose that near-misses a newly introduced identifier.
237
+
238
+ Args:
239
+ document_set: Candidate staged documents and repository root.
240
+
241
+ Returns:
242
+ Diagnostics that name the prose file and line.
243
+ """
244
+ return adapter_detectors.terminology_diagnostics(
245
+ document_set, _pr_loop_script_module
246
+ )
247
+
248
+
249
+ def hook_format_diagnostics(
250
+ document: Document, repository_root: Path
251
+ ) -> tuple[Diagnostic, ...]:
252
+ """Check one settings document for a nonportable hook command.
253
+
254
+ Args:
255
+ document: Current settings text and path.
256
+ repository_root: Request repository root for document-path resolution.
257
+
258
+ Returns:
259
+ A diagnostic when the command uses a home-relative Python script path.
260
+ """
261
+ return adapter_detectors.hook_format_diagnostics(
262
+ document, repository_root, _hooks_module
263
+ )
264
+
265
+
266
+ def accepts_python(document: Document) -> bool:
267
+ return document.path.suffix.lower() == constants.PYTHON_SUFFIX
268
+
269
+
270
+ def accepts_code(document: Document) -> bool:
271
+ """Return whether the document contains supported source code.
272
+
273
+ Args:
274
+ document: Candidate document.
275
+
276
+ Returns:
277
+ True for a supported source extension.
278
+ """
279
+ return document.path.suffix.lower() in constants.ALL_CODE_SUFFIXES
280
+
281
+
282
+ def accepts_markdown(document: Document) -> bool:
283
+ return document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
284
+
285
+
286
+ def accepts_source_or_markdown(document: Document) -> bool:
287
+ """Return whether a state-description rule can inspect the document.
288
+
289
+ Args:
290
+ document: Candidate document.
291
+
292
+ Returns:
293
+ True for supported source or Markdown.
294
+ """
295
+ return accepts_code(document) or accepts_markdown(document)
296
+
297
+
298
+ def accepts_stored_prompt(document: Document) -> bool:
299
+ """Return whether the document is stored prompt prose.
300
+
301
+ Args:
302
+ document: Candidate document.
303
+
304
+ Returns:
305
+ True for Markdown in an instruction directory.
306
+ """
307
+ normalized_path = f"/{document.path.as_posix().lower()}"
308
+ return accepts_markdown(document) and any(
309
+ each_segment in normalized_path
310
+ for each_segment in constants.ALL_STORED_PROMPT_SEGMENTS
311
+ )
312
+
313
+
314
+ def accepts_hook_configuration(document: Document) -> bool:
315
+ return document.path.name == "hooks.json"
316
+
317
+
318
+ def accepts_hook_format(document: Document) -> bool:
319
+ """Return whether the document is Claude settings JSON.
320
+
321
+ Args:
322
+ document: Candidate document.
323
+
324
+ Returns:
325
+ True for ``settings.json`` under a ``.claude`` directory.
326
+ """
327
+ settings_file_name = "settings.json"
328
+ claude_directory_segment = "/.claude/"
329
+ normalized_path = f"/{document.path.as_posix().lower()}"
330
+ return (
331
+ document.path.name.lower() == settings_file_name
332
+ and claude_directory_segment in normalized_path
333
+ )
334
+
335
+
336
+ def accepts_workflow(document: Document) -> bool:
337
+ """Return whether the document is a workflow substitution template.
338
+
339
+ Args:
340
+ document: Candidate document.
341
+
342
+ Returns:
343
+ True for a ``.workflow.js`` path.
344
+ """
345
+ workflow_constants = _hooks_module(
346
+ "hooks_constants.workflow_substitution_slot_blocker_constants"
347
+ )
348
+ return document.path.as_posix().lower().endswith(
349
+ workflow_constants.WORKFLOW_FILE_SUFFIX
350
+ )
351
+
352
+
353
+ def accepts_plans(document: Document) -> bool:
354
+ """Return whether the document lives in a plan directory.
355
+
356
+ Args:
357
+ document: Candidate document.
358
+
359
+ Returns:
360
+ True for repository ``docs/plans`` or ``.claude/plans`` paths.
361
+ """
362
+ plans_constants = _hooks_module(
363
+ "hooks_constants.open_questions_in_plans_blocker_constants"
364
+ )
365
+ normalized_path = f"/{document.path.as_posix().lower()}"
366
+ relative_path = document.path.as_posix().lower()
367
+ return (
368
+ plans_constants.PLANS_PATH_SEGMENT in normalized_path
369
+ or plans_constants.DOCS_PLANS_PATH_SEGMENT in normalized_path
370
+ or relative_path.startswith(
371
+ (
372
+ plans_constants.PLANS_PATH_PREFIX,
373
+ plans_constants.DOCS_PLANS_PATH_PREFIX,
374
+ )
375
+ )
376
+ )
@@ -0,0 +1 @@
1
+ """Configuration values for the policy linter."""
@@ -0,0 +1,77 @@
1
+ import re
2
+
3
+ INCOMPLETE_EXIT_CODE = 3
4
+ INVALID_INPUT_EXIT_CODE = 2
5
+ GIT_EXECUTABLE = "git"
6
+ UTF8_ENCODING = "utf-8"
7
+ PATH_SEPARATOR = "/"
8
+ NUL_BYTE = b"\0"
9
+ ALL_GIT_ROOT_ARGUMENTS = ("rev-parse", "--show-toplevel")
10
+ ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "--verify", "HEAD")
11
+ ALL_UNBORN_HEAD_ERROR_FRAGMENTS = ("needed a single revision",)
12
+ GIT_MERGE_BASE_ARGUMENT = "merge-base"
13
+ GIT_HEAD_REFERENCE = "HEAD"
14
+ GIT_INDEX_REFERENCE_PREFIX = ""
15
+ GIT_ENVIRONMENT_VARIABLE_PREFIX = "GIT_"
16
+ ALL_GIT_BLOB_ARGUMENTS = ("cat-file", "blob")
17
+ ALL_MISSING_BLOB_ERROR_FRAGMENTS = (
18
+ "does not exist in",
19
+ "does not exist (at stage",
20
+ "exists on disk, but not in",
21
+ )
22
+ GIT_DIFF_ARGUMENT = "diff"
23
+ GIT_CACHED_FLAG = "--cached"
24
+ GIT_NAME_STATUS_FLAG = "--name-status"
25
+ GIT_FIND_RENAMES_FLAG = "--find-renames"
26
+ GIT_ZERO_TERMINATED_FLAG = "-z"
27
+ GIT_FILES_ARGUMENT = "ls-files"
28
+ GIT_SEPARATOR = "--"
29
+ ALL_DIFF_CHANGED_TAGS = frozenset({"replace", "insert"})
30
+ NEW_DOCUMENT_ORIGIN_LINE = 1
31
+ RENAME_PATH_COUNT = 2
32
+ PYTHON_SUFFIX = ".py"
33
+ ALL_CODE_SUFFIXES = frozenset({".py", ".js", ".jsx", ".ts", ".tsx", ".mjs"})
34
+ ALL_MARKDOWN_SUFFIXES = frozenset({".md", ".mdx"})
35
+ ALL_STORED_PROMPT_SEGMENTS = (
36
+ "/.agents/",
37
+ "/commands/",
38
+ "/rules/",
39
+ "/system-prompts/",
40
+ )
41
+ ALL_HOOK_REGISTRATION_KEYS = frozenset({"command", "path", "script", "entrypoint"})
42
+ ALL_DECISION_REGISTRATION_KEYS = frozenset(
43
+ {"permissionDecision", "permission_decision", "decision"}
44
+ )
45
+ ALL_ACTION_BOUNDARY_SEGMENTS = frozenset(
46
+ {
47
+ "deny",
48
+ "block",
49
+ "ask",
50
+ "blocking",
51
+ "blocker",
52
+ "permissiondecision",
53
+ "permission_decision",
54
+ "pre_tool_use_dispatcher",
55
+ }
56
+ )
57
+ ALL_ACTION_BOUNDARY_PREFIXES = ("deny_", "block_", "ask_")
58
+ ALL_TEST_DIRECTORY_NAMES = frozenset({"tests"})
59
+ ALL_TEST_FILE_PREFIXES = ("test_",)
60
+ ALL_TEST_FILE_SUFFIXES = ("_test.py",)
61
+ POLICY_LINT_DIRECTORY_NAME = "policy_lint"
62
+ POLICY_LINT_RULES_TEST_PREFIX = "test_policy_lint_rules"
63
+ POLICY_LINT_SELECTION_TEST_PREFIX = "test_policy_lint_selection"
64
+ RUN_ALL_VALIDATORS_STEM = "run_all_validators"
65
+ FAST_SAVE_VALIDATORS_STEM = "fast_save_validators"
66
+ ALL_OVERLAPPING_VALIDATOR_NAMES = frozenset(
67
+ {"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
68
+ )
69
+ ALL_PR_LOOP_SCRIPTS_PATH_SEGMENTS = ("_shared", "pr-loop", "scripts")
70
+ TERMINOLOGY_SWEEP_MODULE_NAME = "terminology_sweep"
71
+ TERMINOLOGY_SWEEP_RULE_ID = "terminology-sweep"
72
+ TERMINOLOGY_FINDING_PATTERN = re.compile(
73
+ r"^(?P<path>.+):(?P<line_number>\d+): (?P<message>.+)$"
74
+ )
75
+ TERMINOLOGY_PATH_GROUP = "path"
76
+ TERMINOLOGY_LINE_NUMBER_GROUP = "line_number"
77
+ TERMINOLOGY_MESSAGE_GROUP = "message"
@@ -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 rule.selections:
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
+ )