pycheck-neuron 1.0.8__tar.gz → 1.1.0__tar.gz
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.
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/PKG-INFO +5 -5
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/README.md +3 -3
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/__init__.py +1 -1
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/audit.py +4 -2
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/flow.py +32 -13
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/rules.py +415 -63
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/PKG-INFO +5 -5
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/SOURCES.txt +1 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pyproject.toml +2 -2
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/tests/test_cli.py +1 -1
- pycheck_neuron-1.1.0/tests/test_regression.py +418 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/tests/test_rules.py +16 -5
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/LICENSE +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/__main__.py +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/analyzer.py +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/cli.py +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/server.py +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/dependency_links.txt +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/entry_points.txt +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/requires.txt +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/top_level.txt +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/setup.cfg +0 -0
- {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/tests/test_audit.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycheck-neuron
|
|
3
|
-
Version: 1.0
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: 37-rule static AST analyzer catching temporal lookahead bias, causal confusion, and uncertainty bugs in Python ML code. Zero dependencies.
|
|
5
5
|
Author: NEURON Team
|
|
6
6
|
Author-email: Fayo Ibrahim <fayo@neuron-lab.org>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
# PyCheck — ML Safety Analyzer
|
|
40
40
|
|
|
41
|
-
**
|
|
41
|
+
**37 rules. Zero dependencies. Catches the bugs that cost millions.**
|
|
42
42
|
|
|
43
43
|
PyCheck is a static AST analyzer that detects **temporal lookahead bias**, **causal confusion**, and **unguarded uncertainty** in Python ML and trading scripts — before you run a single line of code.
|
|
44
44
|
|
|
@@ -68,7 +68,7 @@ PyCheck catches all three before execution:
|
|
|
68
68
|
=================================================================
|
|
69
69
|
PyCheck — NEURON ML Safety Analyzer
|
|
70
70
|
Scanning: strategy.py
|
|
71
|
-
Rules:
|
|
71
|
+
Rules: 37 active + Data Flow & Provenance Engine
|
|
72
72
|
=================================================================
|
|
73
73
|
|
|
74
74
|
ERROR[T001]: .shift(-1) accesses data 1 rows INTO THE FUTURE
|
|
@@ -190,7 +190,7 @@ pycheck script.py # Errors + warnings (default)
|
|
|
190
190
|
pycheck script.py --info # Include info-level diagnostics
|
|
191
191
|
pycheck script.py --quiet # Errors only
|
|
192
192
|
pycheck script.py --json # JSON output — pipe into CI/CD
|
|
193
|
-
pycheck --list # List all
|
|
193
|
+
pycheck --list # List all 37 rules
|
|
194
194
|
pycheck --help
|
|
195
195
|
```
|
|
196
196
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PyCheck — ML Safety Analyzer
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**37 rules. Zero dependencies. Catches the bugs that cost millions.**
|
|
4
4
|
|
|
5
5
|
PyCheck is a static AST analyzer that detects **temporal lookahead bias**, **causal confusion**, and **unguarded uncertainty** in Python ML and trading scripts — before you run a single line of code.
|
|
6
6
|
|
|
@@ -30,7 +30,7 @@ PyCheck catches all three before execution:
|
|
|
30
30
|
=================================================================
|
|
31
31
|
PyCheck — NEURON ML Safety Analyzer
|
|
32
32
|
Scanning: strategy.py
|
|
33
|
-
Rules:
|
|
33
|
+
Rules: 37 active + Data Flow & Provenance Engine
|
|
34
34
|
=================================================================
|
|
35
35
|
|
|
36
36
|
ERROR[T001]: .shift(-1) accesses data 1 rows INTO THE FUTURE
|
|
@@ -152,7 +152,7 @@ pycheck script.py # Errors + warnings (default)
|
|
|
152
152
|
pycheck script.py --info # Include info-level diagnostics
|
|
153
153
|
pycheck script.py --quiet # Errors only
|
|
154
154
|
pycheck script.py --json # JSON output — pipe into CI/CD
|
|
155
|
-
pycheck --list # List all
|
|
155
|
+
pycheck --list # List all 37 rules
|
|
156
156
|
pycheck --help
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -3,7 +3,7 @@ PyCheck — NEURON ML Safety Analyzer
|
|
|
3
3
|
Catches temporal leaks, causal confusion, and uncertainty bugs in Python ML scripts.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
__version__ = "1.0
|
|
6
|
+
__version__ = "1.1.0"
|
|
7
7
|
|
|
8
8
|
from pycheck.analyzer import analyze_file, analyze_code, format_output
|
|
9
9
|
from pycheck.rules import ALL_RULES, RULES_BY_CODE
|
|
@@ -12,6 +12,8 @@ import time
|
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
13
|
from typing import Dict, List, Any, Optional, Set, Tuple
|
|
14
14
|
|
|
15
|
+
from pycheck.rules import ALL_RULES
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
def get_git_metadata(repo_path: str) -> Dict[str, str]:
|
|
17
19
|
"""Extract git commit hash, branch, and remote URL if available."""
|
|
@@ -299,7 +301,7 @@ def format_certificate_text(cert: Dict[str, Any]) -> str:
|
|
|
299
301
|
unscanned_count = cert.get('unscanned_local_imports_count', 0)
|
|
300
302
|
|
|
301
303
|
if cert["audit_status"] == "CERTIFIED_ZERO_LEAKAGE":
|
|
302
|
-
summary_line = f"no known patterns detected (
|
|
304
|
+
summary_line = f"no known patterns detected ({len(ALL_RULES)} rules, {scanned_count} file{'s' if scanned_count != 1 else ''} scanned, {unscanned_count} local import{'s' if unscanned_count != 1 else ''} not scanned)"
|
|
303
305
|
cat_status = "no known patterns detected"
|
|
304
306
|
elif "UNVERIFIABLE" in cert["audit_status"]:
|
|
305
307
|
summary_line = f"unverifiable ({scanned_count} file{'s' if scanned_count != 1 else ''} scanned, {unscanned_count} local import{'s' if unscanned_count != 1 else ''} not scanned)"
|
|
@@ -320,7 +322,7 @@ def format_certificate_text(cert: Dict[str, Any]) -> str:
|
|
|
320
322
|
f" Git Branch: {cert['git_branch']}",
|
|
321
323
|
f" Scanned Files: {scanned_count} ({unscanned_count} local imports not scanned)",
|
|
322
324
|
"+----------------------------------------------------------------------+",
|
|
323
|
-
" INVARIANT VERIFICATION CHECKLIST (
|
|
325
|
+
f" INVARIANT VERIFICATION CHECKLIST ({len(ALL_RULES)} RULES):",
|
|
324
326
|
f" Summary: {summary_line}",
|
|
325
327
|
f" * Temporal Lookahead Bias (T001-T019): {cat_status if cert['invariants_verified']['temporal_lookahead_free'] else 'FAILED'}",
|
|
326
328
|
f" * Cross-Fold Lineage & Leakage (F001-F002): {cat_status if cert['invariants_verified']['partition_provenance_clean'] else 'FAILED'}",
|
|
@@ -290,30 +290,49 @@ class TaintTracker:
|
|
|
290
290
|
return left_taint or right_taint
|
|
291
291
|
|
|
292
292
|
if isinstance(node, ast.Subscript):
|
|
293
|
-
# Subscript propagates taint from the value being indexed
|
|
294
|
-
val_taint = self._expr_taint(node.value)
|
|
295
|
-
if val_taint:
|
|
296
|
-
return val_taint
|
|
297
293
|
obj_name = self._get_var_name(node.value)
|
|
298
|
-
|
|
294
|
+
|
|
295
|
+
# --- Column-level dataflow: when accessing specific columns by name,
|
|
296
|
+
# only propagate taint from THAT column, not from sibling columns.
|
|
297
|
+
# This prevents df['safe_col'] from being flagged just because
|
|
298
|
+
# df['tainted_col'] exists. ---
|
|
299
|
+
|
|
299
300
|
if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str):
|
|
300
301
|
col = node.slice.value
|
|
302
|
+
col_lower = col.lower()
|
|
303
|
+
|
|
304
|
+
# 1. Check if this specific column is tainted
|
|
301
305
|
if obj_name and f"{obj_name}.{col}" in self.tainted_vars:
|
|
302
306
|
return f"column '{col}' is tainted: {self.tainted_vars[f'{obj_name}.{col}'].reason}"
|
|
307
|
+
|
|
308
|
+
# 2. Check future-named columns
|
|
309
|
+
future_cols = {'future_price', 'future_return', 'next_close', 'next_price',
|
|
310
|
+
'next_day', 'tomorrow', 'price_change_next_day', 'future_avg_price'}
|
|
311
|
+
if any(f in col_lower for f in future_cols):
|
|
312
|
+
return f"accessing future-named column '{col}'"
|
|
313
|
+
|
|
314
|
+
# 3. Propagate only DIRECT variable-level taint (e.g. df = tainted.shift(-1)),
|
|
315
|
+
# NOT column-derived taint from sibling columns
|
|
316
|
+
if obj_name and obj_name in self.tainted_vars:
|
|
317
|
+
return self.tainted_vars[obj_name].reason
|
|
318
|
+
return None
|
|
319
|
+
|
|
303
320
|
if isinstance(node.slice, (ast.List, ast.Tuple)):
|
|
321
|
+
# Multi-column access: check each column individually
|
|
304
322
|
for elt in node.slice.elts:
|
|
305
323
|
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
306
324
|
col = elt.value
|
|
307
325
|
if obj_name and f"{obj_name}.{col}" in self.tainted_vars:
|
|
308
326
|
return f"column '{col}' is tainted: {self.tainted_vars[f'{obj_name}.{col}'].reason}"
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
327
|
+
# Only propagate direct variable-level taint
|
|
328
|
+
if obj_name and obj_name in self.tainted_vars:
|
|
329
|
+
return self.tainted_vars[obj_name].reason
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
# Non-column subscript (numeric index, slice, variable): propagate all parent taints
|
|
333
|
+
val_taint = self._expr_taint(node.value)
|
|
334
|
+
if val_taint:
|
|
335
|
+
return val_taint
|
|
317
336
|
|
|
318
337
|
if isinstance(node, ast.Attribute):
|
|
319
338
|
return self._expr_taint(node.value)
|