pycheck-neuron 1.0.7__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.7 → pycheck_neuron-1.1.0}/PKG-INFO +5 -5
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/README.md +3 -3
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/__init__.py +1 -1
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/audit.py +12 -8
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/flow.py +32 -13
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/rules.py +430 -64
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/PKG-INFO +5 -5
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/SOURCES.txt +1 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pyproject.toml +2 -2
- {pycheck_neuron-1.0.7 → 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.7 → pycheck_neuron-1.1.0}/tests/test_rules.py +1136 -1118
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/LICENSE +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/__main__.py +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/analyzer.py +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/cli.py +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck/server.py +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/dependency_links.txt +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/entry_points.txt +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/requires.txt +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/pycheck_neuron.egg-info/top_level.txt +0 -0
- {pycheck_neuron-1.0.7 → pycheck_neuron-1.1.0}/setup.cfg +0 -0
- {pycheck_neuron-1.0.7 → 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."""
|
|
@@ -273,7 +275,9 @@ def generate_proof_of_causality_certificate(
|
|
|
273
275
|
raw_serialized = json.dumps(cert_payload, sort_keys=True).encode("utf-8")
|
|
274
276
|
cert_id = f"CERT-POC-{hashlib.sha256(raw_serialized).hexdigest()[:16].upper()}"
|
|
275
277
|
cert_payload["certificate_id"] = cert_id
|
|
276
|
-
|
|
278
|
+
seal_hash = hashlib.sha256(raw_serialized).hexdigest()
|
|
279
|
+
cert_payload["sha256_digital_seal"] = seal_hash
|
|
280
|
+
cert_payload["sha256_audit_digest"] = seal_hash
|
|
277
281
|
|
|
278
282
|
return cert_payload
|
|
279
283
|
|
|
@@ -297,7 +301,7 @@ def format_certificate_text(cert: Dict[str, Any]) -> str:
|
|
|
297
301
|
unscanned_count = cert.get('unscanned_local_imports_count', 0)
|
|
298
302
|
|
|
299
303
|
if cert["audit_status"] == "CERTIFIED_ZERO_LEAKAGE":
|
|
300
|
-
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)"
|
|
301
305
|
cat_status = "no known patterns detected"
|
|
302
306
|
elif "UNVERIFIABLE" in cert["audit_status"]:
|
|
303
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)"
|
|
@@ -308,26 +312,26 @@ def format_certificate_text(cert: Dict[str, Any]) -> str:
|
|
|
308
312
|
|
|
309
313
|
lines = [
|
|
310
314
|
"+======================================================================+",
|
|
311
|
-
"| PROOF-OF-CAUSALITY(TM) AUDIT
|
|
312
|
-
"| PyCheck -
|
|
315
|
+
"| PROOF-OF-CAUSALITY(TM) AUDIT REPORT |",
|
|
316
|
+
"| PyCheck - Leakage & Causality Scanner |",
|
|
313
317
|
"+======================================================================+",
|
|
314
|
-
f"
|
|
318
|
+
f" Report ID: {cert['certificate_id']}",
|
|
315
319
|
f" Audit Status: {status_symbol}",
|
|
316
320
|
f" Timestamp (UTC): {cert['timestamp_utc']}",
|
|
317
321
|
f" Git Commit SHA: {cert['git_commit']}",
|
|
318
322
|
f" Git Branch: {cert['git_branch']}",
|
|
319
323
|
f" Scanned Files: {scanned_count} ({unscanned_count} local imports not scanned)",
|
|
320
324
|
"+----------------------------------------------------------------------+",
|
|
321
|
-
" INVARIANT VERIFICATION CHECKLIST (
|
|
325
|
+
f" INVARIANT VERIFICATION CHECKLIST ({len(ALL_RULES)} RULES):",
|
|
322
326
|
f" Summary: {summary_line}",
|
|
323
327
|
f" * Temporal Lookahead Bias (T001-T019): {cat_status if cert['invariants_verified']['temporal_lookahead_free'] else 'FAILED'}",
|
|
324
328
|
f" * Cross-Fold Lineage & Leakage (F001-F002): {cat_status if cert['invariants_verified']['partition_provenance_clean'] else 'FAILED'}",
|
|
325
329
|
f" * Causal Mode Confounding (C001-C007): {cat_status if cert['invariants_verified']['causal_intervention_isolated'] else 'FAILED'}",
|
|
326
330
|
f" * Uncertainty Bounds Guard (U001-U006): {cat_status if cert['invariants_verified']['uncertainty_guards_present'] else 'FAILED'}",
|
|
327
331
|
"+----------------------------------------------------------------------+",
|
|
328
|
-
f" SHA-256
|
|
332
|
+
f" SHA-256 Audit Digest: {cert['sha256_digital_seal']}",
|
|
329
333
|
" Issued by: Neuron Labs - https://neuron-lab.org/pycheck",
|
|
330
|
-
"
|
|
334
|
+
" Compile-Time Guard: NEURON Language (https://neuron-lab.org)",
|
|
331
335
|
"+======================================================================+"
|
|
332
336
|
]
|
|
333
337
|
if cert.get("unscanned_local_imports"):
|
|
@@ -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)
|