kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl
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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
<!-- dead-code - An agent that looks for dead code and create a markdown report -->
|
|
2
|
+
|
|
3
|
+
skill name: dead-code
|
|
4
|
+
|
|
5
|
+
purpose:
|
|
6
|
+
systematically identify, categorize, and report dead code across the codebase.
|
|
7
|
+
provides multi-phase analysis for unused functions, variables, imports, classes,
|
|
8
|
+
and unreachable code paths. generates comprehensive markdown reports with
|
|
9
|
+
actionable recommendations.
|
|
10
|
+
|
|
11
|
+
when to use:
|
|
12
|
+
[ ] codebase cleanup and maintenance
|
|
13
|
+
[ ] reducing technical debt
|
|
14
|
+
[ ] preparing for major refactoring
|
|
15
|
+
[ ] improving code coverage
|
|
16
|
+
[ ] before deploying to production
|
|
17
|
+
[ ] onboarding to new codebase
|
|
18
|
+
[ ] performance optimization
|
|
19
|
+
|
|
20
|
+
methodology:
|
|
21
|
+
|
|
22
|
+
phase 0: environment and context verification
|
|
23
|
+
understand project structure
|
|
24
|
+
identify programming languages
|
|
25
|
+
verify tool availability
|
|
26
|
+
establish scope boundaries
|
|
27
|
+
check git history for context
|
|
28
|
+
|
|
29
|
+
phase 1: static analysis discovery
|
|
30
|
+
unused imports detection
|
|
31
|
+
unused functions and methods
|
|
32
|
+
unused variables and constants
|
|
33
|
+
unused classes and modules
|
|
34
|
+
commented-out code blocks
|
|
35
|
+
|
|
36
|
+
phase 2: dynamic usage analysis
|
|
37
|
+
cross-reference function calls
|
|
38
|
+
analyze import dependencies
|
|
39
|
+
check test coverage gaps
|
|
40
|
+
identify unreachable code paths
|
|
41
|
+
verify configuration references
|
|
42
|
+
|
|
43
|
+
phase 3: categorization and prioritization
|
|
44
|
+
classify dead code by severity
|
|
45
|
+
assess removal risk and impact
|
|
46
|
+
identify safe-to-delete candidates
|
|
47
|
+
flag potentially dead-but-uncertain code
|
|
48
|
+
create action priorities
|
|
49
|
+
|
|
50
|
+
phase 4: markdown report generation
|
|
51
|
+
compile findings into structured report
|
|
52
|
+
include code snippets with line numbers
|
|
53
|
+
provide removal recommendations
|
|
54
|
+
document dependencies and side effects
|
|
55
|
+
create automated cleanup scripts
|
|
56
|
+
|
|
57
|
+
phase 5: verification and validation
|
|
58
|
+
cross-check findings with tests
|
|
59
|
+
validate safe deletions
|
|
60
|
+
document false positives
|
|
61
|
+
create pull request with changes
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
phase 0: environment and context verification
|
|
65
|
+
|
|
66
|
+
step 1: understand project structure
|
|
67
|
+
|
|
68
|
+
<terminal>pwd</terminal>
|
|
69
|
+
<terminal>ls -la</terminal>
|
|
70
|
+
<terminal>find . -type f -name "*.py" -o -name "*.js" -o -name "*.ts" | head -30</terminal>
|
|
71
|
+
|
|
72
|
+
identify primary languages:
|
|
73
|
+
<terminal>find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.java" -o -name "*.go" -o -name "*.rs" \) | wc -l</terminal>
|
|
74
|
+
|
|
75
|
+
check for build systems:
|
|
76
|
+
<terminal>ls -la | grep -E "(package.json|requirements.txt|Cargo.toml|go.mod|pom.xml|build.gradle)"</terminal>
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
step 2: identify codebase type
|
|
80
|
+
|
|
81
|
+
<terminal>if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then echo "python"; fi</terminal>
|
|
82
|
+
<terminal>if [ -f "package.json" ]; then echo "javascript/typescript"; fi</terminal>
|
|
83
|
+
<terminal>if [ -f "Cargo.toml" ]; then echo "rust"; fi</terminal>
|
|
84
|
+
<terminal>if [ -f "go.mod" ]; then echo "go"; fi</terminal>
|
|
85
|
+
|
|
86
|
+
detect frameworks:
|
|
87
|
+
<terminal>grep -r "from django" . --include="*.py" 2>/dev/null | head -1 && echo "django"</terminal>
|
|
88
|
+
<terminal>grep -r "import React" . --include="*.jsx" 2>/dev/null | head -1 && echo "react"</terminal>
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
step 3: verify tool availability
|
|
92
|
+
|
|
93
|
+
check for python dead code tools:
|
|
94
|
+
<terminal>which vulture || which pyflakes || which pycodestyle || echo "no python linter found"</terminal>
|
|
95
|
+
|
|
96
|
+
check for javascript tools:
|
|
97
|
+
<terminal>which eslint || echo "eslint not found"</terminal>
|
|
98
|
+
|
|
99
|
+
install tools if needed:
|
|
100
|
+
<terminal>pip install vulture autoflake 2>/dev/null || echo "pip install vulture autoflake"</terminal>
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
step 4: establish scope boundaries
|
|
104
|
+
|
|
105
|
+
define what to include/exclude:
|
|
106
|
+
<terminal>cat > /tmp/dead_code_scope.txt << 'EOF'
|
|
107
|
+
included:
|
|
108
|
+
- main source code directories (src/, core/, lib/, app/)
|
|
109
|
+
- application logic
|
|
110
|
+
- utility functions
|
|
111
|
+
|
|
112
|
+
excluded:
|
|
113
|
+
- test directories (tests/, test/, __tests__/)
|
|
114
|
+
- third-party dependencies (node_modules/, venv/, .venv/)
|
|
115
|
+
- build artifacts (dist/, build/, *.egg-info/)
|
|
116
|
+
- migration files (migrations/)
|
|
117
|
+
- auto-generated code
|
|
118
|
+
EOF
|
|
119
|
+
cat /tmp/dead_code_scope.txt</terminal>
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
step 5: check git history
|
|
123
|
+
|
|
124
|
+
identify recently modified files:
|
|
125
|
+
<terminal>git log --name-only --pretty=format: --since="3 months ago" | grep -v "^$" | sort -u | head -20</terminal>
|
|
126
|
+
|
|
127
|
+
find old untouched files (potential dead code):
|
|
128
|
+
<terminal>find . -name "*.py" -mtime +180 ! -path "./tests/*" ! -path "./venv/*" ! -path "./.venv/*" -type f | head -10</terminal>
|
|
129
|
+
|
|
130
|
+
check for large files with low activity:
|
|
131
|
+
<terminal>find . -name "*.py" -size +10k ! -path "./tests/*" -exec ls -lh {} \; | awk '{print $9, $5}'</terminal>
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
phase 1: static analysis discovery
|
|
135
|
+
|
|
136
|
+
python-specific analysis:
|
|
137
|
+
|
|
138
|
+
step 1: unused imports detection using vulture
|
|
139
|
+
|
|
140
|
+
<terminal>vulture . --min-confidence 60 --exclude "venv,.venv,tests,migrations,node_modules" 2>&1 | tee /tmp/vulture_report.txt</terminal>
|
|
141
|
+
|
|
142
|
+
<terminal>cat /tmp/vulture_report.txt | grep "unused import"</terminal>
|
|
143
|
+
|
|
144
|
+
alternative using autoflake:
|
|
145
|
+
<terminal>autoflake --remove-all-unused-imports --recursive --exclude .venv,venv,tests . 2>&1 | tee /tmp/autoflake_report.txt</terminal>
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
step 2: unused functions and classes
|
|
149
|
+
|
|
150
|
+
<terminal>vulture . --min-confidence 70 --exclude "venv,.venv,tests,migrations" --sort-by-size 2>&1 | grep -E "unused function|unused class|unused attribute"</terminal>
|
|
151
|
+
|
|
152
|
+
find defined but never called functions:
|
|
153
|
+
<terminal>grep -rn "^def " . --include="*.py" --exclude-dir=venv --exclude-dir=.venv --exclude-dir=tests | grep -v "__" | awk -F: '{print $1}' | sort -u | head -20</terminal>
|
|
154
|
+
|
|
155
|
+
for each candidate function, check if it's called:
|
|
156
|
+
<terminal>grep -r "function_name" . --include="*.py" --exclude-dir=tests | wc -l</terminal>
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
step 3: unused variables and constants
|
|
160
|
+
|
|
161
|
+
<terminal>vulture . --min-confidence 80 --exclude "venv,.venv,tests" | grep -E "unused variable|unused attribute"</terminal>
|
|
162
|
+
|
|
163
|
+
find unused global variables:
|
|
164
|
+
<terminal>grep -rn "^[A-Z_][A-Z0-9_]*\s*=" . --include="*.py" --exclude-dir=venv --exclude-dir=.venv --exclude-dir=tests | head -20</terminal>
|
|
165
|
+
|
|
166
|
+
verify usage:
|
|
167
|
+
<terminal>grep -r "VARIABLE_NAME" . --include="*.py" | wc -l</terminal>
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
step 4: commented-out code blocks
|
|
171
|
+
|
|
172
|
+
find multi-line commented code:
|
|
173
|
+
<terminal>grep -rn "^# def\|^# class\|^# import\|^# from" . --include="*.py" --exclude-dir=venv --exclude-dir=.venv --exclude-dir=tests | head -30</terminal>
|
|
174
|
+
|
|
175
|
+
find TODO/FIXME comments (potential unfinished code):
|
|
176
|
+
<terminal>grep -rn "TODO:\|FIXME:\|HACK:\|XXX:" . --include="*.py" --exclude-dir=venv --exclude-dir=.venv --exclude-dir=tests | head -20</terminal>
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
javascript/typescript-specific analysis:
|
|
180
|
+
|
|
181
|
+
step 1: unused imports using eslint
|
|
182
|
+
|
|
183
|
+
<terminal>eslint . --ext .js,.jsx,.ts,.tsx --rule "no-unused-vars: error" --ignore-pattern "node_modules/*" 2>&1 | tee /tmp/eslint_report.txt</terminal>
|
|
184
|
+
|
|
185
|
+
<terminal>cat /tmp/eslint_report.txt | grep "no-unused-vars"</terminal>
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
step 2: unused functions and variables
|
|
189
|
+
|
|
190
|
+
<terminal>eslint . --ext .js,.jsx,.ts,.tsx --rule "no-unused-vars: [2, { \"vars\": \"all\", \"args\": \"after-used\", \"ignoreRestSiblings\": false }]" --ignore-pattern "node_modules/*"</terminal>
|
|
191
|
+
|
|
192
|
+
find exported but unused functions:
|
|
193
|
+
<terminal>grep -rn "export.*function\|export const" . --include="*.js" --include="*.ts" --exclude-dir=node_modules | head -20</terminal>
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
step 3: find dead code in common patterns
|
|
197
|
+
|
|
198
|
+
unused event handlers:
|
|
199
|
+
<terminal>grep -rn "on[A-Z].*=\|onClick\|onSubmit" . --include="*.jsx" --include="*.tsx" | head -20</terminal>
|
|
200
|
+
|
|
201
|
+
duplicate function definitions:
|
|
202
|
+
<terminal>grep -rn "^function\|^const.*=.*=>" . --include="*.js" --include="*.ts" | cut -d: -f3 | sort | uniq -d | head -10</terminal>
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
phase 2: dynamic usage analysis
|
|
206
|
+
|
|
207
|
+
step 1: cross-reference function calls
|
|
208
|
+
|
|
209
|
+
create function call map for python:
|
|
210
|
+
<terminal>cat > /tmp/analyze_calls.py << 'EOF'
|
|
211
|
+
import ast
|
|
212
|
+
import sys
|
|
213
|
+
from pathlib import Path
|
|
214
|
+
from collections import defaultdict
|
|
215
|
+
|
|
216
|
+
def analyze_file(filepath):
|
|
217
|
+
with open(filepath, 'r') as f:
|
|
218
|
+
try:
|
|
219
|
+
tree = ast.parse(f.read(), filepath)
|
|
220
|
+
except:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
functions = []
|
|
224
|
+
calls = []
|
|
225
|
+
|
|
226
|
+
for node in ast.walk(tree):
|
|
227
|
+
if isinstance(node, ast.FunctionDef):
|
|
228
|
+
functions.append(node.name)
|
|
229
|
+
elif isinstance(node, ast.Call):
|
|
230
|
+
if isinstance(node.func, ast.Name):
|
|
231
|
+
calls.append(node.func.id)
|
|
232
|
+
|
|
233
|
+
return filepath, functions, calls
|
|
234
|
+
|
|
235
|
+
def main():
|
|
236
|
+
defined = defaultdict(list)
|
|
237
|
+
called = defaultdict(int)
|
|
238
|
+
|
|
239
|
+
for py_file in Path('.').rglob('*.py'):
|
|
240
|
+
if 'venv' in str(py_file) or '.venv' in str(py_file) or 'tests' in str(py_file):
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
result = analyze_file(py_file)
|
|
244
|
+
if result:
|
|
245
|
+
filepath, functions, calls = result
|
|
246
|
+
for func in functions:
|
|
247
|
+
defined[func].append(filepath)
|
|
248
|
+
for call in calls:
|
|
249
|
+
called[call] += 1
|
|
250
|
+
|
|
251
|
+
print("=== POTENTIALLY UNUSED FUNCTIONS ===")
|
|
252
|
+
for func, files in sorted(defined.items()):
|
|
253
|
+
if func.startswith('_'):
|
|
254
|
+
continue
|
|
255
|
+
if called.get(func, 0) == 0 and len(files) == 1:
|
|
256
|
+
print(f"{func}: defined in {files[0]}")
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
main()
|
|
260
|
+
EOF
|
|
261
|
+
python /tmp/analyze_calls.py</terminal>
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
step 2: analyze import dependencies
|
|
265
|
+
|
|
266
|
+
find imported but unused modules:
|
|
267
|
+
<terminal>grep -rn "^import \|^from " . --include="*.py" --exclude-dir=venv --exclude-dir=.venv --exclude-dir=tests | sed 's/.*import //' | sed 's/ as .*//' | sort -u | head -30</terminal>
|
|
268
|
+
|
|
269
|
+
check for circular imports:
|
|
270
|
+
<terminal>python -c "
|
|
271
|
+
import ast
|
|
272
|
+
import sys
|
|
273
|
+
from pathlib import Path
|
|
274
|
+
|
|
275
|
+
imports = {}
|
|
276
|
+
for py_file in Path('.').rglob('*.py'):
|
|
277
|
+
if 'venv' in str(py_file) or '.venv' in str(py_file):
|
|
278
|
+
continue
|
|
279
|
+
try:
|
|
280
|
+
with open(py_file) as f:
|
|
281
|
+
tree = ast.parse(f.read())
|
|
282
|
+
for node in ast.walk(tree):
|
|
283
|
+
if isinstance(node, ast.Import):
|
|
284
|
+
for alias in node.names:
|
|
285
|
+
imports.setdefault(str(py_file), set()).add(alias.name.split('.')[0])
|
|
286
|
+
elif isinstance(node, ast.ImportFrom):
|
|
287
|
+
if node.module:
|
|
288
|
+
imports.setdefault(str(py_file), set()).add(node.module.split('.')[0])
|
|
289
|
+
except:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# Check for cycles
|
|
293
|
+
for file, deps in imports.items():
|
|
294
|
+
for dep in deps:
|
|
295
|
+
dep_file = None
|
|
296
|
+
for f in imports.keys():
|
|
297
|
+
if dep in f.replace('/', '.').replace('.py', ''):
|
|
298
|
+
dep_file = f
|
|
299
|
+
break
|
|
300
|
+
if dep_file and file in imports.get(dep_file, set()):
|
|
301
|
+
print(f'Circular: {file} <-> {dep_file}')
|
|
302
|
+
"</terminal>
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
step 3: check test coverage gaps
|
|
306
|
+
|
|
307
|
+
if pytest coverage is available:
|
|
308
|
+
<terminal>python -m pytest --cov=. --cov-report=term-missing 2>/dev/null | grep -E "TOTAL|.*\.py.*\d+%")</terminal>
|
|
309
|
+
|
|
310
|
+
find files with zero or low coverage:
|
|
311
|
+
<terminal>python -m pytest --cov=. --cov-report=json 2>/dev/null && cat coverage.json | grep -A 3 '"files"' | grep -E "name|covered_percent" | paste - - | awk '$2 < 50 {print}' | head -10</terminal>
|
|
312
|
+
|
|
313
|
+
identify untested functions:
|
|
314
|
+
<terminal>grep -rn "^def " . --include="*.py" --exclude-dir=tests | while read line; do
|
|
315
|
+
file=$(echo "$line" | cut -d: -f1)
|
|
316
|
+
func=$(echo "$line" | cut -d: -f2 | sed 's/def //')
|
|
317
|
+
if ! grep -r "$func" tests/ --include="*.py" >/dev/null 2>&1; then
|
|
318
|
+
echo "$file:$func"
|
|
319
|
+
fi
|
|
320
|
+
done | head -20</terminal>
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
step 4: identify unreachable code paths
|
|
324
|
+
|
|
325
|
+
find code after return statements:
|
|
326
|
+
<terminal>grep -A 3 "return " . --include="*.py" --exclude-dir=tests | grep -E "^\s+[a-zA-Z_]" | grep -v "^--$" | head -20</terminal>
|
|
327
|
+
|
|
328
|
+
find impossible conditions:
|
|
329
|
+
<terminal>grep -rn "if True:\|if False:\|if 1:\|if 0:" . --include="*.py" --exclude-dir=tests | head -10</terminal>
|
|
330
|
+
|
|
331
|
+
find empty exception handlers:
|
|
332
|
+
<terminal>grep -A 2 "except.*:" . --include="*.py" --exclude-dir=tests | grep -E "^\s+pass\s*$" | head -10</terminal>
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
step 5: verify configuration references
|
|
336
|
+
|
|
337
|
+
find config keys that might be unused:
|
|
338
|
+
<terminal>grep -rn "os.getenv\|os.environ" . --include="*.py" --exclude-dir=tests | sed "s/.*os.getenv(['\"]//" | sed "s/['\"].*//" | sort -u | head -20</terminal>
|
|
339
|
+
|
|
340
|
+
check .env.example vs actual usage:
|
|
341
|
+
<terminal>if [ -f ".env.example" ]; then
|
|
342
|
+
while IFS='=' read -r key value; do
|
|
343
|
+
if [ ! -z "$key" ] && [[ ! "$key" == "#"* ]]; then
|
|
344
|
+
if ! grep -r "$key" . --include="*.py" --exclude-dir=tests >/dev/null 2>&1; then
|
|
345
|
+
echo "Possibly unused: $key"
|
|
346
|
+
fi
|
|
347
|
+
fi
|
|
348
|
+
done < .env.example | head -10
|
|
349
|
+
fi</terminal>
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
phase 3: categorization and prioritization
|
|
353
|
+
|
|
354
|
+
step 1: classify dead code by severity
|
|
355
|
+
|
|
356
|
+
create severity classification:
|
|
357
|
+
|
|
358
|
+
[critical] - definitely dead, high confidence, safe to remove
|
|
359
|
+
- unused imports
|
|
360
|
+
- unused private functions (prefixed with _)
|
|
361
|
+
- commented-out code blocks
|
|
362
|
+
- unreachable code paths
|
|
363
|
+
|
|
364
|
+
[high] - likely dead, moderate-high confidence
|
|
365
|
+
- unused public functions not in tests
|
|
366
|
+
- unused classes with no references
|
|
367
|
+
- unused constants/variables
|
|
368
|
+
- old migration files
|
|
369
|
+
|
|
370
|
+
[medium] - potentially dead, needs verification
|
|
371
|
+
- functions only called in tests (might be test-only utilities)
|
|
372
|
+
- event handlers not bound
|
|
373
|
+
- configuration keys not found in code
|
|
374
|
+
|
|
375
|
+
[low] - possibly dead, requires manual review
|
|
376
|
+
- functions with dynamic calls (getattr, __getattr__)
|
|
377
|
+
- plugin interfaces
|
|
378
|
+
- api endpoints
|
|
379
|
+
- database models
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
step 2: assess removal risk and impact
|
|
383
|
+
|
|
384
|
+
check git history for recent changes:
|
|
385
|
+
<terminal>git log --oneline --all -- "dead_code_candidate.py" | head -5</terminal>
|
|
386
|
+
|
|
387
|
+
check if file is in recent commits:
|
|
388
|
+
<terminal>git log --since="6 months ago" --name-only --pretty=format: -- "dead_code_candidate.py" | wc -l</terminal>
|
|
389
|
+
|
|
390
|
+
identify files in main entry points:
|
|
391
|
+
<terminal>grep -r "from.*import\|import" main.py setup.py pyproject.toml 2>/dev/null | grep -i "dead_code_candidate"</terminal>
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
step 3: identify safe-to-delete candidates
|
|
395
|
+
|
|
396
|
+
criteria for safe deletion:
|
|
397
|
+
[x] no references in current codebase
|
|
398
|
+
[x] not in git history in last 6 months
|
|
399
|
+
[x] not in requirements or dependency declarations
|
|
400
|
+
[x] no tests reference it
|
|
401
|
+
[x] not an entry point or main module
|
|
402
|
+
[x] not a public API
|
|
403
|
+
|
|
404
|
+
generate safe deletion list:
|
|
405
|
+
<terminal>cat > /tmp/safe_delete_candidates.txt << 'EOF'
|
|
406
|
+
# Safe to delete - verified criteria met
|
|
407
|
+
EOF
|
|
408
|
+
# Add verified candidates to this file
|
|
409
|
+
cat /tmp/safe_delete_candidates.txt</terminal>
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
step 4: flag potentially dead-but-uncertain code
|
|
413
|
+
|
|
414
|
+
code that requires manual review:
|
|
415
|
+
<terminal>cat > /tmp/requires_review.txt << 'EOF'
|
|
416
|
+
# Requires manual review - uncertain status
|
|
417
|
+
EOF
|
|
418
|
+
|
|
419
|
+
examples:
|
|
420
|
+
- functions called via string (getattr(obj, method_name))
|
|
421
|
+
- classes loaded dynamically (importlib.import_module)
|
|
422
|
+
- api routes with swagger docs
|
|
423
|
+
- database models with migrations
|
|
424
|
+
- configuration with default values
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
step 5: create action priorities
|
|
428
|
+
|
|
429
|
+
priority 1 (immediate action):
|
|
430
|
+
- unused imports (safe, easy wins)
|
|
431
|
+
- commented-out code blocks (cleanup)
|
|
432
|
+
- unreachable code after returns
|
|
433
|
+
|
|
434
|
+
priority 2 (next sprint):
|
|
435
|
+
- unused private functions
|
|
436
|
+
- unused utility classes
|
|
437
|
+
- dead configuration keys
|
|
438
|
+
|
|
439
|
+
priority 3 (technical debt backlog):
|
|
440
|
+
- old migration files
|
|
441
|
+
- deprecated functions (marked with @deprecated)
|
|
442
|
+
- experimental/feature flags
|
|
443
|
+
|
|
444
|
+
priority 4 (investigate first):
|
|
445
|
+
- potentially unused public APIs
|
|
446
|
+
- database models with uncertain usage
|
|
447
|
+
- plugin interfaces
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
phase 4: markdown report generation
|
|
451
|
+
|
|
452
|
+
create comprehensive markdown report:
|
|
453
|
+
|
|
454
|
+
<create><file>DEAD_CODE_REPORT.md</file><content># Dead Code Analysis Report
|
|
455
|
+
|
|
456
|
+
generated: $(date '+%Y-%m-%d %H:%M:%S')
|
|
457
|
+
project: $(basename $(pwd))
|
|
458
|
+
analysis scope: main source code (excluding tests, dependencies)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
## executive summary
|
|
462
|
+
|
|
463
|
+
- total files analyzed: X
|
|
464
|
+
- total lines of code: Y
|
|
465
|
+
- potential dead code items found: Z
|
|
466
|
+
- safe to delete: A
|
|
467
|
+
- requires review: B
|
|
468
|
+
- estimated cleanup effort: C hours
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
## findings overview
|
|
472
|
+
|
|
473
|
+
### by category
|
|
474
|
+
|
|
475
|
+
| category | count | severity | safe to delete |
|
|
476
|
+
|----------|-------|----------|----------------|
|
|
477
|
+
| unused imports | | critical | yes |
|
|
478
|
+
| unused functions | | high | maybe |
|
|
479
|
+
| unused classes | | high | maybe |
|
|
480
|
+
| unused variables | | high | yes |
|
|
481
|
+
| commented code | | critical | yes |
|
|
482
|
+
| unreachable code | | critical | yes |
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
### by severity
|
|
486
|
+
|
|
487
|
+
- [critical] X items - definitely dead, safe to remove
|
|
488
|
+
- [high] Y items - likely dead, moderate confidence
|
|
489
|
+
- [medium] Z items - potentially dead, needs verification
|
|
490
|
+
- [low] W items - uncertain, requires manual review
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
## detailed findings
|
|
494
|
+
|
|
495
|
+
### 1. unused imports
|
|
496
|
+
|
|
497
|
+
confidence: 100%
|
|
498
|
+
action: safe to remove
|
|
499
|
+
|
|
500
|
+
#### high-priority files
|
|
501
|
+
|
|
502
|
+
**file: `src/module.py`**
|
|
503
|
+
- line 5: `import unused_module` - not referenced
|
|
504
|
+
- line 12: `from another import dead_function` - not called
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
# before
|
|
508
|
+
import unused_module
|
|
509
|
+
from another import dead_function
|
|
510
|
+
|
|
511
|
+
def main():
|
|
512
|
+
pass
|
|
513
|
+
|
|
514
|
+
# after
|
|
515
|
+
def main():
|
|
516
|
+
pass
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
### 2. unused functions
|
|
522
|
+
|
|
523
|
+
confidence: 70-90%
|
|
524
|
+
action: review dependencies, then remove
|
|
525
|
+
|
|
526
|
+
#### `src/utils.py`
|
|
527
|
+
|
|
528
|
+
**function: `old_helper` (line 45)**
|
|
529
|
+
- defined but never called
|
|
530
|
+
- no references in codebase
|
|
531
|
+
- not in test files
|
|
532
|
+
- last modified: 2023-06-15
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
def old_helper(data):
|
|
536
|
+
"""Deprecated helper function."""
|
|
537
|
+
# This function is no longer used
|
|
538
|
+
result = process(data)
|
|
539
|
+
return result
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
recommendation: safe to delete
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
### 3. unused classes
|
|
547
|
+
|
|
548
|
+
confidence: 80%
|
|
549
|
+
action: verify inheritance/composition, then remove
|
|
550
|
+
|
|
551
|
+
#### `src/models.py`
|
|
552
|
+
|
|
553
|
+
**class: `LegacyModel` (line 120)**
|
|
554
|
+
- no instances found
|
|
555
|
+
- no imports or references
|
|
556
|
+
- replaced by `NewModel`
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
class LegacyModel:
|
|
560
|
+
"""Old model class, deprecated."""
|
|
561
|
+
|
|
562
|
+
def __init__(self):
|
|
563
|
+
self.data = {}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
recommendation: delete after verifying no schema references
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### 4. commented-out code blocks
|
|
571
|
+
|
|
572
|
+
confidence: 100%
|
|
573
|
+
action: safe to remove
|
|
574
|
+
|
|
575
|
+
#### `src/main.py`
|
|
576
|
+
|
|
577
|
+
**lines 200-220: commented function**
|
|
578
|
+
```python
|
|
579
|
+
# def old_feature():
|
|
580
|
+
# """This was removed in v1.2."""
|
|
581
|
+
# pass
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
recommendation: remove comments
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
### 5. unreachable code paths
|
|
589
|
+
|
|
590
|
+
confidence: 95%
|
|
591
|
+
action: safe to remove
|
|
592
|
+
|
|
593
|
+
#### `src/processor.py`
|
|
594
|
+
|
|
595
|
+
**lines 85-90: code after return**
|
|
596
|
+
```python
|
|
597
|
+
def process_item(item):
|
|
598
|
+
if not item:
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
# This code is unreachable
|
|
602
|
+
logger.info("Processing item")
|
|
603
|
+
return transform(item)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
recommendation: remove unreachable code
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
### 6. unused variables and constants
|
|
611
|
+
|
|
612
|
+
confidence: 90%
|
|
613
|
+
action: safe to remove
|
|
614
|
+
|
|
615
|
+
#### `src/config.py`
|
|
616
|
+
|
|
617
|
+
**line 15: `OLD_API_KEY`**
|
|
618
|
+
- defined but never used
|
|
619
|
+
- likely from removed feature
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
OLD_API_KEY = "deprecated_key" # unused
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
recommendation: delete
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## requires manual review
|
|
630
|
+
|
|
631
|
+
### uncertain items
|
|
632
|
+
|
|
633
|
+
1. **`src/api.py::endpoint_handler`**
|
|
634
|
+
- only called in tests
|
|
635
|
+
- might be intentional test-only code
|
|
636
|
+
- action: verify intent with team
|
|
637
|
+
|
|
638
|
+
2. **`src/plugins.py::load_plugin`**
|
|
639
|
+
- uses dynamic imports
|
|
640
|
+
- called via string name
|
|
641
|
+
- grep may miss references
|
|
642
|
+
- action: manual code review
|
|
643
|
+
|
|
644
|
+
3. **`src/database.py::LegacyTable`**
|
|
645
|
+
- no code references
|
|
646
|
+
- check database schema
|
|
647
|
+
- action: verify in production database
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## safe deletion script
|
|
652
|
+
|
|
653
|
+
python script to automate safe deletions:
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
#!/usr/bin/env python3
|
|
657
|
+
"""automated dead code removal - use with caution!"""
|
|
658
|
+
|
|
659
|
+
import ast
|
|
660
|
+
import re
|
|
661
|
+
from pathlib import Path
|
|
662
|
+
|
|
663
|
+
def remove_unused_imports(filepath):
|
|
664
|
+
"""remove unused imports from a python file."""
|
|
665
|
+
# run autoflake on the file
|
|
666
|
+
import subprocess
|
|
667
|
+
result = subprocess.run(
|
|
668
|
+
['autoflake', '--remove-all-unused-imports', '--in-place', str(filepath)],
|
|
669
|
+
capture_output=True
|
|
670
|
+
)
|
|
671
|
+
return result.returncode == 0
|
|
672
|
+
|
|
673
|
+
def remove_commented_code(filepath):
|
|
674
|
+
"""remove large commented-out blocks."""
|
|
675
|
+
with open(filepath, 'r') as f:
|
|
676
|
+
lines = f.readlines()
|
|
677
|
+
|
|
678
|
+
# TODO: implement smart commented code detection
|
|
679
|
+
# Look for 3+ consecutive commented lines that look like code
|
|
680
|
+
|
|
681
|
+
with open(filepath, 'w') as f:
|
|
682
|
+
f.writelines(lines)
|
|
683
|
+
|
|
684
|
+
# main execution
|
|
685
|
+
if __name__ == "__main__":
|
|
686
|
+
# always review changes before committing!
|
|
687
|
+
pass
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## recommendations
|
|
693
|
+
|
|
694
|
+
### immediate actions (this week)
|
|
695
|
+
|
|
696
|
+
1. remove all unused imports
|
|
697
|
+
- estimated effort: 1-2 hours
|
|
698
|
+
- tool: autoflake or vulture
|
|
699
|
+
- risk: very low
|
|
700
|
+
|
|
701
|
+
2. remove commented-out code blocks
|
|
702
|
+
- estimated effort: 2-3 hours
|
|
703
|
+
- tool: manual review + script
|
|
704
|
+
- risk: low
|
|
705
|
+
|
|
706
|
+
3. fix unreachable code paths
|
|
707
|
+
- estimated effort: 1-2 hours
|
|
708
|
+
- tool: code review
|
|
709
|
+
- risk: low
|
|
710
|
+
|
|
711
|
+
### short-term actions (next sprint)
|
|
712
|
+
|
|
713
|
+
1. remove unused private functions
|
|
714
|
+
- estimated effort: 4-6 hours
|
|
715
|
+
- tool: vulture + manual review
|
|
716
|
+
- risk: low-medium
|
|
717
|
+
|
|
718
|
+
2. clean up unused constants/variables
|
|
719
|
+
- estimated effort: 2-3 hours
|
|
720
|
+
- tool: vulture
|
|
721
|
+
- risk: low
|
|
722
|
+
|
|
723
|
+
### long-term actions (technical debt backlog)
|
|
724
|
+
|
|
725
|
+
1. remove unused classes
|
|
726
|
+
- estimated effort: 6-8 hours
|
|
727
|
+
- tool: manual review + testing
|
|
728
|
+
- risk: medium
|
|
729
|
+
|
|
730
|
+
2. clean up old migration files
|
|
731
|
+
- estimated effort: 2-4 hours
|
|
732
|
+
- tool: manual
|
|
733
|
+
- risk: low
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## prevention strategies
|
|
738
|
+
|
|
739
|
+
### code review guidelines
|
|
740
|
+
|
|
741
|
+
- [ ] check for new unused imports
|
|
742
|
+
- [ ] remove dead code before merging
|
|
743
|
+
- [ ] use linters in ci/cd
|
|
744
|
+
- [ ] require code coverage > 80%
|
|
745
|
+
|
|
746
|
+
### tool configuration
|
|
747
|
+
|
|
748
|
+
add to `.pre-commit-config.yaml`:
|
|
749
|
+
|
|
750
|
+
```yaml
|
|
751
|
+
repos:
|
|
752
|
+
- repo: https://github.com/PyCQA/autoflake
|
|
753
|
+
rev: v1.7.0
|
|
754
|
+
hooks:
|
|
755
|
+
- id: autoflake
|
|
756
|
+
args: ['--remove-all-unused-imports', '--in-place']
|
|
757
|
+
|
|
758
|
+
- repo: https://github.com/jendrikseipp/vulture
|
|
759
|
+
rev: v2.6
|
|
760
|
+
hooks:
|
|
761
|
+
- id: vulture
|
|
762
|
+
args: ['--min-confidence', '70']
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### continuous monitoring
|
|
766
|
+
|
|
767
|
+
schedule dead code analysis:
|
|
768
|
+
- weekly automated scan
|
|
769
|
+
- monthly comprehensive review
|
|
770
|
+
- quarterly cleanup sprint
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## appendix
|
|
775
|
+
|
|
776
|
+
### tools used
|
|
777
|
+
|
|
778
|
+
- vulture: static analysis for dead code
|
|
779
|
+
- autoflake: remove unused imports
|
|
780
|
+
- grep: pattern matching
|
|
781
|
+
- git: history analysis
|
|
782
|
+
- custom scripts: dynamic analysis
|
|
783
|
+
|
|
784
|
+
### limitations
|
|
785
|
+
|
|
786
|
+
- cannot detect dynamic references (getattr, __import__)
|
|
787
|
+
- may have false positives for plugin systems
|
|
788
|
+
- test coverage gaps don't always mean dead code
|
|
789
|
+
- api endpoints may be called externally
|
|
790
|
+
|
|
791
|
+
### next analysis
|
|
792
|
+
|
|
793
|
+
recommended schedule: monthly
|
|
794
|
+
next analysis date: $(date -v+1m '+%Y-%m-%d')
|