claude-dev-env 1.80.0 → 1.82.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 (64) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
  6. package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
  7. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
  9. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
  10. package/audit-rubrics/CLAUDE.md +3 -2
  11. package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
  12. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
  13. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
  14. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
  15. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
  16. package/audit-rubrics/prompts/CLAUDE.md +2 -1
  17. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
  18. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
  19. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
  20. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
  21. package/docs/CODE_RULES.md +2 -2
  22. package/hooks/blocking/CLAUDE.md +3 -0
  23. package/hooks/blocking/code_rules_annotations_length.py +59 -11
  24. package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
  25. package/hooks/blocking/code_rules_command_dispatch.py +140 -0
  26. package/hooks/blocking/code_rules_docstrings.py +93 -0
  27. package/hooks/blocking/code_rules_enforcer.py +54 -4
  28. package/hooks/blocking/code_rules_js_conventions.py +246 -0
  29. package/hooks/blocking/code_rules_naming_collection.py +5 -0
  30. package/hooks/blocking/code_rules_test_assertions.py +3 -0
  31. package/hooks/blocking/code_rules_unused_imports.py +0 -3
  32. package/hooks/blocking/conventional_pr_title_gate.py +444 -0
  33. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
  34. package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
  35. package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
  36. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
  37. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
  38. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
  39. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
  40. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +16 -3
  41. package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
  42. package/hooks/blocking/test_conventional_pr_title_gate.py +640 -0
  43. package/hooks/hooks.json +5 -0
  44. package/hooks/hooks_constants/CLAUDE.md +3 -0
  45. package/hooks/hooks_constants/blocking_check_limits.py +9 -0
  46. package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
  47. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +58 -0
  48. package/hooks/hooks_constants/js_conventions_constants.py +54 -0
  49. package/package.json +1 -1
  50. package/rules/docstring-prose-matches-implementation.md +2 -1
  51. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
  52. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
  53. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
  54. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
  55. package/skills/autoconverge/SKILL.md +92 -7
  56. package/skills/autoconverge/reference/convergence.md +3 -3
  57. package/skills/autoconverge/reference/gotchas.md +5 -5
  58. package/skills/autoconverge/reference/stop-conditions.md +1 -1
  59. package/skills/autoconverge/workflow/converge.contract.test.mjs +161 -29
  60. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +342 -1
  61. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
  62. package/skills/autoconverge/workflow/converge.mjs +178 -32
  63. package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
  64. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
@@ -11,7 +11,8 @@ Python scripts invoked at runtime by the PR-loop skills. Each script is a standa
11
11
  | `post_audit_thread.py` | Posts an audit review (APPROVE / REQUEST_CHANGES) to a draft PR via the GitHub reviews API; reads the body skeleton from `audit-reply-template.md` at runtime |
12
12
  | `grant_project_claude_permissions.py` | Writes idempotent allow-rules and `additionalDirectories` entries into `~/.claude/settings.json` so subagents can edit the project's `.claude/` tree without prompting |
13
13
  | `revoke_project_claude_permissions.py` | Removes the allow-rules and entries that `grant_project_claude_permissions.py` wrote; safe to run when no prior grant exists |
14
- | `code_rules_gate.py` | Pre-commit gate that runs `code_rules_enforcer` checks on staged Python files before a fix commit lands |
14
+ | `code_rules_gate.py` | Pre-commit gate that runs `code_rules_enforcer` checks on staged Python files before a fix commit lands, and the terminology sweep over the staged diff |
15
+ | `terminology_sweep.py` | Flags a prose term that near-misses an identifier introduced on added code lines of a unified diff (shared leading word, divergent tail) |
15
16
  | `reviews_disabled.py` | Shared helper for the `CLAUDE_REVIEWS_DISABLED` opt-out gate; parses the env-var token to find which reviewer types are suppressed |
16
17
  | `copilot_quota.py` | Copilot premium-request quota pre-check: resolves a configured GitHub account, reads its remaining `premium_interactions` quota via `gh api copilot_internal/user`, and exits 0 (run Copilot) or non-zero (skip: out of quota, API down, or no account configured) |
17
18
  | `fix_hookspath.py` | Repairs a malformed `core.hooksPath` global git config entry |
@@ -15,16 +15,17 @@ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
15
15
  ALL_CODE_FILE_EXTENSIONS,
16
16
  ALL_GIT_DIFF_CACHED_NAME_ONLY_NULL_TERMINATED_COMMAND,
17
17
  ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX,
18
+ ALL_PYTEST_MODULE_INVOCATION,
18
19
  ALL_TEST_FILENAME_GLOB_SUFFIXES,
