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.
Files changed (23) hide show
  1. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/PKG-INFO +5 -5
  2. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/README.md +3 -3
  3. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/__init__.py +1 -1
  4. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/audit.py +4 -2
  5. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/flow.py +32 -13
  6. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/rules.py +415 -63
  7. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/PKG-INFO +5 -5
  8. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/SOURCES.txt +1 -0
  9. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pyproject.toml +2 -2
  10. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/tests/test_cli.py +1 -1
  11. pycheck_neuron-1.1.0/tests/test_regression.py +418 -0
  12. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/tests/test_rules.py +16 -5
  13. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/LICENSE +0 -0
  14. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/__main__.py +0 -0
  15. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/analyzer.py +0 -0
  16. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/cli.py +0 -0
  17. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck/server.py +0 -0
  18. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/dependency_links.txt +0 -0
  19. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/entry_points.txt +0 -0
  20. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/requires.txt +0 -0
  21. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/top_level.txt +0 -0
  22. {pycheck_neuron-1.0.8 → pycheck_neuron-1.1.0}/setup.cfg +0 -0
  23. {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.8
4
- Summary: 34-rule static AST analyzer catching temporal lookahead bias, causal confusion, and uncertainty bugs in Python ML code. Zero dependencies.
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
- **34 rules. Zero dependencies. Catches the bugs that cost millions.**
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: 34 active + Data Flow & Provenance Engine
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 34 rules
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
- **34 rules. Zero dependencies. Catches the bugs that cost millions.**
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: 34 active + Data Flow & Provenance Engine
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 34 rules
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.8"
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 (34 rules, {scanned_count} file{'s' if scanned_count != 1 else ''} scanned, {unscanned_count} local import{'s' if unscanned_count != 1 else ''} not scanned)"
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 (34 RULES):",
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
- # Check if accessing a tracked tainted column: df['feat'] or df[['feat']]
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
- # Check if indexing a tainted column name
310
- if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str):
311
- col_lower = node.slice.value.lower()
312
- # Only flag column names that unambiguously refer to future data.
313
- future_cols = {'future_price', 'future_return', 'next_close', 'next_price',
314
- 'next_day', 'tomorrow', 'price_change_next_day', 'future_avg_price'}
315
- if any(f in col_lower for f in future_cols):
316
- return f"accessing future-named column '{node.slice.value}'"
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)