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
|
@@ -131,6 +131,8 @@ from dev_env_scripts_constants.claude_chain_constants import (
|
|
|
131
131
|
UTF8_ENCODING,
|
|
132
132
|
)
|
|
133
133
|
|
|
134
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
135
|
+
|
|
134
136
|
|
|
135
137
|
def _decode_captured_stream(raw_bytes: bytes, encoding: str, errors: str) -> str:
|
|
136
138
|
"""Decode captured *raw_bytes* with ``text=True`` universal-newline semantics.
|
|
@@ -222,6 +224,7 @@ def _run_captured_subprocess(
|
|
|
222
224
|
else None
|
|
223
225
|
),
|
|
224
226
|
input=input_bytes,
|
|
227
|
+
creationflags=hidden_window_creation_flags(),
|
|
225
228
|
)
|
|
226
229
|
except subprocess.TimeoutExpired as timeout_error:
|
|
227
230
|
_attach_partial_timeout_streams(
|
|
@@ -55,10 +55,7 @@ CONVENTIONAL_TITLE_PATTERN: re.Pattern[str] = re.compile(
|
|
|
55
55
|
rf"^(?:{'|'.join(ALL_CONVENTIONAL_TITLE_TYPES)})(?:\([^)\r\n]+\))?!?: [^\s].*$"
|
|
56
56
|
)
|
|
57
57
|
ALL_REQUIRED_PR_DESCRIPTION_HEADINGS: tuple[str, ...] = (
|
|
58
|
-
"Summary",
|
|
59
|
-
"Description",
|
|
60
58
|
"Why",
|
|
61
|
-
"How",
|
|
62
59
|
"Verification",
|
|
63
60
|
)
|
|
64
61
|
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
|
|
@@ -4,6 +4,7 @@ from durable_post_lint_config.config.constants import (
|
|
|
4
4
|
ALL_CONVENTIONAL_TITLE_TYPES,
|
|
5
5
|
ALL_POST_ACTIONS,
|
|
6
6
|
ALL_RELEASE_BODY_MARKERS,
|
|
7
|
+
ALL_REQUIRED_PR_DESCRIPTION_HEADINGS,
|
|
7
8
|
CONVENTIONAL_TITLE_PATTERN,
|
|
8
9
|
RELEASE_BRANCH_PREFIX,
|
|
9
10
|
)
|
|
@@ -36,3 +37,7 @@ def should_carry_both_markers_release_automation_writes() -> None:
|
|
|
36
37
|
|
|
37
38
|
def should_name_the_branch_prefix_release_automation_pushes() -> None:
|
|
38
39
|
assert RELEASE_BRANCH_PREFIX == "release-please--branches--"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def should_require_only_the_headings_both_pull_request_rule_sets_name() -> None:
|
|
43
|
+
assert ALL_REQUIRED_PR_DESCRIPTION_HEADINGS == ("Why", "Verification")
|
|
@@ -41,6 +41,8 @@ from dev_env_scripts_constants.gh_artifact_upload_constants import (
|
|
|
41
41
|
UTF8_ENCODING,
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
45
|
+
|
|
44
46
|
|
|
45
47
|
class ArtifactUploadError(Exception):
|
|
46
48
|
"""Raised when creating the release or uploading the asset fails."""
|
|
@@ -53,6 +55,7 @@ def _run_gh(all_arguments: list[str]) -> subprocess.CompletedProcess[str]:
|
|
|
53
55
|
text=True,
|
|
54
56
|
encoding=UTF8_ENCODING,
|
|
55
57
|
check=False,
|
|
58
|
+
creationflags=hidden_window_creation_flags(),
|
|
56
59
|
)
|
|
57
60
|
|
|
58
61
|
|
|
@@ -85,6 +85,8 @@ from process_tree_kill import ( # noqa: E402
|
|
|
85
85
|
terminate_process_tree,
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
89
|
+
|
|
88
90
|
runner_popen = subprocess.Popen
|
|
89
91
|
|
|
90
92
|
|
|
@@ -361,6 +363,7 @@ def _invoke_process(
|
|
|
361
363
|
encoding=UTF8_ENCODING,
|
|
362
364
|
errors=UTF8_DECODE_ERRORS,
|
|
363
365
|
start_new_session=should_start_new_session(),
|
|
366
|
+
creationflags=hidden_window_creation_flags(),
|
|
364
367
|
)
|
|
365
368
|
except FileNotFoundError:
|
|
366
369
|
return _missing_binary_outcome()
|
|
@@ -27,6 +27,8 @@ from dev_env_scripts_constants.grok_run_ledger_constants import (
|
|
|
27
27
|
UTF8_ENCODING,
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
def compute_sha256_hex(content: bytes) -> str:
|
|
32
34
|
"""Return the hex SHA-256 digest of raw bytes.
|
|
@@ -58,11 +60,13 @@ def extract_worktree_diff(
|
|
|
58
60
|
["git", "-C", str(worktree_path), "diff", base_sha],
|
|
59
61
|
text=True,
|
|
60
62
|
encoding=UTF8_ENCODING,
|
|
63
|
+
creationflags=hidden_window_creation_flags(),
|
|
61
64
|
)
|
|
62
65
|
changed_paths_listing = subprocess.check_output(
|
|
63
66
|
["git", "-C", str(worktree_path), "diff", "--name-only", base_sha],
|
|
64
67
|
text=True,
|
|
65
68
|
encoding=UTF8_ENCODING,
|
|
69
|
+
creationflags=hidden_window_creation_flags(),
|
|
66
70
|
)
|
|
67
71
|
changed_paths = tuple(
|
|
68
72
|
each_line.strip()
|
|
@@ -90,6 +90,8 @@ from dev_env_scripts_constants.grok_worker_constants import (
|
|
|
90
90
|
UTF8_ENCODING,
|
|
91
91
|
)
|
|
92
92
|
|
|
93
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
94
|
+
|
|
93
95
|
preflight_which = shutil.which
|
|
94
96
|
preflight_subprocess_runner = subprocess.run
|
|
95
97
|
preflight_time = time.time
|
|
@@ -259,6 +261,7 @@ def _run_grok_command(
|
|
|
259
261
|
errors=UTF8_DECODE_ERRORS,
|
|
260
262
|
timeout=timeout_seconds,
|
|
261
263
|
check=False,
|
|
264
|
+
creationflags=hidden_window_creation_flags(),
|
|
262
265
|
)
|
|
263
266
|
except OSError:
|
|
264
267
|
return None
|
|
@@ -104,6 +104,8 @@ from dev_env_scripts_constants.timing import ( # noqa: E402
|
|
|
104
104
|
DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
108
|
+
|
|
107
109
|
|
|
108
110
|
@dataclass(frozen=True)
|
|
109
111
|
class CodeReviewOutcome:
|
|
@@ -265,6 +267,7 @@ def is_working_tree_dirty(working_directory: Path) -> bool:
|
|
|
265
267
|
capture_output=True,
|
|
266
268
|
text=True,
|
|
267
269
|
check=False,
|
|
270
|
+
creationflags=hidden_window_creation_flags(),
|
|
268
271
|
)
|
|
269
272
|
if completion.returncode != 0:
|
|
270
273
|
return True
|
|
@@ -11,6 +11,8 @@ from .config import (
|
|
|
11
11
|
)
|
|
12
12
|
from .model import CommandCapture
|
|
13
13
|
|
|
14
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
def run_command(
|
|
16
18
|
all_arguments: tuple[str, ...], check_directory: Path, timeout_seconds: float
|
|
@@ -73,6 +75,7 @@ def _run_subprocess(
|
|
|
73
75
|
encoding=UTF8_ENCODING,
|
|
74
76
|
errors="replace",
|
|
75
77
|
timeout=timeout_seconds,
|
|
78
|
+
creationflags=hidden_window_creation_flags(),
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
|
|
@@ -23,6 +23,8 @@ from .config import (
|
|
|
23
23
|
)
|
|
24
24
|
from .config.timing import GIT_METADATA_TIMEOUT_SECONDS
|
|
25
25
|
|
|
26
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
@dataclass(frozen=True)
|
|
28
30
|
class CandidateSnapshot:
|
|
@@ -82,6 +84,7 @@ def _run_git_bytes(
|
|
|
82
84
|
capture_output=True,
|
|
83
85
|
check=False,
|
|
84
86
|
timeout=GIT_METADATA_TIMEOUT_SECONDS,
|
|
87
|
+
creationflags=hidden_window_creation_flags(),
|
|
85
88
|
)
|
|
86
89
|
except (OSError, subprocess.SubprocessError):
|
|
87
90
|
return None
|
|
@@ -121,7 +121,31 @@ def _registered_nested_strings(each_entry: object) -> tuple[str, ...]:
|
|
|
121
121
|
return tuple(all_registered_strings)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def _names_exempt_registration_path(registered_string: str) -> bool:
|
|
125
|
+
"""Return True when the registration names a chain that decides no policy.
|
|
126
|
+
|
|
127
|
+
::
|
|
128
|
+
|
|
129
|
+
hooks/blocking/bash_pre_tool_use_dispatcher.py -> exempt
|
|
130
|
+
hooks/blocking/some_new_blocker.py -> flagged
|
|
131
|
+
|
|
132
|
+
The Bash PreToolUse dispatcher sits under ``blocking/`` for layout reasons
|
|
133
|
+
while its roster hosts one allow-and-rewrite hook. Its path segment reads as
|
|
134
|
+
a policy boundary that the chain never carries.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
registered_string: One command, path, script, or entrypoint string.
|
|
138
|
+
"""
|
|
139
|
+
path_shaped_string = registered_string.lower().replace("\\", "/")
|
|
140
|
+
return any(
|
|
141
|
+
each_exempt_path in path_shaped_string
|
|
142
|
+
for each_exempt_path in constants.ALL_ACTION_BOUNDARY_EXEMPT_REGISTRATION_PATHS
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
124
146
|
def _contains_action_boundary_marker(registered_string: str) -> bool:
|
|
147
|
+
if _names_exempt_registration_path(registered_string):
|
|
148
|
+
return False
|
|
125
149
|
normalized_string = registered_string.lower().replace("\\", "/")
|
|
126
150
|
normalized_string = normalized_string.replace(".", "/").replace("-", "/")
|
|
127
151
|
return any(
|
|
@@ -4,6 +4,7 @@ from collections.abc import Iterable
|
|
|
4
4
|
from contextlib import redirect_stderr, redirect_stdout
|
|
5
5
|
from io import StringIO
|
|
6
6
|
from pathlib import Path, PurePosixPath
|
|
7
|
+
from types import ModuleType
|
|
7
8
|
|
|
8
9
|
from . import adapter_support
|
|
9
10
|
from .config import constants
|
|
@@ -118,10 +119,49 @@ def validator_diagnostics(
|
|
|
118
119
|
return adapter_support._diagnostics_for_messages(document, "validators", all_messages)
|
|
119
120
|
|
|
120
121
|
|
|
122
|
+
def _is_untouched_function(
|
|
123
|
+
message: str, all_changed_lines: frozenset[int], scoping_module: ModuleType
|
|
124
|
+
) -> bool:
|
|
125
|
+
span_range = scoping_module.function_length_span_range(message)
|
|
126
|
+
return span_range is not None and all_changed_lines.isdisjoint(span_range)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _change_scoped_messages(
|
|
130
|
+
all_messages: Iterable[str],
|
|
131
|
+
document: Document,
|
|
132
|
+
load_pr_loop_module: adapter_support.HookModuleLoader,
|
|
133
|
+
) -> tuple[str, ...]:
|
|
134
|
+
"""Drop function-length messages for functions the change never touched.
|
|
135
|
+
|
|
136
|
+
::
|
|
137
|
+
|
|
138
|
+
an import added above an existing long ``main`` -> dropped
|
|
139
|
+
a statement added inside that ``main`` -> kept
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
all_messages: Every message the code-rule engine returned.
|
|
143
|
+
document: Current document and the lines this change touched.
|
|
144
|
+
load_pr_loop_module: Loader for the shared pull-request-loop scripts.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The messages that remain in scope for this change.
|
|
148
|
+
"""
|
|
149
|
+
all_changed_lines = document.changed_lines
|
|
150
|
+
if all_changed_lines is None:
|
|
151
|
+
return tuple(all_messages)
|
|
152
|
+
scoping_module = load_pr_loop_module(constants.VIOLATION_SCOPING_MODULE_NAME)
|
|
153
|
+
return tuple(
|
|
154
|
+
each_message
|
|
155
|
+
for each_message in all_messages
|
|
156
|
+
if not _is_untouched_function(each_message, all_changed_lines, scoping_module)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
121
160
|
def code_rule_diagnostics(
|
|
122
161
|
document: Document,
|
|
123
162
|
repository_root: Path,
|
|
124
163
|
load_module: adapter_support.HookModuleLoader,
|
|
164
|
+
load_pr_loop_module: adapter_support.HookModuleLoader,
|
|
125
165
|
) -> tuple[Diagnostic, ...]:
|
|
126
166
|
"""Run the code-rule engine on one document.
|
|
127
167
|
|
|
@@ -129,9 +169,10 @@ def code_rule_diagnostics(
|
|
|
129
169
|
document: Current text and optional prior text.
|
|
130
170
|
repository_root: Request repository root for sibling-file resolution.
|
|
131
171
|
load_module: Hook module loader.
|
|
172
|
+
load_pr_loop_module: Loader for the shared pull-request-loop scripts.
|
|
132
173
|
|
|
133
174
|
Returns:
|
|
134
|
-
Diagnostics from the code-rule engine.
|
|
175
|
+
Diagnostics from the code-rule engine, scoped to the change.
|
|
135
176
|
"""
|
|
136
177
|
absolute_path = adapter_support._document_path(repository_root, document)
|
|
137
178
|
legacy_module = load_module("blocking.code_rules_enforcer")
|
|
@@ -146,7 +187,12 @@ def code_rule_diagnostics(
|
|
|
146
187
|
sibling_directory=absolute_path.parent,
|
|
147
188
|
include_comment_policy=True,
|
|
148
189
|
)
|
|
149
|
-
|
|
190
|
+
all_scoped_messages = _change_scoped_messages(
|
|
191
|
+
all_messages, document, load_pr_loop_module
|
|
192
|
+
)
|
|
193
|
+
return adapter_support._diagnostics_for_messages(
|
|
194
|
+
document, "code-rules", all_scoped_messages
|
|
195
|
+
)
|
|
150
196
|
|
|
151
197
|
|
|
152
198
|
def state_description_diagnostics(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from collections import Counter
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
|
|
6
7
|
from . import adapter_support
|
|
@@ -52,12 +53,19 @@ def _remove_docstrings(parsed_tree: ast.AST) -> None:
|
|
|
52
53
|
_remove_docstrings(each_child)
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
def
|
|
56
|
+
def _parsed_without_docstrings(source_text: str) -> ast.Module | None:
|
|
56
57
|
try:
|
|
57
58
|
parsed_tree = ast.parse(source_text)
|
|
58
|
-
except SyntaxError:
|
|
59
|
+
except (SyntaxError, ValueError):
|
|
59
60
|
return None
|
|
60
61
|
_remove_docstrings(parsed_tree)
|
|
62
|
+
return parsed_tree
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ast_without_docstrings(source_text: str) -> str | None:
|
|
66
|
+
parsed_tree = _parsed_without_docstrings(source_text)
|
|
67
|
+
if parsed_tree is None:
|
|
68
|
+
return None
|
|
61
69
|
return ast.dump(parsed_tree, include_attributes=False)
|
|
62
70
|
|
|
63
71
|
|
|
@@ -71,6 +79,144 @@ def _is_docstring_only_python_document(document: Document) -> bool:
|
|
|
71
79
|
return current_ast is not None and current_ast == prior_ast
|
|
72
80
|
|
|
73
81
|
|
|
82
|
+
def _is_import_statement(node: object) -> bool:
|
|
83
|
+
return isinstance(node, (ast.Import, ast.ImportFrom))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _dumped_import_tally(all_nodes: list[object]) -> Counter[str]:
|
|
87
|
+
return Counter(
|
|
88
|
+
ast.dump(each_node, include_attributes=False)
|
|
89
|
+
for each_node in all_nodes
|
|
90
|
+
if isinstance(each_node, ast.AST) and _is_import_statement(each_node)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _matches_positional_list(
|
|
95
|
+
all_current_nodes: list[object], all_prior_nodes: list[object]
|
|
96
|
+
) -> bool:
|
|
97
|
+
if len(all_current_nodes) != len(all_prior_nodes):
|
|
98
|
+
return False
|
|
99
|
+
return all(
|
|
100
|
+
_matches_field(each_current_node, each_prior_node)
|
|
101
|
+
for each_current_node, each_prior_node in zip(
|
|
102
|
+
all_current_nodes, all_prior_nodes, strict=True
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _matches_statement_list(
|
|
108
|
+
all_current_nodes: list[object], all_prior_nodes: list[object]
|
|
109
|
+
) -> bool:
|
|
110
|
+
if _dumped_import_tally(all_prior_nodes) - _dumped_import_tally(all_current_nodes):
|
|
111
|
+
return False
|
|
112
|
+
return _matches_positional_list(
|
|
113
|
+
[
|
|
114
|
+
each_node
|
|
115
|
+
for each_node in all_current_nodes
|
|
116
|
+
if not _is_import_statement(each_node)
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
each_node
|
|
120
|
+
for each_node in all_prior_nodes
|
|
121
|
+
if not _is_import_statement(each_node)
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_statement_list(all_nodes: list[object]) -> bool:
|
|
127
|
+
return all(isinstance(each_node, ast.stmt) for each_node in all_nodes)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _matches_node_list(
|
|
131
|
+
all_current_nodes: list[object], all_prior_nodes: list[object]
|
|
132
|
+
) -> bool:
|
|
133
|
+
if _is_statement_list(all_current_nodes) and _is_statement_list(all_prior_nodes):
|
|
134
|
+
return _matches_statement_list(all_current_nodes, all_prior_nodes)
|
|
135
|
+
return _matches_positional_list(all_current_nodes, all_prior_nodes)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _matches_field(current_field: object, prior_field: object) -> bool:
|
|
139
|
+
if isinstance(current_field, ast.AST) and isinstance(prior_field, ast.AST):
|
|
140
|
+
return _matches_node(current_field, prior_field)
|
|
141
|
+
if isinstance(current_field, list) and isinstance(prior_field, list):
|
|
142
|
+
return _matches_node_list(current_field, prior_field)
|
|
143
|
+
if type(current_field) is not type(prior_field):
|
|
144
|
+
return False
|
|
145
|
+
return bool(current_field == prior_field)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _matches_keyword(current_keyword: ast.keyword, prior_keyword: ast.keyword) -> bool:
|
|
149
|
+
return current_keyword.arg == prior_keyword.arg and _matches_node(
|
|
150
|
+
current_keyword.value, prior_keyword.value
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _keywords_after_match(
|
|
155
|
+
all_candidate_keywords: list[ast.keyword], prior_keyword: ast.keyword
|
|
156
|
+
) -> list[ast.keyword] | None:
|
|
157
|
+
for each_index, each_candidate_keyword in enumerate(all_candidate_keywords):
|
|
158
|
+
if _matches_keyword(each_candidate_keyword, prior_keyword):
|
|
159
|
+
return all_candidate_keywords[each_index + 1 :]
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _prior_keywords_survive(
|
|
164
|
+
all_current_keywords: list[ast.keyword], all_prior_keywords: list[ast.keyword]
|
|
165
|
+
) -> bool:
|
|
166
|
+
all_remaining_keywords: list[ast.keyword] | None = list(all_current_keywords)
|
|
167
|
+
for each_prior_keyword in all_prior_keywords:
|
|
168
|
+
if all_remaining_keywords is None:
|
|
169
|
+
return False
|
|
170
|
+
all_remaining_keywords = _keywords_after_match(
|
|
171
|
+
all_remaining_keywords, each_prior_keyword
|
|
172
|
+
)
|
|
173
|
+
return all_remaining_keywords is not None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _mapping_unpacking_count(all_keywords: list[ast.keyword]) -> int:
|
|
177
|
+
return sum(1 for each_keyword in all_keywords if each_keyword.arg is None)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _matches_call(current_call: ast.Call, prior_call: ast.Call) -> bool:
|
|
181
|
+
if not _matches_node(current_call.func, prior_call.func):
|
|
182
|
+
return False
|
|
183
|
+
if not _matches_positional_list(list(current_call.args), list(prior_call.args)):
|
|
184
|
+
return False
|
|
185
|
+
if _mapping_unpacking_count(current_call.keywords) != _mapping_unpacking_count(
|
|
186
|
+
prior_call.keywords
|
|
187
|
+
):
|
|
188
|
+
return False
|
|
189
|
+
return _prior_keywords_survive(
|
|
190
|
+
list(current_call.keywords), list(prior_call.keywords)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _matches_node(current_node: ast.AST, prior_node: ast.AST) -> bool:
|
|
195
|
+
if type(current_node) is not type(prior_node):
|
|
196
|
+
return False
|
|
197
|
+
if isinstance(current_node, ast.Call) and isinstance(prior_node, ast.Call):
|
|
198
|
+
return _matches_call(current_node, prior_node)
|
|
199
|
+
return all(
|
|
200
|
+
_matches_field(
|
|
201
|
+
getattr(current_node, each_field_name, None),
|
|
202
|
+
getattr(prior_node, each_field_name, None),
|
|
203
|
+
)
|
|
204
|
+
for each_field_name in current_node._fields
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _is_import_and_keyword_addition_python_document(document: Document) -> bool:
|
|
209
|
+
if document.path.suffix.lower() != constants.PYTHON_SUFFIX:
|
|
210
|
+
return False
|
|
211
|
+
if document.prior_text is None:
|
|
212
|
+
return False
|
|
213
|
+
current_tree = _parsed_without_docstrings(document.text)
|
|
214
|
+
prior_tree = _parsed_without_docstrings(document.prior_text)
|
|
215
|
+
if current_tree is None or prior_tree is None:
|
|
216
|
+
return False
|
|
217
|
+
return _matches_node(current_tree, prior_tree)
|
|
218
|
+
|
|
219
|
+
|
|
74
220
|
def _candidate_test_names(path: PurePosixPath) -> frozenset[str]:
|
|
75
221
|
stem = path.stem
|
|
76
222
|
suffix = path.suffix.lower()
|
|
@@ -157,6 +303,8 @@ def _is_unpaired_production(
|
|
|
157
303
|
return False
|
|
158
304
|
if _is_docstring_only_python_document(production_document):
|
|
159
305
|
return False
|
|
306
|
+
if _is_import_and_keyword_addition_python_document(production_document):
|
|
307
|
+
return False
|
|
160
308
|
return not _has_changed_test(production_path, all_changed_test_paths)
|
|
161
309
|
|
|
162
310
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Detect rules prose that credits a hook module which runs nowhere.
|
|
2
|
+
|
|
3
|
+
A hook module runs when ``hooks/hooks.json`` registers it or a dispatcher
|
|
4
|
+
roster hosts it. A module that survives on disk while appearing in neither
|
|
5
|
+
one reaches no tool call, so prose that gives it a present-tense action
|
|
6
|
+
describes a gate the reader never faces.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
|
|
14
|
+
from .config import constants
|
|
15
|
+
from .model import Diagnostic, Document, Location, Severity
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def accepts_rules_markdown(document: Document) -> bool:
|
|
19
|
+
"""Return whether the document is Markdown in a rules directory.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
document: Candidate document.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
True for a Markdown file directly under a ``rules`` directory.
|
|
26
|
+
"""
|
|
27
|
+
return (
|
|
28
|
+
document.path.suffix.lower() in constants.ALL_MARKDOWN_SUFFIXES
|
|
29
|
+
and document.path.parent.name == constants.RULES_DIRECTORY_NAME
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _package_root(repository_root: Path, document: Document) -> Path:
|
|
34
|
+
return repository_root / document.path.parent.parent.as_posix()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _read_text(path: Path) -> str:
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
return ""
|
|
40
|
+
return path.read_text(
|
|
41
|
+
encoding="utf-8", errors=constants.REPLACEMENT_CHARACTER_ERRORS
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _registered_hook_stems(hooks_root: Path) -> frozenset[str]:
|
|
46
|
+
all_stems = set(
|
|
47
|
+
re.findall(
|
|
48
|
+
constants.HOOKS_JSON_PATH_PATTERN,
|
|
49
|
+
_read_text(hooks_root / constants.HOOKS_CONFIGURATION_FILE_NAME),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
constants_root = hooks_root / constants.HOOK_CONSTANTS_DIRECTORY_NAME
|
|
53
|
+
if constants_root.is_dir():
|
|
54
|
+
for each_path in sorted(
|
|
55
|
+
constants_root.glob(constants.DISPATCHER_CONSTANTS_GLOB)
|
|
56
|
+
):
|
|
57
|
+
all_stems.update(
|
|
58
|
+
PurePosixPath(each_relative_path).stem
|
|
59
|
+
for each_relative_path in re.findall(
|
|
60
|
+
constants.QUOTED_HOOK_PATH_PATTERN, _read_text(each_path)
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
return frozenset(all_stems)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _present_hook_stems(hooks_root: Path) -> frozenset[str]:
|
|
67
|
+
if not hooks_root.is_dir():
|
|
68
|
+
return frozenset()
|
|
69
|
+
return frozenset(
|
|
70
|
+
each_path.stem for each_path in hooks_root.rglob(constants.PYTHON_GLOB)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _retired_roster_stems(package_root: Path) -> frozenset[str]:
|
|
75
|
+
installer_path = package_root.joinpath(*constants.ALL_INSTALLER_PATH_SEGMENTS)
|
|
76
|
+
maybe_roster = re.search(
|
|
77
|
+
constants.RETIRED_ROSTER_PATTERN, _read_text(installer_path)
|
|
78
|
+
)
|
|
79
|
+
if maybe_roster is None:
|
|
80
|
+
return frozenset()
|
|
81
|
+
return frozenset(
|
|
82
|
+
PurePosixPath(each_relative_path).stem
|
|
83
|
+
for each_relative_path in re.findall(
|
|
84
|
+
constants.QUOTED_HOOK_PATH_PATTERN, maybe_roster.group(1)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _hook_stem_in_span(span_text: str) -> str | None:
|
|
90
|
+
candidate_name = (
|
|
91
|
+
span_text.strip()
|
|
92
|
+
.rsplit(constants.PATH_SEPARATOR, 1)[-1]
|
|
93
|
+
.removesuffix(constants.PYTHON_SUFFIX)
|
|
94
|
+
)
|
|
95
|
+
if not re.fullmatch(constants.HOOK_MODULE_NAME_PATTERN, candidate_name):
|
|
96
|
+
return None
|
|
97
|
+
if not candidate_name.endswith(constants.ALL_HOOK_MODULE_NAME_SUFFIXES):
|
|
98
|
+
return None
|
|
99
|
+
return candidate_name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _names_a_live_action(segment_text: str) -> bool:
|
|
103
|
+
return any(
|
|
104
|
+
each_word in constants.ALL_LIVE_CLAIM_VERBS
|
|
105
|
+
for each_word in re.findall(
|
|
106
|
+
constants.PROSE_WORD_PATTERN, segment_text.lower()
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _describes_a_replacement(line_text: str) -> bool:
|
|
112
|
+
normalized_line = line_text.lower()
|
|
113
|
+
return any(
|
|
114
|
+
each_phrase in normalized_line
|
|
115
|
+
for each_phrase in constants.ALL_REPLACEMENT_DELIVERY_PHRASES
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _claim_segment_end(
|
|
120
|
+
line_text: str, all_spans: tuple[re.Match[str], ...], span_index: int
|
|
121
|
+
) -> int:
|
|
122
|
+
maybe_sentence_end = re.compile(constants.SENTENCE_BOUNDARY_PATTERN).search(
|
|
123
|
+
line_text, all_spans[span_index].end()
|
|
124
|
+
)
|
|
125
|
+
segment_end = (
|
|
126
|
+
len(line_text) if maybe_sentence_end is None else maybe_sentence_end.start()
|
|
127
|
+
)
|
|
128
|
+
for each_span in all_spans[span_index + 1 :]:
|
|
129
|
+
if each_span.start() >= segment_end:
|
|
130
|
+
break
|
|
131
|
+
if _hook_stem_in_span(each_span.group(1)) is not None:
|
|
132
|
+
return each_span.start()
|
|
133
|
+
return segment_end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _line_diagnostics(
|
|
137
|
+
document: Document,
|
|
138
|
+
line_text: str,
|
|
139
|
+
line_number: int,
|
|
140
|
+
all_known_stems: frozenset[str],
|
|
141
|
+
all_registered_stems: frozenset[str],
|
|
142
|
+
) -> tuple[Diagnostic, ...]:
|
|
143
|
+
all_spans = tuple(re.finditer(constants.INLINE_CODE_PATTERN, line_text))
|
|
144
|
+
all_diagnostics: list[Diagnostic] = []
|
|
145
|
+
for each_index, each_span in enumerate(all_spans):
|
|
146
|
+
maybe_stem = _hook_stem_in_span(each_span.group(1))
|
|
147
|
+
if maybe_stem is None or maybe_stem not in all_known_stems:
|
|
148
|
+
continue
|
|
149
|
+
if maybe_stem in all_registered_stems:
|
|
150
|
+
continue
|
|
151
|
+
claim_end = _claim_segment_end(line_text, all_spans, each_index)
|
|
152
|
+
if not _names_a_live_action(line_text[each_span.end() : claim_end]):
|
|
153
|
+
continue
|
|
154
|
+
all_diagnostics.append(
|
|
155
|
+
Diagnostic(
|
|
156
|
+
constants.RETIRED_HOOK_PROSE_RULE_ID,
|
|
157
|
+
Severity.ERROR,
|
|
158
|
+
constants.RETIRED_HOOK_PROSE_MESSAGE.format(hook_name=maybe_stem),
|
|
159
|
+
Location(document.path, line_number, each_span.start() + 1),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
return tuple(all_diagnostics)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _hook_stem_sets(package_root: Path) -> tuple[frozenset[str], frozenset[str]]:
|
|
166
|
+
hooks_root = package_root / constants.HOOKS_DIRECTORY_NAME
|
|
167
|
+
all_known_stems = _present_hook_stems(hooks_root) | _retired_roster_stems(
|
|
168
|
+
package_root
|
|
169
|
+
)
|
|
170
|
+
return all_known_stems, _registered_hook_stems(hooks_root)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def retired_hook_prose_diagnostics(
|
|
174
|
+
document: Document, repository_root: Path
|
|
175
|
+
) -> tuple[Diagnostic, ...]:
|
|
176
|
+
"""Report rules prose that credits an unregistered hook with a live action.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
document: Current rules Markdown text and path.
|
|
180
|
+
repository_root: Request repository root for package resolution.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
One diagnostic for each live claim about a hook that runs nowhere.
|
|
184
|
+
"""
|
|
185
|
+
all_known_stems, all_registered_stems = _hook_stem_sets(
|
|
186
|
+
_package_root(repository_root, document)
|
|
187
|
+
)
|
|
188
|
+
all_diagnostics: list[Diagnostic] = []
|
|
189
|
+
for each_line_number, each_line in enumerate(document.text.splitlines(), 1):
|
|
190
|
+
if _describes_a_replacement(each_line):
|
|
191
|
+
continue
|
|
192
|
+
all_diagnostics.extend(
|
|
193
|
+
_line_diagnostics(
|
|
194
|
+
document,
|
|
195
|
+
each_line,
|
|
196
|
+
each_line_number,
|
|
197
|
+
all_known_stems,
|
|
198
|
+
all_registered_stems,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
return tuple(all_diagnostics)
|