19
20
  ALL_TEST_FILENAME_SUFFIXES,
20
- EXPECTED_NON_RENAME_COLUMN_COUNT,
21
- EXPECTED_RENAME_COLUMN_COUNT,
22
21
  BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX,
23
22
  BANNED_NOUN_SPAN_GROUP_INDEX,
24
23
  BANNED_NOUN_VIOLATION_PATTERN,
25
24
  DUPLICATE_BODY_DEFINITION_LINE_GROUP_INDEX,
26
25
  DUPLICATE_BODY_SPAN_GROUP_INDEX,
27
26
  DUPLICATE_BODY_VIOLATION_PATTERN,
27
+ EXPECTED_NON_RENAME_COLUMN_COUNT,
28
+ EXPECTED_RENAME_COLUMN_COUNT,
28
29
  FUNCTION_LENGTH_DEFINITION_LINE_GROUP_INDEX,
29
30
  FUNCTION_LENGTH_SPAN_GROUP_INDEX,
30
31
  FUNCTION_LENGTH_VIOLATION_PATTERN,
@@ -35,10 +36,15 @@ from pr_loop_shared_constants.code_rules_gate_constants import ( # noqa: E402
35
36
  ISOLATION_VIOLATION_PATTERN,
36
37
  MAX_VIOLATIONS_PER_CHECK,
37
38
  PYTHON_FILE_EXTENSION,
39
+ STAGED_PYTEST_TIMEOUT_SECONDS,
40
+ STAGED_TEST_FAILURE_HEADER,
38
41
  TEST_CONFTEST_FILENAME,
39
42
  TEST_FILENAME_PREFIX,
40
43
  TESTS_PATH_SEGMENT,
41
44
  )
45
+ from pr_loop_shared_constants.preflight_constants import ( # noqa: E402
46
+ PYTEST_NO_TESTS_COLLECTED_EXIT_CODE,
47
+ )
42
48
  from pr_loop_shared_constants.inline_duplicate_body_span_constants import ( # noqa: E402
43
49
  INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX,
44
50
  INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX,
@@ -46,7 +52,10 @@ from pr_loop_shared_constants.inline_duplicate_body_span_constants import ( # n
46
52
  INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX,
47
53
  INLINE_DUPLICATE_BODY_VIOLATION_PATTERN,
48
54
  )
49
-
55
+ from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
56
+ TERMINOLOGY_SWEEP_GATE_HEADER,
57
+ )
58
+ from terminology_sweep import staged_terminology_findings # noqa: E402
50
59
 
51
60
  ValidateContentCallable = Callable[..., list[str]]
52
61
 
