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.
- package/.agents/agents/AGENTS.md +0 -31
- package/.agents/agents/clean-coder.md +1 -1
- package/.agents/agents/test_agent_frontmatter.py +3 -1
- package/.agents/skills/AGENTS.md +0 -70
- package/.agents/skills/_shared/AGENTS.md +0 -44
- package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
- package/.agents/skills/autoconverge/AGENTS.md +0 -35
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
- package/.agents/skills/everything-search/AGENTS.md +0 -16
- package/.agents/skills/fresh-branch/AGENTS.md +0 -13
- package/.agents/skills/grok-spawn/AGENTS.md +0 -27
- package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/SKILL.md +44 -53
- package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
- package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
- package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
- package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
- package/.agents/skills/rebase/AGENTS.md +0 -31
- package/.agents/skills/session-log/AGENTS.md +0 -31
- package/.agents/skills/session-tidy/AGENTS.md +0 -35
- package/.agents/skills/skill-builder/AGENTS.md +0 -48
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
- package/.agents/skills/task-build/AGENTS.md +0 -28
- package/.agents/skills/update/AGENTS.md +0 -37
- package/.agents/skills-archived/AGENTS.md +0 -44
- package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
- package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
- package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
- package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
- package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
- package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
- package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
- package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
- package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
- package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
- package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
- package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
- package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/.agents/skills-archived/recall/AGENTS.md +0 -29
- package/.agents/skills-archived/remember/AGENTS.md +0 -30
- package/AGENTS.md +0 -112
- package/_shared/AGENTS.md +0 -16
- package/_shared/advisor/AGENTS.md +0 -21
- package/_shared/advisor/advisor-protocol.md +4 -1
- package/_shared/advisor/reference/consult-format.md +4 -2
- package/_shared/pr-loop/AGENTS.md +0 -27
- package/_shared/pr-loop/scripts/AGENTS.md +0 -49
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
- package/_shared/process-tree/AGENTS.md +0 -40
- package/audit-rubrics/AGENTS.md +0 -42
- package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/AGENTS.md +0 -36
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/bin/AGENTS.md +0 -118
- package/bin/install.test.mjs +1 -1
- package/commands/AGENTS.md +0 -14
- package/docs/AGENTS.md +0 -31
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/AGENTS.md +0 -16
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/AGENTS.md +0 -27
- package/hooks/advisory/AGENTS.md +0 -15
- package/hooks/blocking/AGENTS.md +0 -122
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/AGENTS.md +0 -9
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/git-hooks/AGENTS.md +0 -31
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
- package/hooks/hooks_constants/AGENTS.md +0 -104
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/hooks/lifecycle/AGENTS.md +0 -17
- package/hooks/observability/AGENTS.md +0 -19
- package/hooks/session/AGENTS.md +0 -37
- package/hooks/validation/AGENTS.md +0 -19
- package/hooks/validators/AGENTS.md +0 -51
- package/hooks/workflow/AGENTS.md +0 -15
- package/output-styles/AGENTS.md +0 -14
- package/package.json +1 -1
- package/rules/AGENTS.md +0 -58
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/AGENTS.md +0 -55
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
- package/scripts/sync_to_cursor/AGENTS.md +0 -22
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/AGENTS.md +0 -34
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/AGENTS.md +0 -24
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- package/rules/parallel-tools.md +0 -23
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import os
|
|
5
|
-
import shutil
|
|
6
|
-
import stat
|
|
7
|
-
import tempfile
|
|
8
|
-
from collections.abc import Callable, Iterator
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
14
|
-
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
15
|
-
assert specification is not None and specification.loader is not None
|
|
16
|
-
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
17
|
-
specification.loader.exec_module(code_rules_enforcer)
|
|
18
|
-
|
|
19
|
-
PRODUCTION_FILE_PATH = "packages/app/services/report.py"
|
|
20
|
-
TEST_FILE_PATH = "packages/app/services/test_report.py"
|
|
21
|
-
MIGRATION_FILE_PATH = "packages/app/migrations/0001_initial.py"
|
|
22
|
-
|
|
23
|
-
THEME_UPDATE_CONFIG_BODY = (
|
|
24
|
-
"from dataclasses import dataclass\n"
|
|
25
|
-
"\n"
|
|
26
|
-
"@dataclass\n"
|
|
27
|
-
"class ThemeUpdateConfig:\n"
|
|
28
|
-
" portal_url: str\n"
|
|
29
|
-
" debug_port: int\n"
|
|
30
|
-
" timeout_seconds: int\n"
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _strip_read_only_and_retry(
|
|
35
|
-
removal_function: Callable[[str], object],
|
|
36
|
-
target_path: str,
|
|
37
|
-
_exc_info: BaseException,
|
|
38
|
-
) -> None:
|
|
39
|
-
try:
|
|
40
|
-
os.chmod(target_path, stat.S_IWRITE)
|
|
41
|
-
removal_function(target_path)
|
|
42
|
-
except OSError:
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@pytest.fixture
|
|
47
|
-
def neutral_root() -> Iterator[Path]:
|
|
48
|
-
"""Yield a temp directory whose path carries no ``test_`` segment.
|
|
49
|
-
|
|
50
|
-
The enforcer's ``is_test_file`` keys on the full path string, and pytest's
|
|
51
|
-
own ``tmp_path`` directory name embeds the test name, which would make every
|
|
52
|
-
synthetic config path look like a test file. A neutral ``mkdtemp`` root
|
|
53
|
-
mirrors how a production config module path looks.
|
|
54
|
-
"""
|
|
55
|
-
neutral_directory = Path(tempfile.mkdtemp(prefix="deadcfg-")).resolve()
|
|
56
|
-
try:
|
|
57
|
-
yield neutral_directory
|
|
58
|
-
finally:
|
|
59
|
-
shutil.rmtree(neutral_directory, onexc=_strip_read_only_and_retry)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _check(source: str, file_path: str) -> list[str]:
|
|
63
|
-
return code_rules_enforcer.check_dead_config_dataclass_fields(source, file_path)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _build_config_package(
|
|
67
|
-
workflow_directory: Path,
|
|
68
|
-
config_body: str,
|
|
69
|
-
consumer_body: str,
|
|
70
|
-
) -> Path:
|
|
71
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
72
|
-
config_package.mkdir(parents=True)
|
|
73
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
74
|
-
config_path = config_package / "config.py"
|
|
75
|
-
config_path.write_text(config_body, encoding="utf-8")
|
|
76
|
-
(workflow_directory / "runner.py").write_text(consumer_body, encoding="utf-8")
|
|
77
|
-
return config_path
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_flags_config_field_read_by_no_production_module(neutral_root: Path) -> None:
|
|
81
|
-
consumer_body = (
|
|
82
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
83
|
-
"\n"
|
|
84
|
-
"def run(configuration: ThemeUpdateConfig) -> None:\n"
|
|
85
|
-
" print(configuration.portal_url)\n"
|
|
86
|
-
" print(configuration.timeout_seconds)\n"
|
|
87
|
-
)
|
|
88
|
-
config_path = _build_config_package(
|
|
89
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
90
|
-
)
|
|
91
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
92
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
93
|
-
f"Expected dead 'debug_port' flagged, got: {issues}"
|
|
94
|
-
)
|
|
95
|
-
assert not any(
|
|
96
|
-
"'portal_url'" in each_issue or "'timeout_seconds'" in each_issue for each_issue in issues
|
|
97
|
-
), f"Fields read in consumer must not be flagged, got: {issues}"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_does_not_flag_field_read_as_attribute_in_sibling_module(neutral_root: Path) -> None:
|
|
101
|
-
consumer_body = (
|
|
102
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
103
|
-
"\n"
|
|
104
|
-
"def run(configuration: ThemeUpdateConfig) -> None:\n"
|
|
105
|
-
" print(configuration.portal_url)\n"
|
|
106
|
-
" print(configuration.debug_port)\n"
|
|
107
|
-
" print(configuration.timeout_seconds)\n"
|
|
108
|
-
)
|
|
109
|
-
config_path = _build_config_package(
|
|
110
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
111
|
-
)
|
|
112
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
113
|
-
assert issues == [], f"All fields are read in consumer, none must be flagged, got: {issues}"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_flags_field_read_only_by_test_module(neutral_root: Path) -> None:
|
|
117
|
-
workflow_directory = neutral_root / "workflow"
|
|
118
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
119
|
-
config_package.mkdir(parents=True)
|
|
120
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
121
|
-
config_path = config_package / "config.py"
|
|
122
|
-
config_path.write_text(THEME_UPDATE_CONFIG_BODY, encoding="utf-8")
|
|
123
|
-
test_body = (
|
|
124
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
125
|
-
"\n"
|
|
126
|
-
"def test_debug_port_default() -> None:\n"
|
|
127
|
-
" cfg = ThemeUpdateConfig(portal_url='x', debug_port=8471, timeout_seconds=30)\n"
|
|
128
|
-
" assert cfg.debug_port == 8471\n"
|
|
129
|
-
" assert cfg.portal_url == 'x'\n"
|
|
130
|
-
" assert cfg.timeout_seconds == 30\n"
|
|
131
|
-
)
|
|
132
|
-
(workflow_directory / "test_config.py").write_text(test_body, encoding="utf-8")
|
|
133
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
134
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
135
|
-
f"Field read only by test code must still be flagged as dead-in-production, got: {issues}"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def test_ignores_non_config_named_dataclass(neutral_root: Path) -> None:
|
|
140
|
-
source = (
|
|
141
|
-
"from dataclasses import dataclass\n"
|
|
142
|
-
"\n"
|
|
143
|
-
"@dataclass\n"
|
|
144
|
-
"class ThemeMetadata:\n"
|
|
145
|
-
" title: str\n"
|
|
146
|
-
" debug_port: int\n"
|
|
147
|
-
)
|
|
148
|
-
workflow_directory = neutral_root / "workflow"
|
|
149
|
-
workflow_directory.mkdir(parents=True)
|
|
150
|
-
module_path = workflow_directory / "metadata.py"
|
|
151
|
-
module_path.write_text(source, encoding="utf-8")
|
|
152
|
-
issues = _check(source, str(module_path))
|
|
153
|
-
assert issues == [], (
|
|
154
|
-
f"Non-Config-named dataclasses are outside scope of this check, got: {issues}"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def test_does_not_flag_field_read_via_string_literal(neutral_root: Path) -> None:
|
|
159
|
-
consumer_body = (
|
|
160
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
161
|
-
"\n"
|
|
162
|
-
"def read_field(configuration: ThemeUpdateConfig, field_name: str) -> object:\n"
|
|
163
|
-
" return getattr(configuration, 'debug_port')\n"
|
|
164
|
-
)
|
|
165
|
-
config_path = _build_config_package(
|
|
166
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
167
|
-
)
|
|
168
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
169
|
-
assert not any("'debug_port'" in each_issue for each_issue in issues), (
|
|
170
|
-
f"String literal getattr read must count as a field read, got: {issues}"
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def test_does_not_flag_when_consumer_serializes_whole_instance_via_asdict(
|
|
175
|
-
neutral_root: Path,
|
|
176
|
-
) -> None:
|
|
177
|
-
consumer_body = (
|
|
178
|
-
"from dataclasses import asdict\n"
|
|
179
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
180
|
-
"\n"
|
|
181
|
-
"def serialize(configuration: ThemeUpdateConfig) -> dict[str, object]:\n"
|
|
182
|
-
" return asdict(configuration)\n"
|
|
183
|
-
)
|
|
184
|
-
config_path = _build_config_package(
|
|
185
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
186
|
-
)
|
|
187
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
188
|
-
assert issues == [], (
|
|
189
|
-
f"asdict reads every field at once, so no field may be flagged, got: {issues}"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def test_does_not_flag_when_consumer_reads_instance_dict(neutral_root: Path) -> None:
|
|
194
|
-
consumer_body = (
|
|
195
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
196
|
-
"\n"
|
|
197
|
-
"def serialize(configuration: ThemeUpdateConfig) -> dict[str, object]:\n"
|
|
198
|
-
" return dict(configuration.__dict__)\n"
|
|
199
|
-
)
|
|
200
|
-
config_path = _build_config_package(
|
|
201
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
202
|
-
)
|
|
203
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
204
|
-
assert issues == [], (
|
|
205
|
-
f"__dict__ read consumes every field at once, so none may be flagged, got: {issues}"
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def test_does_not_flag_field_used_only_as_replace_keyword(neutral_root: Path) -> None:
|
|
210
|
-
consumer_body = (
|
|
211
|
-
"from dataclasses import replace\n"
|
|
212
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
213
|
-
"\n"
|
|
214
|
-
"def repoint(configuration: ThemeUpdateConfig) -> ThemeUpdateConfig:\n"
|
|
215
|
-
" return replace(configuration, debug_port=9999)\n"
|
|
216
|
-
)
|
|
217
|
-
config_path = _build_config_package(
|
|
218
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
219
|
-
)
|
|
220
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
221
|
-
assert not any("'debug_port'" in each_issue for each_issue in issues), (
|
|
222
|
-
f"replace keyword usage of debug_port must count as a read, got: {issues}"
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def test_flags_field_set_only_by_constructor_keyword_and_read_nowhere(
|
|
227
|
-
neutral_root: Path,
|
|
228
|
-
) -> None:
|
|
229
|
-
"""A field set ONLY by a ``*Config`` constructor keyword, read nowhere, is dead.
|
|
230
|
-
|
|
231
|
-
A constructor keyword writes the field; it is not a read. When ``debug_port``
|
|
232
|
-
is set by ``ThemeUpdateConfig(debug_port=1)`` and read through no config
|
|
233
|
-
instance anywhere in production, tuning it has no effect, so it is flagged as
|
|
234
|
-
dead config (CODE_RULES §9.8).
|
|
235
|
-
"""
|
|
236
|
-
consumer_body = (
|
|
237
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
238
|
-
"\n"
|
|
239
|
-
"def build() -> ThemeUpdateConfig:\n"
|
|
240
|
-
" configuration = ThemeUpdateConfig(portal_url='x', debug_port=1, timeout_seconds=99)\n"
|
|
241
|
-
" print(configuration.portal_url)\n"
|
|
242
|
-
" print(configuration.timeout_seconds)\n"
|
|
243
|
-
" return configuration\n"
|
|
244
|
-
)
|
|
245
|
-
config_path = _build_config_package(
|
|
246
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
247
|
-
)
|
|
248
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
249
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
250
|
-
f"Field set only by constructor keyword and read nowhere must be flagged, got: {issues}"
|
|
251
|
-
)
|
|
252
|
-
assert not any(
|
|
253
|
-
"'portal_url'" in each_issue or "'timeout_seconds'" in each_issue for each_issue in issues
|
|
254
|
-
), f"Fields read through the config instance must not be flagged, got: {issues}"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def test_qualified_config_constructor_keyword_does_not_clear_field(
|
|
258
|
-
neutral_root: Path,
|
|
259
|
-
) -> None:
|
|
260
|
-
"""A qualified ``module.ThemeUpdateConfig(field=value)`` keyword is a write, not a read.
|
|
261
|
-
|
|
262
|
-
The constructor callee may be a qualified attribute (``config_module.ThemeUpdateConfig``)
|
|
263
|
-
rather than a bare name. Its keyword still writes the field, so a field set only
|
|
264
|
-
this way and read through no config instance is flagged dead.
|
|
265
|
-
"""
|
|
266
|
-
consumer_body = (
|
|
267
|
-
"import os_update_workflow.config as config_module\n"
|
|
268
|
-
"\n"
|
|
269
|
-
"def build() -> config_module.ThemeUpdateConfig:\n"
|
|
270
|
-
" configuration = config_module.ThemeUpdateConfig(\n"
|
|
271
|
-
" portal_url='x', debug_port=1, timeout_seconds=99\n"
|
|
272
|
-
" )\n"
|
|
273
|
-
" print(configuration.portal_url)\n"
|
|
274
|
-
" print(configuration.timeout_seconds)\n"
|
|
275
|
-
" return configuration\n"
|
|
276
|
-
)
|
|
277
|
-
config_path = _build_config_package(
|
|
278
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
279
|
-
)
|
|
280
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
281
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
282
|
-
f"A qualified config constructor keyword is a write, so debug_port is flagged, got: {issues}"
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def test_does_not_flag_field_set_by_constructor_keyword_and_read_elsewhere(
|
|
287
|
-
neutral_root: Path,
|
|
288
|
-
) -> None:
|
|
289
|
-
"""A field set by a constructor keyword AND read via attribute elsewhere is live.
|
|
290
|
-
|
|
291
|
-
The constructor keyword does not clear the field, but a genuine attribute read
|
|
292
|
-
of the same field in another module does, so the field is not flagged.
|
|
293
|
-
"""
|
|
294
|
-
builder_body = (
|
|
295
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
296
|
-
"\n"
|
|
297
|
-
"def build() -> ThemeUpdateConfig:\n"
|
|
298
|
-
" return ThemeUpdateConfig(portal_url='x', debug_port=1, timeout_seconds=99)\n"
|
|
299
|
-
)
|
|
300
|
-
config_path = _build_config_package(
|
|
301
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, builder_body
|
|
302
|
-
)
|
|
303
|
-
reader_body = (
|
|
304
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
305
|
-
"\n"
|
|
306
|
-
"def connect(configuration: ThemeUpdateConfig) -> int:\n"
|
|
307
|
-
" return configuration.debug_port\n"
|
|
308
|
-
)
|
|
309
|
-
(config_path.parent.parent / "reader.py").write_text(reader_body, encoding="utf-8")
|
|
310
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
311
|
-
assert not any("'debug_port'" in each_issue for each_issue in issues), (
|
|
312
|
-
f"An attribute read in another module keeps debug_port live, got: {issues}"
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def test_flags_field_set_by_default_and_constructor_keyword_only(
|
|
317
|
-
neutral_root: Path,
|
|
318
|
-
) -> None:
|
|
319
|
-
"""The PR #317 dead-config shape: field set by config default + constructor keyword only.
|
|
320
|
-
|
|
321
|
-
``AppInfoConfig.sound_upload_max_attempts: int = submission_timing.sound_upload_max_attempts``
|
|
322
|
-
sets the field from a same-named attribute on another object inside the config
|
|
323
|
-
body, and the orchestrator sets ``sound_upload_timeout_ms`` by a constructor
|
|
324
|
-
keyword. Neither field is read through any config instance. The default-value
|
|
325
|
-
read inside the config body and the constructor keyword are both writes, so
|
|
326
|
-
both fields are flagged dead.
|
|
327
|
-
|
|
328
|
-
Residual limitation: when a consumer module's constructor VALUE expression
|
|
329
|
-
itself reads a same-named attribute on a different object
|
|
330
|
-
(``AppInfoConfig(field=other.field)``), the object-blind attribute-read
|
|
331
|
-
collector counts ``field`` as read and the field escapes. This test keeps the
|
|
332
|
-
constructor value a literal so the keyword-write and default-write exclusions
|
|
333
|
-
are exercised without that foreign-attribute leak.
|
|
334
|
-
"""
|
|
335
|
-
config_body = (
|
|
336
|
-
"from dataclasses import dataclass\n"
|
|
337
|
-
"import submission_timing_module as submission_timing\n"
|
|
338
|
-
"\n"
|
|
339
|
-
"@dataclass(frozen=True)\n"
|
|
340
|
-
"class AppInfoConfig:\n"
|
|
341
|
-
" sound_upload_timeout_ms: int = submission_timing.sound_upload_timeout_ms\n"
|
|
342
|
-
" sound_upload_max_attempts: int = submission_timing.sound_upload_max_attempts\n"
|
|
343
|
-
)
|
|
344
|
-
workflow_directory = neutral_root / "workflow"
|
|
345
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
346
|
-
config_package.mkdir(parents=True)
|
|
347
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
348
|
-
config_path = config_package / "config.py"
|
|
349
|
-
config_path.write_text(config_body, encoding="utf-8")
|
|
350
|
-
orchestrator_body = (
|
|
351
|
-
"from os_update_workflow.config import AppInfoConfig\n"
|
|
352
|
-
"\n"
|
|
353
|
-
"def build() -> AppInfoConfig:\n"
|
|
354
|
-
" config = AppInfoConfig(sound_upload_timeout_ms=60000)\n"
|
|
355
|
-
" return config\n"
|
|
356
|
-
)
|
|
357
|
-
(workflow_directory / "orchestrator.py").write_text(orchestrator_body, encoding="utf-8")
|
|
358
|
-
issues = _check(config_body, str(config_path))
|
|
359
|
-
assert any("'sound_upload_timeout_ms'" in each_issue for each_issue in issues), (
|
|
360
|
-
f"Field set by constructor keyword and read nowhere must be flagged, got: {issues}"
|
|
361
|
-
)
|
|
362
|
-
assert any("'sound_upload_max_attempts'" in each_issue for each_issue in issues), (
|
|
363
|
-
f"Field set by config default only and read nowhere must be flagged, got: {issues}"
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def test_does_not_flag_config_default_field_read_through_instance(
|
|
368
|
-
neutral_root: Path,
|
|
369
|
-
) -> None:
|
|
370
|
-
"""A field whose default reads a foreign attribute but is read through the instance is live.
|
|
371
|
-
|
|
372
|
-
The default-value exclusion only drops the self-referential read inside the
|
|
373
|
-
config body; a genuine ``config.sound_upload_timeout_ms`` read in production
|
|
374
|
-
keeps the field live.
|
|
375
|
-
"""
|
|
376
|
-
config_body = (
|
|
377
|
-
"from dataclasses import dataclass\n"
|
|
378
|
-
"import submission_timing_module as submission_timing\n"
|
|
379
|
-
"\n"
|
|
380
|
-
"@dataclass(frozen=True)\n"
|
|
381
|
-
"class AppInfoConfig:\n"
|
|
382
|
-
" sound_upload_timeout_ms: int = submission_timing.sound_upload_timeout_ms\n"
|
|
383
|
-
)
|
|
384
|
-
workflow_directory = neutral_root / "workflow"
|
|
385
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
386
|
-
config_package.mkdir(parents=True)
|
|
387
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
388
|
-
config_path = config_package / "config.py"
|
|
389
|
-
config_path.write_text(config_body, encoding="utf-8")
|
|
390
|
-
processor_body = (
|
|
391
|
-
"from os_update_workflow.config import AppInfoConfig\n"
|
|
392
|
-
"\n"
|
|
393
|
-
"def wait(config: AppInfoConfig) -> int:\n"
|
|
394
|
-
" return config.sound_upload_timeout_ms\n"
|
|
395
|
-
)
|
|
396
|
-
(workflow_directory / "processor.py").write_text(processor_body, encoding="utf-8")
|
|
397
|
-
issues = _check(config_body, str(config_path))
|
|
398
|
-
assert issues == [], (
|
|
399
|
-
f"A genuine config-instance read keeps the field live, got: {issues}"
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def test_config_default_sourcing_differently_named_field_keeps_it_live(
|
|
404
|
-
neutral_root: Path,
|
|
405
|
-
) -> None:
|
|
406
|
-
"""A default that sources a DIFFERENTLY-named field on another config keeps it live.
|
|
407
|
-
|
|
408
|
-
``AppConfig.timeout_ms: int = DEFAULT_TIMING.base_timeout`` reads
|
|
409
|
-
``base_timeout`` on another config object inside the class body. The
|
|
410
|
-
default-value exclusion drops only the self-referential read whose attribute
|
|
411
|
-
name equals the field being defined; a differently-named attribute read is a
|
|
412
|
-
genuine consumer of ``base_timeout``, so it stays live and is not flagged.
|
|
413
|
-
"""
|
|
414
|
-
config_body = (
|
|
415
|
-
"from dataclasses import dataclass\n"
|
|
416
|
-
"\n"
|
|
417
|
-
"@dataclass(frozen=True)\n"
|
|
418
|
-
"class TimingConfig:\n"
|
|
419
|
-
" base_timeout: int\n"
|
|
420
|
-
" poll_interval: int\n"
|
|
421
|
-
"\n"
|
|
422
|
-
"DEFAULT_TIMING = TimingConfig(base_timeout=30, poll_interval=5)\n"
|
|
423
|
-
"\n"
|
|
424
|
-
"@dataclass(frozen=True)\n"
|
|
425
|
-
"class AppConfig:\n"
|
|
426
|
-
" timeout_ms: int = DEFAULT_TIMING.base_timeout\n"
|
|
427
|
-
" poll_interval: int = DEFAULT_TIMING.poll_interval\n"
|
|
428
|
-
)
|
|
429
|
-
workflow_directory = neutral_root / "workflow"
|
|
430
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
431
|
-
config_package.mkdir(parents=True)
|
|
432
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
433
|
-
config_path = config_package / "config.py"
|
|
434
|
-
config_path.write_text(config_body, encoding="utf-8")
|
|
435
|
-
consumer_body = (
|
|
436
|
-
"from os_update_workflow.config import AppConfig\n"
|
|
437
|
-
"\n"
|
|
438
|
-
"def run(app_config: AppConfig) -> int:\n"
|
|
439
|
-
" return app_config.timeout_ms + app_config.poll_interval\n"
|
|
440
|
-
)
|
|
441
|
-
(workflow_directory / "runner.py").write_text(consumer_body, encoding="utf-8")
|
|
442
|
-
issues = _check(config_body, str(config_path))
|
|
443
|
-
assert not any("'base_timeout'" in each_issue for each_issue in issues), (
|
|
444
|
-
f"A default sourcing a differently-named field must keep it live, got: {issues}"
|
|
445
|
-
)
|
|
446
|
-
assert not any("'poll_interval'" in each_issue for each_issue in issues), (
|
|
447
|
-
f"poll_interval is read both in the default and the consumer, got: {issues}"
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def test_self_referential_default_still_excludes_only_the_self_read(
|
|
452
|
-
neutral_root: Path,
|
|
453
|
-
) -> None:
|
|
454
|
-
"""The self-referential default read is still excluded after narrowing.
|
|
455
|
-
|
|
456
|
-
``sound_upload_timeout_ms: int = submission_timing.sound_upload_timeout_ms``
|
|
457
|
-
reads an attribute whose name equals the field being defined, so it is still
|
|
458
|
-
excluded; when that field is read by no module it stays flagged dead.
|
|
459
|
-
"""
|
|
460
|
-
config_body = (
|
|
461
|
-
"from dataclasses import dataclass\n"
|
|
462
|
-
"import submission_timing_module as submission_timing\n"
|
|
463
|
-
"\n"
|
|
464
|
-
"@dataclass(frozen=True)\n"
|
|
465
|
-
"class AppInfoConfig:\n"
|
|
466
|
-
" sound_upload_timeout_ms: int = submission_timing.sound_upload_timeout_ms\n"
|
|
467
|
-
)
|
|
468
|
-
workflow_directory = neutral_root / "workflow"
|
|
469
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
470
|
-
config_package.mkdir(parents=True)
|
|
471
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
472
|
-
config_path = config_package / "config.py"
|
|
473
|
-
config_path.write_text(config_body, encoding="utf-8")
|
|
474
|
-
(workflow_directory / "runner.py").write_text(
|
|
475
|
-
"def noop() -> None:\n pass\n", encoding="utf-8"
|
|
476
|
-
)
|
|
477
|
-
issues = _check(config_body, str(config_path))
|
|
478
|
-
assert any("'sound_upload_timeout_ms'" in each_issue for each_issue in issues), (
|
|
479
|
-
f"Self-referential default read is excluded, so the field stays flagged, got: {issues}"
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
def test_returns_empty_list_at_file_cap(
|
|
484
|
-
neutral_root: Path, monkeypatch: pytest.MonkeyPatch
|
|
485
|
-
) -> None:
|
|
486
|
-
monkeypatch.setattr("code_rules_dead_config_field.MAX_SCAN_ROOT_FILE_COUNT", 0)
|
|
487
|
-
config_path = _build_config_package(
|
|
488
|
-
neutral_root / "workflow",
|
|
489
|
-
THEME_UPDATE_CONFIG_BODY,
|
|
490
|
-
"def noop() -> None:\n pass\n",
|
|
491
|
-
)
|
|
492
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
493
|
-
assert issues == [], f"File cap hit must return [] (cannot prove dead), got: {issues}"
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
def test_returns_empty_list_on_syntax_error(neutral_root: Path) -> None:
|
|
497
|
-
workflow_directory = neutral_root / "workflow"
|
|
498
|
-
workflow_directory.mkdir(parents=True)
|
|
499
|
-
broken_path = workflow_directory / "config.py"
|
|
500
|
-
broken_source = "@dataclass\nclass BrokenConfig(\n"
|
|
501
|
-
broken_path.write_text(broken_source, encoding="utf-8")
|
|
502
|
-
issues = _check(broken_source, str(broken_path))
|
|
503
|
-
assert issues == [], f"SyntaxError must return [], got: {issues}"
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def test_is_skipped_on_test_file_destination() -> None:
|
|
507
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, TEST_FILE_PATH)
|
|
508
|
-
assert issues == [], f"Test file destinations are exempt, got: {issues}"
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
def test_is_skipped_on_migration_file_destination() -> None:
|
|
512
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, MIGRATION_FILE_PATH)
|
|
513
|
-
assert issues == [], f"Migration file destinations are exempt, got: {issues}"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
def test_real_world_shape_theme_update_config_with_dead_debug_port(neutral_root: Path) -> None:
|
|
517
|
-
workflow_directory = neutral_root / "workflow"
|
|
518
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
519
|
-
config_package.mkdir(parents=True)
|
|
520
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
521
|
-
config_path = config_package / "config.py"
|
|
522
|
-
config_path.write_text(THEME_UPDATE_CONFIG_BODY, encoding="utf-8")
|
|
523
|
-
orchestrator_body = (
|
|
524
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
525
|
-
"\n"
|
|
526
|
-
"def build_session(configuration: ThemeUpdateConfig) -> None:\n"
|
|
527
|
-
" url = configuration.portal_url\n"
|
|
528
|
-
" seconds = configuration.timeout_seconds\n"
|
|
529
|
-
" print(url, seconds)\n"
|
|
530
|
-
)
|
|
531
|
-
(workflow_directory / "orchestrator.py").write_text(orchestrator_body, encoding="utf-8")
|
|
532
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
533
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
534
|
-
f"debug_port field unused in production must be flagged, got: {issues}"
|
|
535
|
-
)
|
|
536
|
-
assert not any(
|
|
537
|
-
"'portal_url'" in each_issue or "'timeout_seconds'" in each_issue for each_issue in issues
|
|
538
|
-
), f"Fields read in orchestrator must not be flagged, got: {issues}"
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
def test_does_not_flag_field_used_only_via_augmented_assignment(neutral_root: Path) -> None:
|
|
542
|
-
consumer_body = (
|
|
543
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
544
|
-
"\n"
|
|
545
|
-
"def run(configuration: ThemeUpdateConfig) -> None:\n"
|
|
546
|
-
" print(configuration.portal_url)\n"
|
|
547
|
-
" print(configuration.timeout_seconds)\n"
|
|
548
|
-
" configuration.debug_port += 1\n"
|
|
549
|
-
)
|
|
550
|
-
config_path = _build_config_package(
|
|
551
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
552
|
-
)
|
|
553
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
554
|
-
assert not any("'debug_port'" in each_issue for each_issue in issues), (
|
|
555
|
-
f"Augmented assignment reads debug_port before writing, so it is not dead, got: {issues}"
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def test_flags_field_read_only_via_whole_instance_comparison(neutral_root: Path) -> None:
|
|
560
|
-
"""A field read ONLY via whole-instance comparison IS flagged (accepted limitation).
|
|
561
|
-
|
|
562
|
-
Unlike the per-file dead-dataclass-field check, this cross-module check does
|
|
563
|
-
not suppress on a dataclass-dunder whole-instance read: instance comparison
|
|
564
|
-
(``left == right``) is not bound to a config instance, and tree-wide one
|
|
565
|
-
incidental ``==`` anywhere would disable the check on any realistic package.
|
|
566
|
-
A ``*Config`` field whose only production read is this whole-instance
|
|
567
|
-
comparison, and that is never read directly, is therefore flagged — a
|
|
568
|
-
documented, rare limitation.
|
|
569
|
-
"""
|
|
570
|
-
consumer_body = (
|
|
571
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
572
|
-
"\n"
|
|
573
|
-
"def is_same(left: ThemeUpdateConfig, right: ThemeUpdateConfig) -> bool:\n"
|
|
574
|
-
" return left == right\n"
|
|
575
|
-
)
|
|
576
|
-
config_path = _build_config_package(
|
|
577
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
578
|
-
)
|
|
579
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
580
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
581
|
-
f"Field read only via whole-instance comparison is flagged, got: {issues}"
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def test_flags_field_read_only_via_whole_instance_stringification(neutral_root: Path) -> None:
|
|
586
|
-
"""A field read ONLY via whole-instance stringification IS flagged (accepted limitation).
|
|
587
|
-
|
|
588
|
-
The cross-module check does not suppress on a formatted-string conversion of
|
|
589
|
-
a whole instance (``f'{configuration}'``): the f-string is not bound to a
|
|
590
|
-
config instance, and tree-wide one incidental f-string anywhere would disable
|
|
591
|
-
the check on any realistic package. A ``*Config`` field whose only production
|
|
592
|
-
read is this whole-instance stringification, and that is never read directly,
|
|
593
|
-
is therefore flagged — a documented, rare limitation.
|
|
594
|
-
"""
|
|
595
|
-
consumer_body = (
|
|
596
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
597
|
-
"\n"
|
|
598
|
-
"def describe(configuration: ThemeUpdateConfig) -> str:\n"
|
|
599
|
-
" return f'{configuration}'\n"
|
|
600
|
-
)
|
|
601
|
-
config_path = _build_config_package(
|
|
602
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
603
|
-
)
|
|
604
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
605
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
606
|
-
f"Field read only via whole-instance stringification is flagged, got: {issues}"
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
def test_unrelated_string_method_does_not_suppress_so_dead_field_flagged(
|
|
611
|
-
neutral_root: Path,
|
|
612
|
-
) -> None:
|
|
613
|
-
"""An unrelated ``.replace(...)`` on a string must not suppress the tree.
|
|
614
|
-
|
|
615
|
-
``"some text".replace("a", "b")`` is a string method, not a ``dataclasses``-
|
|
616
|
-
qualified reflective consumer, so it does not make the check treat the tree
|
|
617
|
-
as a whole-instance read. A genuinely dead ``debug_port`` is still flagged.
|
|
618
|
-
"""
|
|
619
|
-
consumer_body = (
|
|
620
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
621
|
-
"\n"
|
|
622
|
-
"def run(configuration: ThemeUpdateConfig) -> str:\n"
|
|
623
|
-
" print(configuration.portal_url)\n"
|
|
624
|
-
" print(configuration.timeout_seconds)\n"
|
|
625
|
-
" return 'some text'.replace('a', 'b')\n"
|
|
626
|
-
)
|
|
627
|
-
config_path = _build_config_package(
|
|
628
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
629
|
-
)
|
|
630
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
631
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
632
|
-
f"Unrelated string .replace must not suppress, so dead debug_port is flagged, got: {issues}"
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
def test_dataclasses_qualified_replace_call_suppresses_so_no_field_flagged(
|
|
637
|
-
neutral_root: Path,
|
|
638
|
-
) -> None:
|
|
639
|
-
"""A ``dataclasses.replace(cfg, ...)`` call suppresses the whole tree.
|
|
640
|
-
|
|
641
|
-
A genuine ``dataclasses``-qualified reflective consumer reads every field at
|
|
642
|
-
once, so the check is suppressed for the whole tree and no field is flagged.
|
|
643
|
-
"""
|
|
644
|
-
consumer_body = (
|
|
645
|
-
"import dataclasses\n"
|
|
646
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
647
|
-
"\n"
|
|
648
|
-
"def repoint(configuration: ThemeUpdateConfig) -> ThemeUpdateConfig:\n"
|
|
649
|
-
" return dataclasses.replace(configuration, debug_port=9999)\n"
|
|
650
|
-
)
|
|
651
|
-
config_path = _build_config_package(
|
|
652
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
653
|
-
)
|
|
654
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
655
|
-
assert issues == [], (
|
|
656
|
-
f"dataclasses.replace reads every field at once, so none may be flagged, got: {issues}"
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
def test_aliased_replace_read_keeps_field_live_despite_constructor_keyword(
|
|
661
|
-
neutral_root: Path,
|
|
662
|
-
) -> None:
|
|
663
|
-
"""An aliased ``replace`` keyword read survives a same-module constructor keyword.
|
|
664
|
-
|
|
665
|
-
A module constructs ``ThemeUpdateConfig(debug_port=1)`` (a write) and also reads
|
|
666
|
-
the field through an aliased ``dataclasses.replace`` —
|
|
667
|
-
``from dataclasses import replace as rep; rep(cfg, debug_port=2)``. The alias is
|
|
668
|
-
not the bare ``replace`` name, so the whole-instance reflective suppression does
|
|
669
|
-
not fire and the ``rep`` keyword stays a genuine field read. The constructor
|
|
670
|
-
keyword exclusion is scoped per-call, so the constructor's ``debug_port`` keyword
|
|
671
|
-
does not strip the ``rep`` keyword read and the field stays live.
|
|
672
|
-
"""
|
|
673
|
-
consumer_body = (
|
|
674
|
-
"from dataclasses import replace as rep\n"
|
|
675
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
676
|
-
"\n"
|
|
677
|
-
"def build_then_repoint() -> ThemeUpdateConfig:\n"
|
|
678
|
-
" configuration = ThemeUpdateConfig(portal_url='x', debug_port=1, timeout_seconds=99)\n"
|
|
679
|
-
" return rep(configuration, debug_port=2)\n"
|
|
680
|
-
)
|
|
681
|
-
config_path = _build_config_package(
|
|
682
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
683
|
-
)
|
|
684
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
685
|
-
assert not any("'debug_port'" in each_issue for each_issue in issues), (
|
|
686
|
-
f"Aliased replace keyword read must keep debug_port live despite a"
|
|
687
|
-
f" same-module constructor keyword, got: {issues}"
|
|
688
|
-
)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
def test_factory_function_ending_in_config_keeps_field_live(
|
|
692
|
-
neutral_root: Path,
|
|
693
|
-
) -> None:
|
|
694
|
-
"""A keyword to a factory FUNCTION ending in ``Config`` is a read, not a write.
|
|
695
|
-
|
|
696
|
-
``getThemeConfig(debug_port=1)`` calls a factory function, not a ``*Config``
|
|
697
|
-
dataclass constructor. The keyword passes a value into the function, so it
|
|
698
|
-
counts as a field read. The constructor-keyword exclusion fires only for a
|
|
699
|
-
callee that names a known ``*Config`` dataclass defined under the scan root, so
|
|
700
|
-
a factory function whose name merely ends in ``Config`` does not strip the
|
|
701
|
-
field.
|
|
702
|
-
"""
|
|
703
|
-
consumer_body = (
|
|
704
|
-
"from os_update_workflow.config import ThemeUpdateConfig\n"
|
|
705
|
-
"\n"
|
|
706
|
-
"def getThemeConfig(portal_url: str, debug_port: int) -> ThemeUpdateConfig:\n"
|
|
707
|
-
" return ThemeUpdateConfig(\n"
|
|
708
|
-
" portal_url=portal_url, debug_port=debug_port, timeout_seconds=30\n"
|
|
709
|
-
" )\n"
|
|
710
|
-
"\n"
|
|
711
|
-
"def run() -> ThemeUpdateConfig:\n"
|
|
712
|
-
" print('x')\n"
|
|
713
|
-
" return getThemeConfig(portal_url='x', debug_port=1)\n"
|
|
714
|
-
)
|
|
715
|
-
config_path = _build_config_package(
|
|
716
|
-
neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
|
|
717
|
-
)
|
|
718
|
-
issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
719
|
-
assert not any("'debug_port'" in each_issue for each_issue in issues), (
|
|
720
|
-
f"A keyword passed to a factory function ending in Config must keep the"
|
|
721
|
-
f" field live, got: {issues}"
|
|
722
|
-
)
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
def test_dead_config_field_module_has_no_collection_parameter_naming_violation() -> None:
|
|
726
|
-
"""The hook module's own source must pass the collection-parameter naming check.
|
|
727
|
-
|
|
728
|
-
The cross-module dead-config-field check passes a ``set[str]`` of config class
|
|
729
|
-
names through several helpers; every such collection parameter must carry the
|
|
730
|
-
``all_`` prefix (CODE_RULES §5). Run the real collection-prefix check over this
|
|
731
|
-
module's on-disk source so a regression that drops the prefix fails here.
|
|
732
|
-
"""
|
|
733
|
-
collection_naming_path = (
|
|
734
|
-
Path(__file__).resolve().parent / "code_rules_naming_collection.py"
|
|
735
|
-
)
|
|
736
|
-
naming_specification = importlib.util.spec_from_file_location(
|
|
737
|
-
"code_rules_naming_collection", collection_naming_path
|
|
738
|
-
)
|
|
739
|
-
assert naming_specification is not None and naming_specification.loader is not None
|
|
740
|
-
code_rules_naming_collection = importlib.util.module_from_spec(naming_specification)
|
|
741
|
-
naming_specification.loader.exec_module(code_rules_naming_collection)
|
|
742
|
-
module_path = Path(__file__).resolve().parent / "code_rules_dead_config_field.py"
|
|
743
|
-
module_source = module_path.read_text(encoding="utf-8")
|
|
744
|
-
issues = code_rules_naming_collection.check_collection_prefix(
|
|
745
|
-
module_source, str(module_path)
|
|
746
|
-
)
|
|
747
|
-
assert not any("Collection parameter" in each_issue for each_issue in issues), (
|
|
748
|
-
f"dead-config-field module has a collection-parameter naming violation: {issues}"
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
SELECTORS_DATACLASS_BODY = (
|
|
753
|
-
"from dataclasses import dataclass\n"
|
|
754
|
-
"\n"
|
|
755
|
-
"@dataclass(frozen=True)\n"
|
|
756
|
-
"class BinarySelectors:\n"
|
|
757
|
-
" show_more_button_active: str = \"a.show_list_btn.active\"\n"
|
|
758
|
-
" show_more_row_visible: str = \"tr.show_list_tr:not(.ng-hide)\"\n"
|
|
759
|
-
"\n"
|
|
760
|
-
"binary_selectors: BinarySelectors = BinarySelectors()\n"
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def test_flags_selectors_dataclass_field_read_by_no_production_module(
|
|
765
|
-
neutral_root: Path,
|
|
766
|
-
) -> None:
|
|
767
|
-
"""A ``*Selectors`` @dataclass field read by no production module is flagged.
|
|
768
|
-
|
|
769
|
-
A selectors dataclass is a config-like export surface: defined once, bound to
|
|
770
|
-
a module-level singleton (``binary_selectors = BinarySelectors()``), and read
|
|
771
|
-
across files. The cross-module dead-field scan treats it the same as a
|
|
772
|
-
``*Config`` dataclass, so a selector field no production module reads —
|
|
773
|
-
``show_more_row_visible`` here — is flagged, while a selector a consumer reads
|
|
774
|
-
(``show_more_button_active``) is not.
|
|
775
|
-
"""
|
|
776
|
-
consumer_body = (
|
|
777
|
-
"from selectors_module import binary_selectors\n"
|
|
778
|
-
"\n"
|
|
779
|
-
"def expand() -> str:\n"
|
|
780
|
-
" return binary_selectors.show_more_button_active\n"
|
|
781
|
-
)
|
|
782
|
-
workflow_directory = neutral_root / "workflow"
|
|
783
|
-
selectors_package = workflow_directory / "selectors"
|
|
784
|
-
selectors_package.mkdir(parents=True)
|
|
785
|
-
(selectors_package / "__init__.py").write_text("", encoding="utf-8")
|
|
786
|
-
selectors_path = selectors_package / "selectors_module.py"
|
|
787
|
-
selectors_path.write_text(SELECTORS_DATACLASS_BODY, encoding="utf-8")
|
|
788
|
-
(workflow_directory / "processor.py").write_text(consumer_body, encoding="utf-8")
|
|
789
|
-
issues = _check(SELECTORS_DATACLASS_BODY, str(selectors_path))
|
|
790
|
-
assert any("'show_more_row_visible'" in each_issue for each_issue in issues), (
|
|
791
|
-
f"Selector field read by no production module must be flagged, got: {issues}"
|
|
792
|
-
)
|
|
793
|
-
assert not any(
|
|
794
|
-
"'show_more_button_active'" in each_issue for each_issue in issues
|
|
795
|
-
), f"Selector field read in the consumer must not be flagged, got: {issues}"
|
|
796
|
-
selector_issue = next(
|
|
797
|
-
each_issue for each_issue in issues if "'show_more_row_visible'" in each_issue
|
|
798
|
-
)
|
|
799
|
-
assert "config dataclass field" not in selector_issue, (
|
|
800
|
-
f"Flagged selectors field must not be mislabelled a config dataclass field, got: {selector_issue}"
|
|
801
|
-
)
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
def test_does_not_flag_selectors_field_read_in_sibling_module(
|
|
805
|
-
neutral_root: Path,
|
|
806
|
-
) -> None:
|
|
807
|
-
"""A ``*Selectors`` field read through the singleton in a sibling module is live.
|
|
808
|
-
|
|
809
|
-
When every selector field is read by a production consumer, none is flagged.
|
|
810
|
-
"""
|
|
811
|
-
consumer_body = (
|
|
812
|
-
"from selectors_module import binary_selectors\n"
|
|
813
|
-
"\n"
|
|
814
|
-
"def expand() -> tuple[str, str]:\n"
|
|
815
|
-
" return (\n"
|
|
816
|
-
" binary_selectors.show_more_button_active,\n"
|
|
817
|
-
" binary_selectors.show_more_row_visible,\n"
|
|
818
|
-
" )\n"
|
|
819
|
-
)
|
|
820
|
-
workflow_directory = neutral_root / "workflow"
|
|
821
|
-
selectors_package = workflow_directory / "selectors"
|
|
822
|
-
selectors_package.mkdir(parents=True)
|
|
823
|
-
(selectors_package / "__init__.py").write_text("", encoding="utf-8")
|
|
824
|
-
selectors_path = selectors_package / "selectors_module.py"
|
|
825
|
-
selectors_path.write_text(SELECTORS_DATACLASS_BODY, encoding="utf-8")
|
|
826
|
-
(workflow_directory / "processor.py").write_text(consumer_body, encoding="utf-8")
|
|
827
|
-
issues = _check(SELECTORS_DATACLASS_BODY, str(selectors_path))
|
|
828
|
-
assert issues == [], (
|
|
829
|
-
f"All selector fields are read in the consumer, none must be flagged, got: {issues}"
|
|
830
|
-
)
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
def test_validate_content_dispatch_runs_dead_config_field_check(neutral_root: Path) -> None:
|
|
834
|
-
workflow_directory = neutral_root / "workflow"
|
|
835
|
-
config_package = workflow_directory / "os_update_workflow"
|
|
836
|
-
config_package.mkdir(parents=True)
|
|
837
|
-
(config_package / "__init__.py").write_text("", encoding="utf-8")
|
|
838
|
-
config_path = config_package / "config.py"
|
|
839
|
-
config_path.write_text(THEME_UPDATE_CONFIG_BODY, encoding="utf-8")
|
|
840
|
-
(workflow_directory / "runner.py").write_text(
|
|
841
|
-
"def noop() -> None:\n pass\n", encoding="utf-8"
|
|
842
|
-
)
|
|
843
|
-
issues = code_rules_enforcer.validate_content(THEME_UPDATE_CONFIG_BODY, str(config_path))
|
|
844
|
-
assert any("'debug_port'" in each_issue for each_issue in issues), (
|
|
845
|
-
f"Expected the enforcer dispatch to surface the dead config field, got: {issues}"
|
|
846
|
-
)
|