codesuture 0.5.0__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.
codesuture/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
codesuture/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+
2
+ from codesuture.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from codesuture.codesuture_fix import apply_fix_with_info
3
+
4
+ result = apply_fix_with_info()
5
+ print(result)
codesuture/audit.py ADDED
@@ -0,0 +1,127 @@
1
+ import os
2
+ import json
3
+ import sys
4
+ from datetime import datetime
5
+
6
+ def run_audit(patch_store_path: str = None):
7
+
8
+ candidates = [
9
+ ".codesuture_cache", ".codesuture_store",
10
+ ".codesuture", "codesuture_patches"
11
+ ]
12
+ store = patch_store_path
13
+ if not store:
14
+ for c in candidates:
15
+ if os.path.exists(c):
16
+ store = c
17
+ break
18
+
19
+ if not store:
20
+ print("[CodeSuture] No patch store found. Nothing has been patched yet.")
21
+ return
22
+
23
+ patches = _load_all_patches(store)
24
+
25
+ if not patches:
26
+ print("[CodeSuture] Patch store exists but is empty.")
27
+ return
28
+
29
+ now = datetime.utcnow()
30
+
31
+ col_func = max(18, max(len(p.get("func_name","?")) for p in patches) + 2)
32
+ col_guard = 12
33
+ col_target = 10
34
+ col_default = 10
35
+ col_age = 7
36
+
37
+ try:
38
+ "|".encode(sys.stdout.encoding or 'ascii')
39
+ HAS_UNICODE = True
40
+ except Exception:
41
+ HAS_UNICODE = False
42
+
43
+ def row(f, g, t, d, a):
44
+ v = "|" if HAS_UNICODE else "|"
45
+ return (f"{v} {f:<{col_func}} {v} {g:<{col_guard}} {v} "
46
+ f"{t:<{col_target}} {v} {d:<{col_default}} {v} {a:<{col_age}} {v}")
47
+
48
+ if HAS_UNICODE:
49
+ sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
50
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+")
51
+ top = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
52
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+++")
53
+ bot = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
54
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+")
55
+ else:
56
+ sep = (f"|-{'*'*col_func}-+-{'*'*col_guard}-+-"
57
+ f"{'*'*col_target}-+-{'*'*col_default}-+-{'*'*col_age}-|")
58
+ top = sep
59
+ bot = sep
60
+
61
+ print()
62
+ print(" CodeSuture Audit Report")
63
+ print()
64
+ print(top)
65
+ print(row("Function", "Guard", "Target", "Default", "Age"))
66
+ print(sep)
67
+
68
+ oldest_days = 0
69
+ expired = 0
70
+ for p in patches:
71
+ func = p.get("func_name", "?")
72
+ guard = p.get("guard_type", "?")
73
+ target = p.get("target", "?")
74
+ default = repr(p.get("default_value", "?"))[:col_default]
75
+ age_str = "?"
76
+ ttl_days = p.get("ttl_days", 7)
77
+ if "patched_at" in p:
78
+ try:
79
+ dt = datetime.fromisoformat(p["patched_at"])
80
+ days = (now - dt).days
81
+ oldest_days = max(oldest_days, days)
82
+ age_str = f"{days}d"
83
+ if days > ttl_days:
84
+ age_str += " [WARN]"
85
+ expired += 1
86
+ except Exception:
87
+ pass
88
+ print(row(func, guard, target, default, age_str))
89
+
90
+ print(bot)
91
+ print()
92
+ print(f" Total: {len(patches)} active patch(es). "
93
+ f"Oldest: {oldest_days}d. "
94
+ f"{'[WARN] ' + str(expired) + ' expired - run codesuture rollback to clear.' if expired else 'All within TTL.'}")
95
+ print()
96
+ print(" Run 'codesuture rollback <function_name>' to remove a patch.")
97
+ print(" Run 'codesuture rollback --all' to clear everything.")
98
+ print()
99
+
100
+ def _load_all_patches(store_path: str) -> list[dict]:
101
+
102
+ patches = []
103
+ if os.path.isdir(store_path):
104
+ for root, dirs, files in os.walk(store_path):
105
+ for fname in files:
106
+ fpath = os.path.join(root, fname)
107
+ if fname.endswith(".json") and os.path.isfile(fpath):
108
+ try:
109
+ with open(fpath, "r", encoding="utf-8") as f:
110
+ data = json.load(f)
111
+ if isinstance(data, list):
112
+ patches.extend(data)
113
+ elif isinstance(data, dict):
114
+ patches.append(data)
115
+ except Exception:
116
+ pass
117
+ elif os.path.isfile(store_path):
118
+ try:
119
+ with open(store_path, "r", encoding="utf-8") as f:
120
+ data = json.load(f)
121
+ if isinstance(data, list):
122
+ patches = data
123
+ elif isinstance(data, dict):
124
+ patches = list(data.values())
125
+ except Exception:
126
+ pass
127
+ return patches
codesuture/cli.py ADDED
@@ -0,0 +1,127 @@
1
+ import sys
2
+ import argparse
3
+ from codesuture.tracer import install, uninstall
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser(prog='codesuture',
7
+ description='Runtime Python bytecode patcher with self-healing re-execution')
8
+ parser.add_argument('--version', action='version', version='codesuture 0.5.0')
9
+ sub = parser.add_subparsers(dest='command', required=True)
10
+
11
+ run_parser = sub.add_parser('run', help='Run a script with live patching')
12
+ run_parser.add_argument('script', help='Target script to run')
13
+ run_parser.add_argument('--dry-run', action='store_true', help='Show what would be patched without applying')
14
+ run_parser.add_argument('--log', metavar='FILE', help='Append patch events (JSON lines) to FILE')
15
+ run_parser.add_argument('--retries', type=int, default=3, metavar='N', help='Max patching attempts (default: 3)')
16
+ run_parser.add_argument('--self-test', action='store_true', help='Corrupt the engine to test self-healing')
17
+ run_parser.add_argument('--autonomous', action='store_true', help='Enable autonomous LLM bug-fixing')
18
+ run_parser.add_argument('--verbose', action='store_true', help='Show detailed debug output')
19
+ run_parser.add_argument('--shadow', action='store_true', help='Warn if patched functions return sentinel values')
20
+ run_parser.add_argument('--ttl', type=int, default=7, metavar='DAYS', help='Patch TTL in days (default: 7)')
21
+
22
+ sub.add_parser('audit', help='Show all active patches')
23
+
24
+ rb_parser = sub.add_parser('rollback', help='Remove persisted patches')
25
+ rb_parser.add_argument('function_name', nargs='?', default=None, help='Function name to roll back')
26
+ rb_parser.add_argument('--all', action='store_true', dest='rollback_all', help='Remove ALL patches + fingerprints')
27
+ rb_parser.add_argument('--dry-run', action='store_true', dest='rollback_dry_run', help='List what would be removed')
28
+
29
+ watch_parser = sub.add_parser('watch', help='Watch and auto-restart a script with live patching')
30
+ watch_parser.add_argument('script', help='Target script to watch')
31
+ watch_parser.add_argument('--max-restarts', type=int, default=10, metavar='N',
32
+ help='Maximum number of restarts (default: 10)')
33
+ watch_parser.add_argument('--shadow', action='store_true', help='Enable shadow mode warnings')
34
+ watch_parser.add_argument('--verbose', action='store_true', help='Show detailed debug output')
35
+
36
+ explain_parser = sub.add_parser('explain', help='Show detailed explanation of active patches')
37
+ explain_parser.add_argument('func_name', nargs='?', default=None, help='Function name to explain')
38
+
39
+ args = parser.parse_args()
40
+
41
+ if args.command == 'audit':
42
+ from codesuture.audit import run_audit
43
+ run_audit()
44
+ return
45
+
46
+ if args.command == 'rollback':
47
+ from codesuture.rollback import rollback_function, rollback_all, rollback_dry_run
48
+ if args.rollback_dry_run:
49
+ rollback_dry_run()
50
+ elif args.rollback_all:
51
+ rollback_all()
52
+ elif args.function_name:
53
+ rollback_function(args.function_name)
54
+ else:
55
+ print("[CodeSuture] Usage: codesuture rollback <function_name> | --all | --dry-run")
56
+ return
57
+
58
+ if args.command == 'watch':
59
+ from codesuture.watcher import watch
60
+ exit_code = watch(
61
+ args.script,
62
+ max_restarts=args.max_restarts,
63
+ shadow=args.shadow,
64
+ verbose=args.verbose,
65
+ )
66
+ sys.exit(exit_code)
67
+
68
+ if args.command == 'explain':
69
+ from codesuture.explain import run_explain
70
+ run_explain(args.func_name)
71
+ return
72
+
73
+ if getattr(args, 'autonomous', False):
74
+ try:
75
+ import llama_cpp
76
+ except ImportError:
77
+ print("Autonomous mode requires llama-cpp-python. Install with: pip install codesuture[autonomous]")
78
+ sys.exit(1)
79
+
80
+ if args.command == 'run':
81
+ from codesuture.persistence import install_import_hook, make_persisted_patch_globals
82
+
83
+ install_import_hook()
84
+
85
+ if getattr(args, 'self_test', False):
86
+ import codesuture.pattern_matcher as pm
87
+ print("[CodeSuture] SELF-TEST: corrupting _infer_default -> None")
88
+ pm._infer_default = None
89
+ tracer = None
90
+ try:
91
+ with open(args.script, 'r', encoding='utf-8') as f:
92
+ source = f.read()
93
+ code = compile(source, args.script, 'exec')
94
+
95
+ tracer = install(dry_run=args.dry_run, log_file=args.log,
96
+ max_retries=args.retries,
97
+ autonomous=getattr(args, 'autonomous', False),
98
+ script_path=args.script, verbose=args.verbose,
99
+ shadow=args.shadow, ttl=args.ttl)
100
+ max_runs = args.retries + 1
101
+ for run in range(max_runs):
102
+ patched_before = tracer.stats['patched']
103
+ tracer._handled_exc_ids.clear()
104
+ try:
105
+ sys.settrace(tracer)
106
+ globs = make_persisted_patch_globals(
107
+ "__main__",
108
+ {'__name__': '__main__', '__file__': args.script},
109
+ )
110
+ exec(code, globs)
111
+ break
112
+ except Exception as e:
113
+ sys.settrace(None)
114
+ new_patches = tracer.stats['patched'] - patched_before
115
+ if new_patches > 0 and run < max_runs - 1:
116
+ print(f"[CodeSuture] Re-executing after {new_patches} patch(es)...")
117
+ continue
118
+ else:
119
+ print(f"[CodeSuture] Script exited with: {e}")
120
+ break
121
+ finally:
122
+ uninstall()
123
+ if tracer is not None:
124
+ tracer.report()
125
+
126
+ if __name__ == '__main__':
127
+ main()
@@ -0,0 +1,82 @@
1
+ import types
2
+ import inspect
3
+
4
+ def replace_function_code(func, new_code):
5
+
6
+ from codesuture.guard_synthesizer import propagate_patch
7
+ propagate_patch(func, new_code)
8
+
9
+ def get_function_from_frame(frame):
10
+ name = frame.f_code.co_name
11
+ code = frame.f_code
12
+
13
+ if name in frame.f_locals:
14
+ candidate = frame.f_locals[name]
15
+ if hasattr(candidate, '__code__') and candidate.__code__ is code:
16
+ return candidate
17
+
18
+ if name in frame.f_globals:
19
+ candidate = frame.f_globals[name]
20
+ if hasattr(candidate, '__code__') and candidate.__code__ is code:
21
+ return candidate
22
+
23
+ if name == '<module>':
24
+ return None
25
+
26
+ self_obj = frame.f_locals.get('self')
27
+ if self_obj is not None:
28
+ cls = type(self_obj)
29
+ method = getattr(cls, name, None)
30
+ if method is not None:
31
+ if isinstance(method, property) and method.fget is not None:
32
+ func = method.fget
33
+ else:
34
+ func = method
35
+ if hasattr(func, '__func__'):
36
+ func = func.__func__
37
+ if hasattr(func, '__code__') and func.__code__ is code:
38
+ return func
39
+
40
+ cls_obj = frame.f_locals.get('cls')
41
+ if cls_obj is not None and isinstance(cls_obj, type):
42
+ method = getattr(cls_obj, name, None)
43
+ if method is not None:
44
+ if isinstance(method, property) and method.fget is not None:
45
+ func = method.fget
46
+ else:
47
+ func = method
48
+ if hasattr(func, '__func__'):
49
+ func = func.__func__
50
+ if hasattr(func, '__code__') and func.__code__ is code:
51
+ return func
52
+
53
+ for val in frame.f_globals.values():
54
+ if isinstance(val, type):
55
+ method = getattr(val, name, None)
56
+ if method is not None:
57
+ if isinstance(method, property) and method.fget is not None:
58
+ func = method.fget
59
+ else:
60
+ func = method
61
+ if hasattr(func, '__func__'):
62
+ func = func.__func__
63
+ if hasattr(func, '__code__') and func.__code__ is code:
64
+ return func
65
+
66
+ for val in frame.f_globals.values():
67
+ if hasattr(val, '__wrapped__'):
68
+ inner = val.__wrapped__
69
+ if hasattr(inner, '__code__') and inner.__code__ is code:
70
+ return inner
71
+ if hasattr(val, '__code__') and val.__code__ is code:
72
+ return val
73
+
74
+ return None
75
+
76
+ def get_source_from_frame(frame):
77
+
78
+ func = get_function_from_frame(frame)
79
+ try:
80
+ return inspect.getsource(func)
81
+ except Exception as e:
82
+ return f"# Could not get source: {e}\n"
@@ -0,0 +1,85 @@
1
+ import sys
2
+ import inspect
3
+ import os
4
+ import types
5
+ from codesuture.pattern_matcher import analyze_exception
6
+ from codesuture.guard_synthesizer import synthesize_guarded_code
7
+ from codesuture.code_replacer import replace_function_code, get_function_from_frame
8
+ from codesuture.rewind import rewind_frame_to_start
9
+
10
+ def apply_fix(exc_type_name: str = None, exc_msg: str = None) -> str:
11
+
12
+ exc_info = sys.exc_info()
13
+ target_frame = None
14
+ tb = None
15
+
16
+ if exc_info[0] is not None:
17
+ if exc_type_name is None:
18
+ exc_type_name = exc_info[0].__name__
19
+ if exc_msg is None:
20
+ exc_msg = str(exc_info[1])
21
+
22
+ tb = exc_info[2]
23
+ curr_tb = tb
24
+ while curr_tb and curr_tb.tb_next:
25
+ curr_tb = curr_tb.tb_next
26
+ if curr_tb:
27
+ target_frame = curr_tb.tb_frame
28
+
29
+ if target_frame is None:
30
+ if exc_type_name is None or exc_msg is None:
31
+ return "ERROR: No active exception. You must provide exc_type_name and exc_msg if paused on an unhandled exception."
32
+
33
+ for fi in inspect.stack():
34
+ frame = fi.frame
35
+ filename = frame.f_code.co_filename
36
+ func_name = frame.f_code.co_name
37
+
38
+ if 'pydevd' in filename.lower() and func_name in ('evaluate_expression', 'new_func', '_run_with_unblock_threads', '_run_with_interrupt_thread'):
39
+ potential_frame = frame.f_locals.get('frame')
40
+ if isinstance(potential_frame, types.FrameType):
41
+ target_frame = potential_frame
42
+ break
43
+
44
+ if func_name in ('apply_fix', 'apply_fix_with_info', '<module>', 'Exec', 'exec'):
45
+ continue
46
+ if any(x in filename.lower() for x in ('pydevd', 'debugpy', 'threading', 'importlib')):
47
+ continue
48
+
49
+ basename = os.path.basename(filename)
50
+ internal_files = (
51
+ 'codesuture_fix.py', 'pattern_matcher.py', 'guard_synthesizer.py',
52
+ 'code_replacer.py', 'rewind.py', 'tracer.py', 'debuggee.py'
53
+ )
54
+ if basename in internal_files:
55
+ continue
56
+
57
+ target_frame = frame
58
+ break
59
+
60
+ if target_frame is None:
61
+ return "ERROR: No paused frame found"
62
+
63
+ class FakeExc:
64
+ def __init__(self, msg):
65
+ self._msg = msg
66
+ def __str__(self):
67
+ return self._msg
68
+
69
+ exc_type = FakeExc
70
+ exc_type.__name__ = exc_type_name
71
+ exc_value = FakeExc(exc_msg)
72
+
73
+ spec = analyze_exception(target_frame, exc_type, exc_value, tb)
74
+ if spec is None:
75
+ return f"ERROR: No deterministic patch for {exc_type_name}"
76
+
77
+ try:
78
+ func = get_function_from_frame(target_frame)
79
+ new_bc = synthesize_guarded_code(target_frame.f_code, spec)
80
+ new_code = new_bc.to_code()
81
+ replace_function_code(func, new_code)
82
+ rewind_frame_to_start(target_frame, target_frame.f_code)
83
+ return f"OK: patched {target_frame.f_code.co_name} ({spec.strategy} on {spec.var_name})"
84
+ except Exception as e:
85
+ return f"ERROR: {e}"
codesuture/debuggee.py ADDED
@@ -0,0 +1,7 @@
1
+ import debugpy
2
+
3
+ def enable(port=5678):
4
+ debugpy.listen(port)
5
+ print(f"[CodeSuture] Waiting for debugger on port {port}...")
6
+ debugpy.wait_for_client()
7
+ print("[CodeSuture] Debugger attached. Live patching active.")
@@ -0,0 +1,27 @@
1
+ import dis
2
+ from dataclasses import dataclass
3
+
4
+ @dataclass
5
+ class DiffResult:
6
+ guard_type: str
7
+ added: int
8
+ removed: int
9
+ changed: int
10
+ allowed: int
11
+ rejected: bool
12
+ reason: str = ""
13
+
14
+ def semantic_diff(original_code, patched_code, guard_type: str) -> DiffResult:
15
+ orig_ops = [(i.opname, i.argval) for i in dis.get_instructions(original_code)]
16
+ patch_ops = [(i.opname, i.argval) for i in dis.get_instructions(patched_code)]
17
+ added = len([x for x in patch_ops if x not in orig_ops])
18
+ removed = len([x for x in orig_ops if x not in patch_ops])
19
+ changed = max(added, removed)
20
+ allowed = max(50, int(len(orig_ops) * 0.40))
21
+ rejected = changed > allowed
22
+ reason = (
23
+ f"Semantic diff too large for {guard_type}: "
24
+ f"{changed} instructions changed, allowed <= {allowed}. "
25
+ f"Patch was NOT applied."
26
+ ) if rejected else ""
27
+ return DiffResult(guard_type, added, removed, changed, allowed, rejected, reason)
codesuture/explain.py ADDED
@@ -0,0 +1,147 @@
1
+
2
+ import os
3
+ import json
4
+ import sys
5
+ from datetime import datetime
6
+
7
+ def run_explain(func_name=None):
8
+
9
+ candidates = [
10
+ ".codesuture_cache", ".codesuture_store",
11
+ ".codesuture", "codesuture_patches",
12
+ ]
13
+ store = None
14
+ for c in candidates:
15
+ if os.path.exists(c):
16
+ store = c
17
+ break
18
+
19
+ if not store:
20
+ print("[CodeSuture] No active patches.")
21
+ return
22
+
23
+ patches = _load_all_patches(store)
24
+
25
+ if not patches:
26
+ print("[CodeSuture] No active patches.")
27
+ return
28
+
29
+ if func_name:
30
+ patches = [p for p in patches if func_name.lower() in p.get("func_name", "").lower()]
31
+ if not patches:
32
+ print(f"[CodeSuture] No patches found for '{func_name}'.")
33
+ return
34
+
35
+ now = datetime.utcnow()
36
+
37
+ try:
38
+ "|".encode(sys.stdout.encoding or "ascii")
39
+ HAS_UNICODE = True
40
+ except Exception:
41
+ HAS_UNICODE = False
42
+
43
+ col_func = max(12, max(len(p.get("func_name", "?")) for p in patches) + 2)
44
+ col_guard = max(12, max(len(p.get("guard_type", "?")) for p in patches) + 2)
45
+ col_target = max(10, max(len(str(p.get("target", "?"))) for p in patches) + 2)
46
+ col_default = max(10, max(len(repr(p.get("default_value", "?"))[:15]) for p in patches) + 2)
47
+ col_age = 12
48
+ col_safe = 9
49
+
50
+ v = "|" if HAS_UNICODE else "|"
51
+
52
+ def row(f, g, t, d, a, s):
53
+ return (f"{v} {f:<{col_func}} {v} {g:<{col_guard}} {v} "
54
+ f"{t:<{col_target}} {v} {d:<{col_default}} {v} "
55
+ f"{a:<{col_age}} {v} {s:<{col_safe}} {v}")
56
+
57
+ if HAS_UNICODE:
58
+ sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
59
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
60
+ top = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
61
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
62
+ bot = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
63
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
64
+ else:
65
+ sep = (f"+-{'-'*col_func}-+-{'-'*col_guard}-+-"
66
+ f"{'-'*col_target}-+-{'-'*col_default}-+-{'-'*col_age}-+-{'-'*col_safe}-+")
67
+ top = sep
68
+ bot = sep
69
+
70
+ print()
71
+ print(" CodeSuture Explain - Active Patches")
72
+ print()
73
+ print(top)
74
+ print(row("Function", "Guard type", "Target", "Default value", "Age (days)", "Safe?"))
75
+ print(sep)
76
+
77
+ for p in patches:
78
+ func = p.get("func_name", "?")
79
+ guard = p.get("guard_type", "?")
80
+ target = str(p.get("target", "?"))
81
+ default = repr(p.get("default_value", "?"))[:15]
82
+ age_str = "?"
83
+ if "patched_at" in p:
84
+ try:
85
+ dt = datetime.fromisoformat(p["patched_at"])
86
+ days = (now - dt).days
87
+ age_str = f"{days}d ago (patched)"
88
+ except Exception:
89
+ pass
90
+ safe = _assess_safety(p)
91
+ print(row(func, guard, target, default, age_str, safe))
92
+
93
+ print(bot)
94
+ print()
95
+ print(f" Total: {len(patches)} active patch(es).")
96
+ print()
97
+
98
+ def _assess_safety(patch_data):
99
+
100
+ default = patch_data.get("default_value")
101
+ guard_type = patch_data.get("guard_type", "")
102
+ target = str(patch_data.get("target", ""))
103
+
104
+ string_methods = {"strip", "upper", "lower", "title", "capitalize",
105
+ "casefold", "swapcase", "encode", "replace", "split"}
106
+
107
+ is_string_downstream = any(m in target.lower() for m in string_methods)
108
+
109
+ if default == "" and (guard_type == "null_guard" or is_string_downstream):
110
+ return "LIKELY"
111
+
112
+ if default == 0 and is_string_downstream:
113
+ return "RISKY"
114
+
115
+ if default == "" and guard_type in ("null_guard", "subscript_guard", "key_guard"):
116
+ return "LIKELY"
117
+
118
+ return "UNKNOWN"
119
+
120
+ def _load_all_patches(store_path):
121
+
122
+ patches = []
123
+ if os.path.isdir(store_path):
124
+ for root, dirs, files in os.walk(store_path):
125
+ for fname in files:
126
+ fpath = os.path.join(root, fname)
127
+ if fname.endswith(".json") and os.path.isfile(fpath):
128
+ try:
129
+ with open(fpath, "r", encoding="utf-8") as f:
130
+ data = json.load(f)
131
+ if isinstance(data, list):
132
+ patches.extend(data)
133
+ elif isinstance(data, dict):
134
+ patches.append(data)
135
+ except Exception:
136
+ pass
137
+ elif os.path.isfile(store_path):
138
+ try:
139
+ with open(store_path, "r", encoding="utf-8") as f:
140
+ data = json.load(f)
141
+ if isinstance(data, list):
142
+ patches = data
143
+ elif isinstance(data, dict):
144
+ patches = list(data.values())
145
+ except Exception:
146
+ pass
147
+ return patches
@@ -0,0 +1,52 @@
1
+ import hashlib
2
+ import dis
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+
7
+ FINGERPRINT_FILE = ".codesuture_fingerprints"
8
+
9
+ def compute_fingerprint(code_obj, crash_offset: int, exc_type_name: str) -> str:
10
+ instructions = list(dis.get_instructions(code_obj))
11
+ offsets = [i.offset for i in instructions]
12
+ try:
13
+ idx = offsets.index(crash_offset)
14
+ except ValueError:
15
+ idx = len(instructions) - 1
16
+ window = instructions[max(0, idx-2):idx+3]
17
+ key = str([(i.opname, str(i.argval)) for i in window]) + ":" + exc_type_name
18
+ return hashlib.sha256(key.encode()).hexdigest()[:16]
19
+
20
+ def load_registry() -> dict:
21
+ if not os.path.exists(FINGERPRINT_FILE):
22
+ return {}
23
+ with open(FINGERPRINT_FILE, "r", encoding="utf-8") as f:
24
+ return json.load(f)
25
+
26
+ def save_registry(registry: dict):
27
+ with open(FINGERPRINT_FILE, "w", encoding="utf-8") as f:
28
+ json.dump(registry, f, indent=2)
29
+
30
+ def lookup(fingerprint: str) -> dict | None:
31
+ return load_registry().get(fingerprint)
32
+
33
+ def record(fingerprint: str, guard_type: str, target: str,
34
+ func_name: str, error_type: str, default_value=None, key_name=None):
35
+ registry = load_registry()
36
+ if fingerprint not in registry:
37
+ registry[fingerprint] = {
38
+ "guard_type": guard_type,
39
+ "target": target,
40
+ "error_type": error_type,
41
+ "default_value": default_value,
42
+ "key_name": key_name,
43
+ "first_seen": datetime.utcnow().isoformat(),
44
+ "hit_count": 0,
45
+ "functions": []
46
+ }
47
+ entry = registry[fingerprint]
48
+ entry["hit_count"] += 1
49
+ if func_name not in entry["functions"]:
50
+ entry["functions"].append(func_name)
51
+ save_registry(registry)
52
+ return entry