@@ -768,12 +777,13 @@ def renamed_file_source_map_since(
768
777
 
769
778
  Runs `git diff --name-status -M -z merge_base..HEAD` and collects both
770
779
  paths of every rename entry (status code starting with R, e.g. `R100`).
771
- Keys are destination posix paths; values are source posix paths. The
772
- -z flag asks git for null-terminated, unquoted output so paths
773
- containing tab or newline bytes are not misparsed by column or line
774
- splitting; rename records emit three null-terminated tokens in
775
- sequence (status, source, destination), other status records emit
776
- two (status, path).
780
+ Keys are destination posix paths; values are source posix paths.
781
+
782
+ The -z flag asks git for null-terminated, unquoted output. A path
783
+ holding a tab or newline byte then survives column and line splitting
784
+ unmangled. Each rename record emits three null-terminated tokens
785
+ (status, source, destination). Every other status record emits two
786
+ (status, path).
777
787
 
778
788
  Args:
779
789
  repository_root: Repository root used as the ``git -C`` target.
@@ -1024,11 +1034,13 @@ def duplicate_body_span_range(violation_text: str) -> range | None:
1024
1034
  The duplicate-body message carries the copied function's definition line and
1025
1035
  its full body span: ``Function 'NAME' duplicates location.py::name — ...
1026
1036
  (duplicate body span at line X, spanning Y lines)``. The function occupies
1027
- lines ``X`` through ``X + Y - 1`` inclusive, so a duplicate of a sibling helper
1028
- is blocking only when the diff touches the copied function and advisory when an
1029
- unrelated edit leaves a pre-existing copy untouched matching the span-scoped
1030
- PreToolUse Write/Edit behavior rather than blocking every duplicate-body
1031
- message unconditionally.
1037
+ lines ``X`` through ``X + Y - 1`` inclusive.
1038
+
1039
+ So a duplicate of a sibling helper blocks only when the diff touches the
1040
+ copied function. An unrelated edit that leaves a pre-existing copy
1041
+ untouched keeps it advisory. This matches the span-scoped PreToolUse
1042
+ Write/Edit behavior rather than blocking every duplicate-body message
1043
+ unconditionally.
1032
1044
 
1033
1045
  Args:
1034
1046
  violation_text: A single violation string emitted by the enforcer.
@@ -1048,16 +1060,18 @@ def duplicate_body_span_range(violation_text: str) -> range | None:
1048
1060
  def inline_duplicate_body_span_lines(violation_text: str) -> frozenset[int] | None:
1049
1061
  """Return the union of both spans of a same-file inline-duplicate issue, or None.
1050
1062
 
1051
- The same-file inline-duplicate message names two functions that share a body
1052
- the helper and the enclosing function carrying the inline copy and the live
1053
- Write/Edit hook scopes the violation by the UNION of both spans, blocking when
1054
- an edit touches either function. So the message carries both spans: ``(inline
1055
- duplicate body spans: helper at line H spanning P lines, enclosing at line E
1056
- spanning Q lines)``. The two spans can be disjoint (an unrelated function may
1057
- sit between the helper and its inline copy), so this returns the union as a
1058
- line-number set rather than a single contiguous range a range covering the
1059
- gap would wrongly block an edit confined to that intervening function, which
1060
- the PreToolUse path leaves unflagged.
1063
+ The same-file inline-duplicate message names two functions that share a body:
1064
+ the helper and the enclosing function carrying the inline copy. The live
1065
+ Write/Edit hook scopes the violation by the union of both spans. It blocks
1066
+ when an edit touches either function. So the message carries both spans:
1067
+ ``(inline duplicate body spans: helper at line H spanning P lines,
1068
+ enclosing at line E spanning Q lines)``.
1069
+
1070
+ The two spans can be disjoint: an unrelated function may sit between the
1071
+ helper and its inline copy. This returns the union as a line-number set
1072
+ rather than a single contiguous range. A range covering the gap would
1073
+ wrongly block an edit confined to that intervening function, which the
1074
+ PreToolUse path leaves unflagged.
1061
1075
 
1062
1076
  Args:
1063
1077
  violation_text: A single violation string emitted by the enforcer.
@@ -1096,10 +1110,10 @@ def enclosing_span_range(violation_text: str) -> range | None:
1096
1110
 
1097
1111
  Every diff-scoped enforcer check tags its message with an enclosing-unit
1098
1112
  span fragment. This dispatcher tries each span extractor from
1099
- ``_all_span_range_extractors`` so the gate reconstructs every scoped
1100
- check's span through one shared mechanism adding a new scoped check means
1101
- adding one extractor to that registry rather than threading a new branch
1102
- through ``split_violations_by_scope``.
1113
+ ``_all_span_range_extractors``, so the gate reconstructs every scoped
1114
+ check's span through one shared mechanism. Adding a new scoped check
1115
+ means adding one extractor to that registry rather than threading a new
1116
+ branch through ``split_violations_by_scope``.
1103
1117
 
1104
1118
  Args:
1105
1119
  violation_text: A single violation string emitted by the enforcer.
@@ -1510,6 +1524,73 @@ def _report_partitioned_violations(
1510
1524
  return 0
1511
1525
 
1512
1526
 
1527
+ def _staged_test_file_paths(repository_root: Path) -> list[Path]:
1528
+ """Return the staged Python test files that exist under a repository.
1529
+
1530
+ Args:
1531
+ repository_root: The repository root whose staged index is read.
1532
+
1533
+ Returns:
1534
+ The staged paths whose extension is Python, whose name matches a
1535
+ test-file pattern, and which exist on disk.
1536
+ """
1537
+ all_test_paths: list[Path] = []
1538
+ for each_path in paths_from_git_staged(repository_root):
1539
+ if each_path.suffix != PYTHON_FILE_EXTENSION:
1540
+ continue
1541
+ if is_test_path(str(each_path)) and each_path.is_file():
1542
+ all_test_paths.append(each_path)
1543
+ return all_test_paths
1544
+
1545
+
1546
+ def run_staged_test_files(repository_root: Path) -> int:
1547
+ """Run pytest over the staged test files and return the gate exit code.
1548
+
1549
+ Args:
1550
+ repository_root: The repository root the staged test files belong to.
1551
+
1552
+ Returns:
1553
+ 0 when no test file is staged, when the staged test files collect
1554
+ no tests (pytest's no-tests-collected exit code), or when every staged
1555
+ test passes. pytest's non-zero exit code otherwise, which blocks the
1556
+ commit.
1557
+ """
1558
+ all_test_paths = _staged_test_file_paths(repository_root)
1559
+ if not all_test_paths:
1560
+ return 0
1561
+ pytest_process = subprocess.run(
1562
+ [
1563
+ sys.executable,
1564
+ *ALL_PYTEST_MODULE_INVOCATION,
1565
+ *[str(each_path) for each_path in all_test_paths],
1566
+ ],
1567
+ cwd=str(repository_root),
1568
+ timeout=STAGED_PYTEST_TIMEOUT_SECONDS,
1569
+ check=False,
1570
+ )
1571
+ if pytest_process.returncode == PYTEST_NO_TESTS_COLLECTED_EXIT_CODE:
1572
+ return 0
1573
+ if pytest_process.returncode != 0:
1574
+ print(STAGED_TEST_FAILURE_HEADER, file=sys.stderr)
1575
+ return pytest_process.returncode
1576
+
1577
+
1578
+ def _report_terminology_findings(all_findings: list[str]) -> None:
1579
+ """Print the terminology-sweep findings, when any, to standard error.
1580
+
1581
+ Args:
1582
+ all_findings: The near-miss findings from the staged terminology sweep.
1583
+ """
1584
+ if not all_findings:
1585
+ return
1586
+ print(
1587
+ TERMINOLOGY_SWEEP_GATE_HEADER.format(finding_count=len(all_findings)),
1588
+ file=sys.stderr,
1589
+ )
1590
+ for each_finding in all_findings:
1591
+ print(f" {each_finding}", file=sys.stderr)
1592
+
1593
+
1513
1594
  def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
1514
1595
  """Parse the command-line arguments for the code-rules gate.
1515
1596
 
@@ -1590,6 +1671,10 @@ def main(all_arguments: list[str]) -> int:
1590
1671
  validate_content, file_paths, repository_root, all_added_lines_by_path=None
1591
1672
  )
1592
1673
  if arguments.staged:
1674
+ all_terminology_findings = staged_terminology_findings(repository_root)
1675
+ _report_terminology_findings(all_terminology_findings)
1676
+ terminology_exit_code = 1 if all_terminology_findings else 0
1677
+ staged_test_exit_code = run_staged_test_files(repository_root)
1593
1678
  staged_file_paths = paths_from_git_staged(repository_root)
1594
1679
  staged_file_paths = filter_paths_under_prefixes(
1595
1680
  staged_file_paths,
@@ -1597,17 +1682,18 @@ def main(all_arguments: list[str]) -> int:
1597
1682
  arguments.only_under,
1598
1683
  )
1599
1684
  if not staged_file_paths:
1600
- return 0
1685
+ return terminology_exit_code or staged_test_exit_code
1601
1686
  staged_added_lines = added_lines_by_file_staged(
1602
1687
  repository_root, staged_file_paths
1603
1688
  )
1604
- return run_gate(
1689
+ gate_exit_code = run_gate(
1605
1690
  validate_content,
1606
1691
  staged_file_paths,
1607
1692
  repository_root,
1608
1693
  all_added_lines_by_path=staged_added_lines,
1609
1694
  read_staged_content_flag=True,
1610
1695
  )
1696
+ return gate_exit_code or terminology_exit_code or staged_test_exit_code
1611
1697
  file_paths = paths_from_git_diff(repository_root, arguments.base)
1612
1698
  file_paths = filter_paths_under_prefixes(
1613
1699
  file_paths,
@@ -16,6 +16,7 @@ Named constants for every script in `_shared/pr-loop/scripts/`. Each module owns
16
16
  | `preflight_self_heal_constants.py` | `preflight_self_heal.py` — git config keys and local-scope detection strings |
17
17
  | `reviews_disabled_constants.py` | `reviews_disabled.py` — `CLAUDE_REVIEWS_DISABLED` token taxonomy |
18
18
  | `copilot_quota_constants.py` | `copilot_quota.py` — `COPILOT_QUOTA_ACCOUNT` env-var name, `copilot_internal/user` API path, the `premium_interactions` gating field names, the four skip/run exit codes, and the default `.env` path |
19
+ | `terminology_sweep_constants.py` | `terminology_sweep.py` — identifier and prose-token regexes, diff-parsing prefixes, code-file extensions, and the finding-message template |
19
20
  | `__init__.py` | Empty package marker |
20
21
 
21
22
  ## Convention
@@ -80,3 +80,16 @@ ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX: tuple[str, ...] = (
80
80
  "--name-only",
81
81
  "-z",
82
82
  )
83
+
84
+
85
+ ALL_PYTEST_MODULE_INVOCATION: tuple[str, ...] = (
86
+ "-m",
87
+ "pytest",
88
+ "-q",
89
+ )
90
+
91
+ STAGED_PYTEST_TIMEOUT_SECONDS: int = 600
92
+
93
+ STAGED_TEST_FAILURE_HEADER: str = (
94
+ "code_rules_gate: staged test file(s) failed under pytest; commit blocked."
95
+ )
@@ -0,0 +1,113 @@
1
+ """Constants for the commit-time terminology sweep."""
2
+
3
+ import re
4
+
5
+ ALL_SWEEP_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
6
+ {".py", ".mjs", ".js", ".ts"}
7
+ )
8
+
9
+ MARKDOWN_FILE_EXTENSION: str = ".md"
10
+
11
+ SNAKE_CASE_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(
12
+ r"\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b"
13
+ )
14
+
15
+ CAMEL_CASE_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(
16
+ r"\b[a-z]+(?:[A-Z][a-z0-9]*)+\b"
17
+ )
18
+
19
+ CAMEL_CASE_WORD_PATTERN: re.Pattern[str] = re.compile(
20
+ r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+"
21
+ )
22
+
23
+ HYPHENATED_PROSE_TOKEN_PATTERN: re.Pattern[str] = re.compile(
24
+ r"\b[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)+\b"
25
+ )
26
+
27
+ PROSE_WORD_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z][A-Za-z0-9]*")
28
+
29
+ STRING_LITERAL_CONTENT_PATTERN: re.Pattern[str] = re.compile(
30
+ r"\"([^\"]*)\"|'([^']*)'|`([^`]*)`"
31
+ )
32
+
33
+ DIFF_FILE_HEADER_PREFIX: str = "+++ "
34
+
35
+ DIFF_HUNK_HEADER_PATTERN: re.Pattern[str] = re.compile(
36
+ r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@"
37
+ )
38
+
39
+ DIFF_ADDED_LINE_PREFIX: str = "+"
40
+
41
+ DIFF_REMOVED_LINE_PREFIX: str = "-"
42
+
43
+ DIFF_NEW_FILE_HEADER_PREFIX: str = "+++"
44
+
45
+ DIFF_OLD_FILE_HEADER_PREFIX: str = "---"
46
+
47
+ ALL_DIFF_FILE_PATH_STRIP_PREFIXES: tuple[str, ...] = ("a/", "b/")
48
+
49
+ PYTHON_COMMENT_MARKER: str = "#"
50
+
51
+ JAVASCRIPT_LINE_COMMENT_MARKER: str = "//"
52
+
53
+ JSDOC_CONTINUATION_MARKER: str = "*"
54
+
55
+ MINIMUM_IDENTIFIER_TOKEN_COUNT: int = 2
56
+
57
+ ALL_COMMON_ENGLISH_COMPOUND_TAIL_WORDS: frozenset[str] = frozenset(
58
+ {
59
+ "only",
60
+ "driven",
61
+ "safe",
62
+ "based",
63
+ "level",
64
+ "quality",
65
+ "known",
66
+ "source",
67
+ "party",
68
+ "case",
69
+ "time",
70
+ "wide",
71
+ "free",
72
+ "friendly",
73
+ "specific",
74
+ "aware",
75
+ "ready",
76
+ "side",
77
+ "grade",
78
+ "oriented",
79
+ "related",
80
+ "sensitive",
81
+ "facing",
82
+ }
83
+ )
84
+
85
+ TERMINOLOGY_FINDING_TEMPLATE: str = (
86
+ "{file_path}:{line_number}: prose term '{candidate}' near-misses code "
87
+ "identifier '{identifier}' (shared prefix token, divergent tail) — align "
88
+ "the prose wording with the identifier"
89
+ )
90
+
91
+ TERMINOLOGY_SWEEP_DESCRIPTION: str = (
92
+ "Flag prose terms that near-miss an identifier introduced on added code "
93
+ "lines of a unified diff."
94
+ )
95
+
96
+ DIFF_FILE_ARGUMENT_HELP: str = (
97
+ "Read the unified diff from this file instead of standard input."
98
+ )
99
+
100
+
101
+ ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND: tuple[str, ...] = (
102
+ "git",
103
+ "diff",
104
+ "--cached",
105
+ "--unified=0",
106
+ )
107
+
108
+ GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS: int = 30
109
+
110
+ TERMINOLOGY_SWEEP_GATE_HEADER: str = (
111
+ "terminology_sweep: {finding_count} cross-surface terminology near-miss(es) "
112
+ "on staged prose:"
113
+ )