claude-dev-env 2.28.0 → 2.29.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 (212) hide show
  1. package/.agents/agents/AGENTS.md +0 -31
  2. package/.agents/agents/clean-coder.md +1 -1
  3. package/.agents/agents/test_agent_frontmatter.py +3 -1
  4. package/.agents/skills/AGENTS.md +0 -70
  5. package/.agents/skills/_shared/AGENTS.md +0 -44
  6. package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
  7. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
  8. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
  9. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
  10. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
  11. package/.agents/skills/autoconverge/AGENTS.md +0 -35
  12. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
  13. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
  14. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
  15. package/.agents/skills/everything-search/AGENTS.md +0 -16
  16. package/.agents/skills/fresh-branch/AGENTS.md +0 -13
  17. package/.agents/skills/grok-spawn/AGENTS.md +0 -27
  18. package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
  19. package/.agents/skills/orchestrator/AGENTS.md +1 -0
  20. package/.agents/skills/orchestrator/SKILL.md +44 -53
  21. package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
  22. package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
  23. package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
  24. package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
  25. package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
  26. package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
  27. package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
  28. package/.agents/skills/rebase/AGENTS.md +0 -31
  29. package/.agents/skills/session-log/AGENTS.md +0 -31
  30. package/.agents/skills/session-tidy/AGENTS.md +0 -35
  31. package/.agents/skills/skill-builder/AGENTS.md +0 -48
  32. package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
  33. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
  34. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
  35. package/.agents/skills/task-build/AGENTS.md +0 -28
  36. package/.agents/skills/update/AGENTS.md +0 -37
  37. package/.agents/skills-archived/AGENTS.md +0 -44
  38. package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
  39. package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
  40. package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
  41. package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
  42. package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
  43. package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
  44. package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
  45. package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
  46. package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
  47. package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
  48. package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
  49. package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
  50. package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
  51. package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
  52. package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
  53. package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
  54. package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
  55. package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
  56. package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
  57. package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
  58. package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
  59. package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
  60. package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
  61. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  62. package/.agents/skills-archived/recall/AGENTS.md +0 -29
  63. package/.agents/skills-archived/remember/AGENTS.md +0 -30
  64. package/AGENTS.md +0 -112
  65. package/_shared/AGENTS.md +0 -16
  66. package/_shared/advisor/AGENTS.md +0 -21
  67. package/_shared/advisor/advisor-protocol.md +4 -1
  68. package/_shared/advisor/reference/consult-format.md +4 -2
  69. package/_shared/pr-loop/AGENTS.md +0 -27
  70. package/_shared/pr-loop/scripts/AGENTS.md +0 -49
  71. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
  72. package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
  73. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
  74. package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
  75. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
  76. package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
  77. package/_shared/process-tree/AGENTS.md +0 -40
  78. package/audit-rubrics/AGENTS.md +0 -42
  79. package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
  80. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  81. package/audit-rubrics/prompts/AGENTS.md +0 -36
  82. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  83. package/bin/AGENTS.md +0 -118
  84. package/bin/install.test.mjs +1 -1
  85. package/commands/AGENTS.md +0 -14
  86. package/docs/AGENTS.md +0 -31
  87. package/docs/CODE_RULES.md +3 -3
  88. package/docs/agent-spawn-protocol.md +1 -1
  89. package/docs/references/AGENTS.md +0 -16
  90. package/docs/references/prose-style-enforcement.md +5 -9
  91. package/hooks/AGENTS.md +0 -27
  92. package/hooks/advisory/AGENTS.md +0 -15
  93. package/hooks/blocking/AGENTS.md +0 -122
  94. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
  95. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  96. package/hooks/blocking/code_rules_enforcer.py +0 -121
  97. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  98. package/hooks/blocking/code_rules_shared.py +23 -0
  99. package/hooks/blocking/code_rules_test_layout.py +8 -8
  100. package/hooks/blocking/config/AGENTS.md +0 -9
  101. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  102. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  103. package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
  104. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
  105. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
  106. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  107. package/hooks/blocking/state_description_blocker.py +1 -7
  108. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
  109. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  110. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  111. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  112. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  113. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  114. package/hooks/blocking/test_code_rules_shared.py +12 -0
  115. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  116. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  117. package/hooks/blocking/test_state_description_blocker.py +6 -4
  118. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  119. package/hooks/git-hooks/AGENTS.md +0 -31
  120. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
  121. package/hooks/hooks_constants/AGENTS.md +0 -104
  122. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  123. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  124. package/hooks/hooks_constants/messages.py +0 -2
  125. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  126. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  127. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  128. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  129. package/hooks/hooks_constants/test_messages.py +5 -3
  130. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  131. package/hooks/lifecycle/AGENTS.md +0 -17
  132. package/hooks/observability/AGENTS.md +0 -19
  133. package/hooks/session/AGENTS.md +0 -37
  134. package/hooks/validation/AGENTS.md +0 -19
  135. package/hooks/validators/AGENTS.md +0 -51
  136. package/hooks/workflow/AGENTS.md +0 -15
  137. package/output-styles/AGENTS.md +0 -14
  138. package/package.json +1 -1
  139. package/rules/AGENTS.md +0 -58
  140. package/rules/claims-as-quotes.md +0 -10
  141. package/rules/code-standards.md +6 -6
  142. package/rules/explore-thoroughly.md +0 -1
  143. package/rules/failure-blast-radius.md +0 -8
  144. package/rules/falsify-before-green.md +0 -8
  145. package/rules/file-global-constants.md +2 -2
  146. package/rules/filesystem-search.md +1 -1
  147. package/rules/git-workflow.md +1 -9
  148. package/rules/hedging-claims.md +2 -6
  149. package/rules/long-horizon-autonomy.md +1 -1
  150. package/rules/measurement-denominators.md +0 -9
  151. package/rules/research-mode.md +0 -6
  152. package/rules/verify-before-asking.md +0 -5
  153. package/rules/verify-runtime-state.md +0 -5
  154. package/scripts/AGENTS.md +0 -55
  155. package/scripts/codex_compat_materializer.py +0 -12
  156. package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
  157. package/scripts/sync_to_cursor/AGENTS.md +0 -22
  158. package/scripts/sync_to_cursor/rules.py +0 -10
  159. package/scripts/tests/AGENTS.md +0 -34
  160. package/scripts/tests/test_engine.py +0 -1
  161. package/scripts/tests/test_rules.py +0 -1
  162. package/scripts/tests/test_sync_to_cursor.py +0 -1
  163. package/system-prompts/AGENTS.md +0 -24
  164. package/system-prompts/software-engineer.xml +3 -3
  165. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  166. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  167. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  168. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  169. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  170. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  171. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  172. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  173. package/hooks/blocking/hedging_language_blocker.py +0 -221
  174. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  175. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  176. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  177. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  178. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  179. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  180. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  181. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  182. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  183. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  184. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  185. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  186. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  187. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  188. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  189. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  190. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  191. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  192. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  193. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  194. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  195. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  196. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  197. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  198. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  199. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  200. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  201. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  202. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  203. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  204. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  205. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  206. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  207. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  208. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  209. package/rules/conservative-action.md +0 -17
  210. package/rules/context7.md +0 -8
  211. package/rules/few-words.md +0 -3
  212. package/rules/parallel-tools.md +0 -23
@@ -1,568 +0,0 @@
1
- """Dead config-dataclass field check: cross-module scan for config-like @dataclass fields.
2
-
3
- A config-like ``@dataclass`` — any class whose name ends in ``"Config"`` or
4
- ``"Selectors"`` — is defined in one module but constructed and consumed in
5
- others, so the per-file dead-field check in
6
- ``code_rules_dead_dataclass_field`` cannot judge its fields — it skips any class
7
- that is not constructed in the same file. This check resolves the enclosing
8
- package tree — the scan root — and flags a config-like dataclass field whose
9
- name appears as an attribute read (``obj.field``) in no production module
10
- anywhere under that root. A selectors dataclass is the same shape as a config
11
- dataclass: it is bound to a module-level singleton (``binary_selectors =
12
- BinarySelectors()``) and its fields are read across files, so an unwired
13
- selector field is caught the same way as a dead config field.
14
-
15
- The scan is deliberately conservative to keep false positives near zero:
16
-
17
- - Only ``@dataclass`` classes whose name ends in ``"Config"`` or ``"Selectors"``
18
- participate; other dataclasses are covered by the per-file check.
19
- - Test and migration files are exempt as write destinations, so a field added to
20
- a config dataclass inside a test is never flagged.
21
- - Production modules under the scan root are scanned for attribute reads; test
22
- and migration modules are deliberately excluded so a field read only by test
23
- code is still flagged as dead-in-production.
24
- - Field reads are collected as ``ast.Attribute.attr`` values (``obj.field``),
25
- augmented-assignment targets (``cfg.field += 1`` reads ``field`` before
26
- writing it), string literals (covers ``getattr(obj, "field")``),
27
- keyword-argument names on non-constructor calls (covers
28
- ``replace(cfg, debug_port=1)``), and match-pattern keyword attribute names
29
- (``case Config(field=found)``). Two field-write forms are excluded because they
30
- name a field without consuming it: a keyword that writes a field in a constructor
31
- of a known config-like dataclass defined under the scan root
32
- (``ThemeUpdateConfig(debug_port=1)``, excluded per keyword node so a same-named
33
- keyword on a ``replace`` call stays a read, and a factory function whose name
34
- merely ends in ``"Config"`` is not excluded), and a self-referential attribute
35
- read inside a config-like field's own default-value expression in the class body
36
- whose attribute name equals the field being defined
37
- (``debug_port: int = source.debug_port``) — a field written only these ways and
38
- read by no module is the dead-config case this check exists to catch. A default
39
- that sources a differently-named field on another object
40
- (``timeout_ms: int = other_config.base_timeout``) leaves that read counted, so
41
- ``base_timeout`` stays a live consumer. Plain
42
- ``ast.Name`` references are excluded — a local variable named ``debug_port`` is
43
- not a read of ``config.debug_port``.
44
- - A production module that reflectively reads a whole instance — a bare or
45
- ``dataclasses``-qualified call to ``asdict``, ``astuple``, ``fields``,
46
- ``replace``, or ``vars``, or a read of ``obj.__dict__`` — consumes every field
47
- at once without naming any single field, so the check is suppressed for the
48
- whole tree (returns ``[]``).
49
- - A scan root whose total file count exceeds the configured cap cannot prove any
50
- field dead, so the check returns ``[]`` on a cap hit.
51
- - A field read only by a module outside the resolved scan root is treated as dead
52
- — the same conservative scoping the dead-module-constant check accepts.
53
-
54
- Unlike the per-file dead-dataclass-field check, this cross-module check does NOT
55
- suppress on a dataclass-dunder whole-instance read — instance comparison
56
- (``cfg == other``), set or dict membership, formatted-string conversion
57
- (``f"{cfg}"``), or whole-instance stringification
58
- (``str(cfg)``/``repr(cfg)``/``format(cfg)``). Those syntactic forms are not bound
59
- to a config instance, and tree-wide one incidental match anywhere would disable
60
- the check on any realistic package. The consequence is a documented, rare
61
- limitation: a config-like field read ONLY via whole-instance dunder comparison or
62
- stringification, and never read directly anywhere in production, may be flagged.
63
- The augmented-assignment read mechanism (``cfg.field += 1`` reads ``field``
64
- before writing it) is precise and remains a counted read.
65
- """
66
-
67
- import ast
68
- import os
69
- import sys
70
- from collections.abc import Iterator
71
- from pathlib import Path
72
-
73
- _blocking_directory = str(Path(__file__).resolve().parent)
74
- _hooks_directory = str(Path(__file__).resolve().parent.parent)
75
- if _blocking_directory not in sys.path:
76
- sys.path.insert(0, _blocking_directory)
77
- if _hooks_directory not in sys.path:
78
- sys.path.insert(0, _hooks_directory)
79
-
80
- from code_rules_dead_dataclass_field import ( # noqa: E402
81
- _augmented_assignment_attribute_names,
82
- _dataclass_field_definitions,
83
- _is_dataclass,
84
- )
85
- from code_rules_dead_module_constant import ( # noqa: E402
86
- _scan_root_for_constants_module,
87
- )
88
- from code_rules_shared import ( # noqa: E402
89
- is_migration_file,
90
- is_test_file,
91
- )
92
-
93
- from hooks_constants.dead_config_field_constants import ( # noqa: E402
94
- ALL_CONFIG_CLASS_NAME_SUFFIXES,
95
- ALL_REFLECTIVE_FIELD_CONSUMER_NAMES,
96
- DATACLASSES_MODULE_NAME,
97
- DEAD_CONFIG_FIELD_GUIDANCE,
98
- MAX_DEAD_CONFIG_FIELD_ISSUES,
99
- MAX_SCAN_ROOT_FILE_COUNT,
100
- PYTHON_SOURCE_SUFFIX,
101
- WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME,
102
- )
103
-
104
-
105
- def _is_config_dataclass(class_node: ast.ClassDef) -> bool:
106
- """Return whether a class is a @dataclass whose name ends in a config-like suffix.
107
-
108
- A config-like surface is a ``@dataclass`` whose name ends in ``"Config"`` or
109
- ``"Selectors"``. Both shapes are defined in one module, bound to a
110
- module-level singleton, and read across files, so the per-file dead-field
111
- check cannot judge their fields and the cross-module scan covers them here.
112
-
113
- Args:
114
- class_node: The class definition node to test.
115
-
116
- Returns:
117
- True when the class carries a ``@dataclass`` decorator and its name ends
118
- in one of ``ALL_CONFIG_CLASS_NAME_SUFFIXES``.
119
- """
120
- return _is_dataclass(class_node) and class_node.name.endswith(
121
- ALL_CONFIG_CLASS_NAME_SUFFIXES
122
- )
123
-
124
-
125
- def _reads_whole_instance_reflectively(tree: ast.Module) -> bool:
126
- """Return whether a module consumes a whole instance via a reflective read.
127
-
128
- Detects a bare call to any reflective whole-instance consumer (``asdict``,
129
- ``astuple``, ``fields``, ``replace``, ``vars`` imported from ``dataclasses``),
130
- a ``dataclasses``-qualified call to the same consumers
131
- (``dataclasses.asdict(cfg)``, ``dataclasses.replace(cfg, ...)``), and a read
132
- of the ``__dict__`` attribute. The method-call form must be ``dataclasses``-
133
- qualified — an unrelated ``"text".replace(...)`` or ``frame.fields(...)`` on
134
- another object does not match. Each matched form reads every field of an
135
- instance at once without naming any single field, so a module that uses one
136
- cannot prove a config field unread.
137
-
138
- Args:
139
- tree: The parsed module to inspect.
140
-
141
- Returns:
142
- True when the module makes a bare or ``dataclasses``-qualified call to a
143
- reflective whole-instance consumer, or reads ``obj.__dict__``.
144
- """
145
- for each_node in ast.walk(tree):
146
- if isinstance(each_node, ast.Attribute):
147
- if each_node.attr == WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME:
148
- return True
149
- continue
150
- if not isinstance(each_node, ast.Call):
151
- continue
152
- function_node = each_node.func
153
- if isinstance(function_node, ast.Name) and function_node.id in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES:
154
- return True
155
- if (
156
- isinstance(function_node, ast.Attribute)
157
- and isinstance(function_node.value, ast.Name)
158
- and function_node.value.id == DATACLASSES_MODULE_NAME
159
- and function_node.attr in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES
160
- ):
161
- return True
162
- return False
163
-
164
-
165
- def _config_dataclass_names(tree: ast.Module) -> set[str]:
166
- """Return names of config-like ``@dataclass`` classes defined in a module.
167
-
168
- A constructor-keyword exclusion fires only for a callee that names a genuine
169
- config-like dataclass, so the caller first gathers the config-like dataclass
170
- names a module defines, then unions those names across the scan root.
171
-
172
- Args:
173
- tree: The parsed module to inspect.
174
-
175
- Returns:
176
- Every class name in the module that is a ``@dataclass`` whose name ends in
177
- one of ``ALL_CONFIG_CLASS_NAME_SUFFIXES``.
178
- """
179
- config_dataclass_names: set[str] = set()
180
- for each_node in ast.walk(tree):
181
- if isinstance(each_node, ast.ClassDef) and _is_config_dataclass(each_node):
182
- config_dataclass_names.add(each_node.name)
183
- return config_dataclass_names
184
-
185
-
186
- def _call_constructs_config_class(
187
- call_node: ast.Call, all_known_config_class_names: set[str]
188
- ) -> bool:
189
- """Return whether a call constructs a known config-like dataclass.
190
-
191
- A call whose callee names a config-like dataclass defined under the scan root —
192
- ``AppInfoConfig(...)`` or a qualified ``module.AppInfoConfig(...)`` —
193
- constructs the instance, and its keyword arguments write the named fields
194
- rather than read them. A factory function whose name merely ends in
195
- ``"Config"`` (``getThemeConfig(...)``) is not a known config-like dataclass, so
196
- its keyword arguments stay genuine reads.
197
-
198
- Args:
199
- call_node: The call expression to test.
200
- all_known_config_class_names: Names of config-like dataclasses defined under
201
- the scan root.
202
-
203
- Returns:
204
- True when the callee names a known config-like dataclass.
205
- """
206
- callee_node = call_node.func
207
- if isinstance(callee_node, ast.Name):
208
- return callee_node.id in all_known_config_class_names
209
- if isinstance(callee_node, ast.Attribute):
210
- return callee_node.attr in all_known_config_class_names
211
- return False
212
-
213
-
214
- def _config_constructor_keyword_node_ids(
215
- tree: ast.Module, all_known_config_class_names: set[str]
216
- ) -> set[int]:
217
- """Return ids of keyword nodes that write fields in a known config-like constructor.
218
-
219
- A keyword in an ``AppInfoConfig(field=value)`` call sets ``field`` rather than
220
- reading it, so its node id is collected for the caller to exclude. The
221
- exclusion is keyed per keyword node, not by name, so a same-named keyword in a
222
- ``replace(cfg, field=value)`` call — which reuses a live instance and stays a
223
- read — keeps its own distinct node and is not stripped.
224
-
225
- Args:
226
- tree: The parsed module to inspect.
227
- all_known_config_class_names: Names of config-like dataclasses defined under
228
- the scan root.
229
-
230
- Returns:
231
- The ``id()`` of every keyword node passed to a known config-like
232
- constructor call.
233
- """
234
- constructor_keyword_node_ids: set[int] = set()
235
- for each_node in ast.walk(tree):
236
- if not isinstance(each_node, ast.Call):
237
- continue
238
- if not _call_constructs_config_class(each_node, all_known_config_class_names):
239
- continue
240
- for each_keyword in each_node.keywords:
241
- if each_keyword.arg is not None:
242
- constructor_keyword_node_ids.add(id(each_keyword))
243
- return constructor_keyword_node_ids
244
-
245
-
246
- def _self_referential_default_attribute_node_ids(
247
- field_name: str, default_value: ast.expr
248
- ) -> set[int]:
249
- """Return ids of attribute reads in a default whose name equals the field.
250
-
251
- Walks a config-like field's default-value expression and collects the ``id()``
252
- of each ``ast.Attribute`` read whose ``.attr`` equals ``field_name``. Such a
253
- read — ``sound_upload_timeout_ms: int = submission_timing.sound_upload_timeout_ms``
254
- — names the field being defined inside the class body, so it is not a consumer
255
- of the field. An attribute read of a differently-named field
256
- (``timeout_ms: int = other_config.base_timeout``) is a genuine consumer and is
257
- left out of the returned set.
258
-
259
- Args:
260
- field_name: The name of the field being defined.
261
- default_value: The default-value expression of that field.
262
-
263
- Returns:
264
- The ``id()`` of every self-referential attribute read inside the
265
- default-value expression.
266
- """
267
- self_referential_node_ids: set[int] = set()
268
- for each_inner_node in ast.walk(default_value):
269
- if isinstance(each_inner_node, ast.Attribute) and each_inner_node.attr == field_name:
270
- self_referential_node_ids.add(id(each_inner_node))
271
- return self_referential_node_ids
272
-
273
-
274
- def _config_field_default_value_nodes(tree: ast.Module) -> set[int]:
275
- """Return ids of self-referential attribute reads in config-like field defaults.
276
-
277
- A field default such as ``sound_upload_timeout_ms: int =``
278
- ``submission_timing.sound_upload_timeout_ms`` is an attribute read whose name
279
- matches the field being defined. That self-referential read inside the
280
- config-like class body is not a consumer of the field, so its node id is
281
- collected here for the caller to exclude from the attribute-read set. Only the
282
- attribute read whose ``.attr`` equals the field name is collected; a default
283
- that sources a differently-named field on another object
284
- (``timeout_ms: int = other_config.base_timeout``) leaves that read counted, so
285
- ``base_timeout`` stays a live consumer.
286
-
287
- Args:
288
- tree: The parsed module to inspect.
289
-
290
- Returns:
291
- The ``id()`` of every self-referential attribute read within the
292
- default-value expression of a field declared in a config-like dataclass
293
- body.
294
- """
295
- default_value_node_ids: set[int] = set()
296
- for each_node in ast.walk(tree):
297
- if not isinstance(each_node, ast.ClassDef) or not _is_config_dataclass(each_node):
298
- continue
299
- for each_statement in each_node.body:
300
- if not isinstance(each_statement, ast.AnnAssign):
301
- continue
302
- if each_statement.value is None:
303
- continue
304
- if not isinstance(each_statement.target, ast.Name):
305
- continue
306
- default_value_node_ids |= _self_referential_default_attribute_node_ids(
307
- each_statement.target.id, each_statement.value
308
- )
309
- return default_value_node_ids
310
-
311
-
312
- def _attribute_read_names_in_tree(
313
- tree: ast.Module, all_known_config_class_names: set[str]
314
- ) -> tuple[set[str], bool]:
315
- """Return attribute names read in a parsed module and a suppression flag.
316
-
317
- Collects attribute names via five mechanisms: ``ast.Attribute.attr`` values
318
- in Load context, augmented-assignment targets (so ``cfg.debug_port += 1``
319
- contributes ``"debug_port"`` because ``+=`` reads the attribute before
320
- writing it), string literals (so ``getattr(obj, "field")`` contributes
321
- ``"field"``), keyword-argument names (so ``replace(cfg, debug_port=1)``
322
- contributes ``"debug_port"``), and ``ast.MatchClass.kwd_attrs`` names (so
323
- ``case Config(field=x)`` contributes ``"field"``). Two field-write forms are
324
- excluded because they name a field without consuming it: a keyword that writes
325
- a field in a known config-like constructor (``AppInfoConfig(field=value)``,
326
- excluded per keyword node so a same-named ``replace`` keyword stays a read),
327
- and a self-referential attribute read inside a config-like dataclass field's
328
- own default-value expression (``field: int = source.field`` in the class body,
329
- excluded only when the read name equals the field name) — counting either
330
- would hide a field that is written but read by no module. A keyword passed to a
331
- factory function whose name merely ends in ``"Config"`` is not a known
332
- config-like constructor, so it stays a read. The boolean reports whether the module
333
- suppresses the dead-field check, which it does only when it reflectively reads a
334
- whole instance — a bare or ``dataclasses``-qualified
335
- ``asdict``/``astuple``/``fields``/``replace``/``vars`` call, or an
336
- ``obj.__dict__`` read — because that pattern reads every field at once without
337
- naming any single field, so the caller treats it as "cannot prove any field
338
- dead".
339
-
340
- Args:
341
- tree: The parsed module to inspect.
342
- all_known_config_class_names: Names of config-like dataclasses defined under
343
- the scan root, used to scope the constructor-keyword exclusion to
344
- genuine config-like constructors.
345
-
346
- Returns:
347
- A (read_names, suppresses_dead_field_check) pair. The name set is every
348
- attribute name the module reads via the mechanisms above, excluding known
349
- config-like constructor keyword nodes and self-referential config-like-field
350
- default-value attribute reads; suppresses_dead_field_check is True only when
351
- a reflective whole-instance read is present.
352
- """
353
- all_read_names: set[str] = _augmented_assignment_attribute_names(tree)
354
- config_constructor_keyword_node_ids = _config_constructor_keyword_node_ids(
355
- tree, all_known_config_class_names
356
- )
357
- config_field_default_node_ids = _config_field_default_value_nodes(tree)
358
- for each_node in ast.walk(tree):
359
- if (
360
- isinstance(each_node, ast.Attribute)
361
- and isinstance(each_node.ctx, ast.Load)
362
- and id(each_node) not in config_field_default_node_ids
363
- ):
364
- all_read_names.add(each_node.attr)
365
- elif isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
366
- all_read_names.add(each_node.value)
367
- elif isinstance(each_node, ast.MatchClass):
368
- all_read_names.update(each_node.kwd_attrs)
369
- elif (
370
- isinstance(each_node, ast.keyword)
371
- and each_node.arg is not None
372
- and id(each_node) not in config_constructor_keyword_node_ids
373
- ):
374
- all_read_names.add(each_node.arg)
375
- suppresses_dead_field_check = _reads_whole_instance_reflectively(tree)
376
- return all_read_names, suppresses_dead_field_check
377
-
378
-
379
- def _iter_production_module_sources(
380
- scan_root: Path,
381
- written_path: Path,
382
- written_content: str,
383
- ) -> Iterator[str | None]:
384
- """Yield the source of each production module under the scan root.
385
-
386
- Yields ``written_content`` for the written module so the current edit is
387
- included, then each sibling production module's source (excluding test and
388
- migration files). A sibling whose text cannot be read is skipped. A single
389
- ``None`` is yielded when the production module count exceeds the configured
390
- file cap, signalling the caller that no field can be proven dead.
391
-
392
- Args:
393
- scan_root: The directory tree to scan.
394
- written_path: The resolved path of the module being written.
395
- written_content: The post-edit text of the written module.
396
-
397
- Yields:
398
- Each production module's source text, or a single ``None`` on a cap hit.
399
- """
400
- yield written_content
401
- written_path_key = os.path.normcase(str(written_path))
402
- scanned_file_count = 1
403
- for each_path in scan_root.rglob("*" + PYTHON_SOURCE_SUFFIX):
404
- if not each_path.is_file():
405
- continue
406
- if os.path.normcase(str(each_path.resolve())) == written_path_key:
407
- continue
408
- if is_test_file(str(each_path)):
409
- continue
410
- if is_migration_file(str(each_path)):
411
- continue
412
- scanned_file_count += 1
413
- if scanned_file_count > MAX_SCAN_ROOT_FILE_COUNT:
414
- yield None
415
- return
416
- try:
417
- yield each_path.read_text(encoding="utf-8")
418
- except (OSError, UnicodeDecodeError):
419
- continue
420
-
421
-
422
- def _all_production_read_names_under_root(
423
- scan_root: Path,
424
- written_path: Path,
425
- written_content: str,
426
- ) -> tuple[set[str], bool, bool]:
427
- """Return read names, a cap-hit flag, and a suppression flag for the tree.
428
-
429
- Reads and AST-parses every production ``.py`` module under ``scan_root``
430
- (excluding test and migration files) at most once: the module sources are
431
- materialized once, a first pass over the cached sources gathers every
432
- config-like dataclass name defined under the root so the constructor-keyword
433
- exclusion fires only for a genuine config-like constructor, and a second pass over
434
- the same cached sources collects attribute reads. The written module's
435
- post-edit content replaces its on-disk text so the current edit is included.
436
- Scanning stops at the configured file cap. A module that reflectively reads a
437
- whole instance — a bare or ``dataclasses``-qualified
438
- ``asdict``/``astuple``/``fields``/``replace``/``vars`` call, or an
439
- ``obj.__dict__`` read — sets the suppression flag, signalling the caller that
440
- no field can be proven dead.
441
-
442
- Args:
443
- scan_root: The directory tree to scan.
444
- written_path: The resolved path of the module being written.
445
- written_content: The post-edit text of the written module.
446
-
447
- Returns:
448
- A (read_names, cap_was_hit, suppresses_dead_field_check) triple. The name
449
- set is the union of attribute reads across every scanned production
450
- module; cap_was_hit is True when the scan stopped at the configured file
451
- cap before finishing the tree; suppresses_dead_field_check is True when
452
- any scanned module reflectively reads a whole instance.
453
- """
454
- all_module_sources = list(
455
- _iter_production_module_sources(scan_root, written_path, written_content)
456
- )
457
- if None in all_module_sources:
458
- return set(), True, False
459
- all_production_trees: list[ast.Module] = []
460
- for each_source in all_module_sources:
461
- if each_source is None:
462
- continue
463
- try:
464
- all_production_trees.append(ast.parse(each_source))
465
- except SyntaxError:
466
- continue
467
- all_known_config_class_names: set[str] = set()
468
- for each_tree in all_production_trees:
469
- all_known_config_class_names |= _config_dataclass_names(each_tree)
470
- all_read_names: set[str] = set()
471
- suppresses_dead_field_check = False
472
- for each_tree in all_production_trees:
473
- module_read_names, module_suppresses_dead_field_check = _attribute_read_names_in_tree(
474
- each_tree, all_known_config_class_names
475
- )
476
- all_read_names |= module_read_names
477
- suppresses_dead_field_check = (
478
- suppresses_dead_field_check or module_suppresses_dead_field_check
479
- )
480
- return all_read_names, False, suppresses_dead_field_check
481
-
482
-
483
- def check_dead_config_dataclass_fields(
484
- content: str, file_path: str, full_file_content: str | None = None
485
- ) -> list[str]:
486
- """Flag a config-like @dataclass field read by no production module in the package tree.
487
-
488
- Runs a cross-module scan restricted to ``@dataclass`` classes whose name ends
489
- in ``"Config"`` or ``"Selectors"`` — both are config-like surfaces bound to a
490
- module-level singleton and read across files. For each such dataclass in the
491
- written file, every
492
- instance field whose name does not appear as an attribute read (``obj.field``),
493
- augmented-assignment target (``cfg.field += 1``), string literal,
494
- non-constructor keyword-argument name (``replace`` keyword), or match-pattern
495
- keyword attribute in any production module under the enclosing scan root is
496
- flagged as dead. A keyword that writes a field in a config-like constructor
497
- (``ThemeUpdateConfig(debug_port=1)``) is a write, not a read, so it does not
498
- clear a field — a field set by a constructor keyword and read by no module is
499
- flagged. When any production module under the scan root reflectively
500
- reads a whole instance — a bare or ``dataclasses``-qualified call to
501
- ``asdict``, ``astuple``, ``fields``, ``replace``, or ``vars``, or a read of
502
- ``obj.__dict__`` — the check is suppressed for the whole tree and returns
503
- ``[]``, since that pattern reads every field at once without naming any single
504
- field. Test and
505
- migration files are exempt as write destinations; production modules under the
506
- scan root are scanned while test and migration modules in the tree are excluded
507
- so fields read only by test code are still flagged as dead-in-production.
508
- Whole-file analysis runs against ``full_file_content`` when supplied so an Edit
509
- fragment is judged against the reconstructed post-edit file. A scan root
510
- exceeding the file cap returns ``[]`` (cannot prove dead). The scan root is
511
- resolved the same way as the dead-module-constant check: a ``config/`` module's
512
- root is its parent directory, a module in a package directory's root is the
513
- package's parent, and a top-level module's root is its enclosing directory.
514
-
515
- Args:
516
- content: The new content under validation (Edit fragment or whole file).
517
- file_path: The destination path, used for the test/migration exemptions
518
- and scan-root resolution.
519
- full_file_content: The reconstructed post-edit whole-file content for an
520
- Edit, or None for a Write where ``content`` is already the whole file.
521
-
522
- Returns:
523
- One violation message per dead config-like dataclass field, capped at the
524
- configured maximum. Returns an empty list when the file is exempt, no
525
- qualifying config-like dataclass is found, the scan root exceeds the file
526
- cap, or a SyntaxError prevents parsing.
527
- """
528
- if is_test_file(file_path):
529
- return []
530
- if is_migration_file(file_path):
531
- return []
532
- effective_content = content if full_file_content is None else full_file_content
533
- try:
534
- tree = ast.parse(effective_content)
535
- except SyntaxError:
536
- return []
537
- all_config_classes = [
538
- each_node
539
- for each_node in ast.walk(tree)
540
- if isinstance(each_node, ast.ClassDef) and _is_config_dataclass(each_node)
541
- ]
542
- if not all_config_classes:
543
- return []
544
- scan_root = _scan_root_for_constants_module(file_path)
545
- written_path = Path(file_path).resolve()
546
- all_read_names, cap_was_hit, suppresses_dead_field_check = (
547
- _all_production_read_names_under_root(
548
- scan_root,
549
- written_path,
550
- effective_content,
551
- )
552
- )
553
- if cap_was_hit:
554
- return []
555
- if suppresses_dead_field_check:
556
- return []
557
- all_issues: list[str] = []
558
- for each_class in all_config_classes:
559
- for each_field_name, each_field_line in _dataclass_field_definitions(each_class):
560
- if each_field_name in all_read_names:
561
- continue
562
- all_issues.append(
563
- f"Line {each_field_line}: dataclass field {each_field_name!r}"
564
- f" on {each_class.name} - {DEAD_CONFIG_FIELD_GUIDANCE}"
565
- )
566
- if len(all_issues) >= MAX_DEAD_CONFIG_FIELD_ISSUES:
567
- return all_issues
568
- return all_issues