code-oracle 0.1.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.
Files changed (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
code_oracle/cli.py ADDED
@@ -0,0 +1,795 @@
1
+ """
2
+ Command Line Interface for Code Oracle (TopoSlice).
3
+ Entrypoint for the `code-oracle` command.
4
+ """
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from code_oracle import __version__
13
+ from code_oracle.config import load_config, resolve_workspace_root, set_enabled, set_mode
14
+ from code_oracle.engine import TopoSliceEngine
15
+ from code_oracle.hook import (
16
+ format_hook_output,
17
+ get_hook_status,
18
+ install_hook,
19
+ run_hook_verification,
20
+ uninstall_hook,
21
+ )
22
+ from code_oracle.indexer import WorkspaceIndexer
23
+ from code_oracle.linearizer import linearize_subgraph
24
+ from code_oracle.locator import locate_affected_symbols
25
+ from code_oracle.models import GateResult, PatchResult
26
+ from code_oracle.server import run_server
27
+ from code_oracle.slicer import slice_neighborhood
28
+
29
+
30
+ def format_report_pretty(report_dict: dict) -> str:
31
+ """Format verification report into clean, readable terminal output."""
32
+ status = report_dict["status"]
33
+ conf = report_dict["confidence"]
34
+ latency = report_dict["latency_ms"]
35
+
36
+ color_prefix = "\033[92m" if status == "APPROVED" else "\033[91m"
37
+ color_reset = "\033[0m"
38
+
39
+ risk = report_dict.get("risk_score")
40
+ risk_str = f" | Risk Score: {risk:.4f}" if risk is not None else ""
41
+ unc = report_dict.get("epistemic_uncertainty")
42
+ unc_str = f" | Uncertainty: {unc:.4f}" if unc is not None else ""
43
+ engine_mode = report_dict.get("engine_mode")
44
+ engine_str = f" [Engine: {engine_mode}]" if engine_mode else ""
45
+
46
+ lines = [
47
+ f"{color_prefix}===================================================={color_reset}",
48
+ f"{color_prefix} VERDICT: {status} (Confidence: {conf}{risk_str}{unc_str}){engine_str} in {latency} ms{color_reset}",
49
+ f"{color_prefix}===================================================={color_reset}",
50
+ ]
51
+
52
+ active_cats = report_dict.get("active_risk_categories", [])
53
+ if active_cats:
54
+ lines.append("\nActive Risk Categories:")
55
+ for cat in active_cats:
56
+ lines.append(f" \033[93m⚠\033[0m {cat}")
57
+
58
+ violations = report_dict.get("invariant_violations", [])
59
+ if violations:
60
+ lines.append("\nViolations:")
61
+ for v in violations:
62
+ lines.append(f" \033[91m✖\033[0m {v}")
63
+
64
+ cycles = report_dict.get("cycles_detected", [])
65
+ if cycles:
66
+ lines.append("\nCycles Detected:")
67
+ for c in cycles:
68
+ lines.append(f" \033[93m↺\033[0m {' -> '.join(c)} -> {c[0] if c else ''}")
69
+
70
+ dsl = report_dict.get("linearized_subgraph", "")
71
+ if dsl:
72
+ lines.append("\nLinearized Subgraph (< 400 tokens):")
73
+ lines.append("----------------------------------------------------")
74
+ lines.append(dsl)
75
+ lines.append("----------------------------------------------------")
76
+
77
+ return "\n".join(lines)
78
+
79
+
80
+ def cmd_verify(args: argparse.Namespace) -> int:
81
+ """Execute patch verification."""
82
+ file_path = args.file
83
+ patch_content = ""
84
+
85
+ if args.patch == "-":
86
+ patch_content = sys.stdin.read()
87
+ elif args.patch:
88
+ # Check if patch is a file path or direct string
89
+ p_path = Path(args.patch)
90
+ if p_path.is_file():
91
+ patch_content = p_path.read_text(encoding="utf-8")
92
+ elif args.workspace and (Path(args.workspace) / args.patch).is_file():
93
+ patch_content = (Path(args.workspace) / args.patch).read_text(encoding="utf-8")
94
+ else:
95
+ patch_content = args.patch
96
+ elif not sys.stdin.isatty():
97
+ # Read from stdin
98
+ patch_content = sys.stdin.read()
99
+ else:
100
+ print("Error: No patch content provided. Use --patch <file_or_diff> or pipe via stdin.", file=sys.stderr)
101
+ return 2
102
+
103
+ engine = TopoSliceEngine(
104
+ workspace_root=Path(args.workspace) if args.workspace else None,
105
+ enable_neural=getattr(args, "neural", None),
106
+ )
107
+ report = engine.verify(
108
+ file_path=file_path,
109
+ patch_content=patch_content,
110
+ k=args.k,
111
+ taxonomy_threshold=getattr(args, "taxonomy_threshold", 0.5),
112
+ )
113
+ report_dict = report.to_dict()
114
+
115
+ if args.json:
116
+ print(json.dumps(report_dict, indent=2))
117
+ else:
118
+ print(format_report_pretty(report_dict))
119
+
120
+ return 0 if report.status == "APPROVED" else 1
121
+
122
+
123
+ def cmd_index(args: argparse.Namespace) -> int:
124
+ """Index workspace symbols into .code_oracle/index.json."""
125
+ ws = Path(args.workspace) if args.workspace else Path.cwd()
126
+ indexer = WorkspaceIndexer(workspace_root=ws)
127
+ stats = indexer.scan_workspace(force=args.force)
128
+
129
+ if args.json:
130
+ print(json.dumps(stats, indent=2))
131
+ else:
132
+ print(f"Workspace indexed successfully: {ws}")
133
+ print(f" Scanned files: {stats['scanned']}")
134
+ print(f" Reindexed files: {stats['reindexed']}")
135
+ print(f" Symbols indexed: {stats['symbols_indexed']}")
136
+ print(f" Latency: {stats['latency_ms']:.2f} ms")
137
+ print(f" Index location: {indexer.index_file}")
138
+
139
+ return 0
140
+
141
+
142
+ def cmd_slice(args: argparse.Namespace) -> int:
143
+ """Extract k-hop subgraph slice for a symbol."""
144
+ ws = Path(args.workspace) if args.workspace else Path.cwd()
145
+ indexer = WorkspaceIndexer(workspace_root=ws)
146
+ indexer.scan_workspace()
147
+
148
+ symbol = indexer.get_definition(args.symbol)
149
+ if not symbol:
150
+ # Search by file and name
151
+ matches = [
152
+ s for s in indexer.get_file_symbols(args.file)
153
+ if s.name == args.symbol or s.qualname == args.symbol
154
+ ]
155
+ if matches:
156
+ symbol = matches[0]
157
+
158
+ if not symbol:
159
+ err_msg = f"Error: Symbol '{args.symbol}' not found in {args.file} or workspace."
160
+ if getattr(args, "json", False):
161
+ print(json.dumps({"error": err_msg}, indent=2))
162
+ else:
163
+ print(err_msg, file=sys.stderr)
164
+ return 1
165
+
166
+ graph = slice_neighborhood(seeds=[symbol], indexer=indexer, k=args.k)
167
+ dummy_patch = PatchResult(
168
+ file_path=args.file,
169
+ original_content="",
170
+ patched_content="",
171
+ affected_symbols=[symbol],
172
+ )
173
+ dummy_gate = GateResult(status="APPROVED", confidence=1.0)
174
+ dsl = linearize_subgraph(dummy_patch, graph, dummy_gate)
175
+
176
+ if getattr(args, "json", False):
177
+ slice_data = {
178
+ "symbol": symbol.qualname,
179
+ "file": symbol.file_path,
180
+ "k": args.k,
181
+ "truncated": graph.truncated,
182
+ "nodes": [
183
+ {
184
+ "id": n.id,
185
+ "name": n.name,
186
+ "file_path": n.file_path,
187
+ "kind": n.kind,
188
+ "signature": n.signature,
189
+ "is_seed": n.is_seed,
190
+ "is_modified": n.is_modified,
191
+ }
192
+ for n in graph.nodes.values()
193
+ ],
194
+ "edges": [
195
+ {"source": e.source, "target": e.target, "relation": e.relation}
196
+ for e in graph.edges
197
+ ],
198
+ "linearized_subgraph": dsl,
199
+ }
200
+ print(json.dumps(slice_data, indent=2))
201
+ else:
202
+ print(dsl)
203
+
204
+ return 0
205
+
206
+
207
+ def cmd_clean(args: argparse.Namespace) -> int:
208
+ """Safely remove .code_oracle/ cache directory (Rollback Resilience)."""
209
+ ws = Path(args.workspace) if args.workspace else Path.cwd()
210
+ indexer = WorkspaceIndexer(workspace_root=ws)
211
+ cleaned = indexer.clean()
212
+ if getattr(args, "json", False):
213
+ print(json.dumps({"cleaned": cleaned, "workspace": str(ws)}, indent=2))
214
+ else:
215
+ if cleaned:
216
+ print(f"Code Oracle cache cleaned successfully from {ws}")
217
+ else:
218
+ print(f"Notice: Cache directory not found or already clean in {ws}")
219
+ return 0
220
+
221
+
222
+ def cmd_dead_code(args: argparse.Namespace) -> int:
223
+ """Execute dead code and orphan symbol detection."""
224
+ from code_oracle.dead_code import detect_dead_code
225
+
226
+ ws = Path(args.workspace) if args.workspace else Path.cwd()
227
+ report = detect_dead_code(
228
+ workspace_root=ws,
229
+ paths=args.paths if args.paths else None,
230
+ min_lines=args.min_lines,
231
+ include_unexported=args.include_unexported,
232
+ semantic=getattr(args, "semantic", False),
233
+ suppress_api=getattr(args, "suppress_api", False),
234
+ )
235
+
236
+ fmt = "json" if getattr(args, "json", False) else args.format
237
+
238
+ if fmt == "json":
239
+ print(json.dumps(report.to_dict(), indent=2))
240
+ elif fmt == "text":
241
+ print(report.format_text())
242
+ else: # "table"
243
+ print(report.format_table())
244
+
245
+ return 1 if report.dead_symbols_count > 0 else 0
246
+
247
+
248
+ def cmd_perf_lint(args: argparse.Namespace) -> int:
249
+ """Execute performance anti-pattern and resource leak scan."""
250
+ from code_oracle.perf_lint import lint_performance
251
+
252
+ ws = Path(args.workspace) if args.workspace else Path.cwd()
253
+ max_depth = getattr(args, "max_loop_depth", None)
254
+ if max_depth is None:
255
+ max_depth = getattr(args, "max_depth", 2)
256
+
257
+ report = lint_performance(
258
+ workspace_root=ws,
259
+ paths=args.paths if args.paths else None,
260
+ severity=args.severity,
261
+ max_depth=max_depth,
262
+ )
263
+
264
+ fmt = "json" if getattr(args, "json", False) else args.format
265
+
266
+ if fmt == "json":
267
+ print(json.dumps(report.to_dict(), indent=2))
268
+ elif fmt == "text":
269
+ print(report.format_text())
270
+ else: # "table"
271
+ print(report.format_table())
272
+
273
+ fail_on = args.fail_on.lower() if args.fail_on else "error"
274
+ if fail_on == "none":
275
+ return 0
276
+ elif fail_on == "warn":
277
+ return 1 if (report.warnings_count > 0 or report.errors_count > 0) else 0
278
+ else: # "error"
279
+ return 1 if report.errors_count > 0 else 0
280
+
281
+
282
+ def cmd_serve(args: argparse.Namespace) -> int:
283
+ """Launch the FastMCP server."""
284
+ run_server()
285
+ return 0
286
+
287
+
288
+ def cmd_hook_install(args: argparse.Namespace) -> int:
289
+ """Safely install git pre-commit hook non-destructively."""
290
+ ws = Path(args.workspace) if args.workspace else None
291
+ success, msg = install_hook(
292
+ workspace_root=ws,
293
+ hook_name=args.hook,
294
+ mode=args.mode,
295
+ )
296
+ if getattr(args, "json", False):
297
+ status_info = get_hook_status(ws)
298
+ status_info["success"] = success
299
+ status_info["message"] = msg
300
+ print(json.dumps(status_info, indent=2))
301
+ else:
302
+ print(msg)
303
+ return 0 if success else 1
304
+
305
+
306
+ def cmd_hook_uninstall(args: argparse.Namespace) -> int:
307
+ """Safely uninstall git hook (Rollback Resilience)."""
308
+ ws = Path(args.workspace) if args.workspace else None
309
+ success, msg = uninstall_hook(
310
+ workspace_root=ws,
311
+ hook_name=args.hook,
312
+ )
313
+ if getattr(args, "json", False):
314
+ status_info = get_hook_status(ws)
315
+ status_info["success"] = success
316
+ status_info["message"] = msg
317
+ print(json.dumps(status_info, indent=2))
318
+ else:
319
+ print(msg)
320
+ return 0
321
+
322
+
323
+ def cmd_hook_on(args: argparse.Namespace) -> int:
324
+ """Enable Code Oracle pre-commit hook."""
325
+ ws = Path(args.workspace) if args.workspace else None
326
+ cfg = set_enabled(ws, True)
327
+ target_ws = resolve_workspace_root(ws)
328
+ if getattr(args, "json", False):
329
+ print(json.dumps({"enabled": True, "mode": cfg.get("mode", "block"), "workspace": str(target_ws)}, indent=2))
330
+ else:
331
+ print("Code Oracle hook enabled.")
332
+ return 0
333
+
334
+
335
+ def cmd_hook_off(args: argparse.Namespace) -> int:
336
+ """Disable Code Oracle pre-commit hook."""
337
+ ws = Path(args.workspace) if args.workspace else None
338
+ cfg = set_enabled(ws, False)
339
+ target_ws = resolve_workspace_root(ws)
340
+ if getattr(args, "json", False):
341
+ print(json.dumps({"enabled": False, "mode": cfg.get("mode", "block"), "workspace": str(target_ws)}, indent=2))
342
+ else:
343
+ print("Code Oracle hook disabled.")
344
+ return 0
345
+
346
+
347
+ def cmd_hook_mode(args: argparse.Namespace) -> int:
348
+ """Switch hook mode between 'block' and 'warn'."""
349
+ ws = Path(args.workspace) if args.workspace else None
350
+ cfg = set_mode(ws, args.mode)
351
+ target_ws = resolve_workspace_root(ws)
352
+ if getattr(args, "json", False):
353
+ print(json.dumps({"enabled": cfg.get("enabled", True), "mode": args.mode, "workspace": str(target_ws)}, indent=2))
354
+ else:
355
+ print(f"Code Oracle hook mode set to '{args.mode}'.")
356
+ return 0
357
+
358
+
359
+ def cmd_hook_status(args: argparse.Namespace) -> int:
360
+ """Show hook installation and configuration status."""
361
+ ws = Path(args.workspace) if args.workspace else None
362
+ status_info = get_hook_status(ws)
363
+ if getattr(args, "json", False):
364
+ print(json.dumps(status_info, indent=2))
365
+ else:
366
+ git_str = f"Yes ({status_info['workspace']})" if status_info["is_git_repo"] else "No"
367
+ inst_list = []
368
+ if status_info["pre_commit_installed"]:
369
+ inst_list.append("pre-commit: installed")
370
+ else:
371
+ inst_list.append("pre-commit: not installed")
372
+ if status_info["pre_push_installed"]:
373
+ inst_list.append("pre-push: installed")
374
+ else:
375
+ inst_list.append("pre-push: not installed")
376
+ inst_str = f"Yes ({', '.join(inst_list)})" if status_info["installed"] else f"No ({', '.join(inst_list)})"
377
+ state_str = "ENABLED" if status_info["enabled"] else "DISABLED"
378
+ mode_desc = (
379
+ f"{status_info['mode']} (strict exit code 1)"
380
+ if status_info["mode"] == "block"
381
+ else f"{status_info['mode']} (advisory exit code 0)"
382
+ )
383
+
384
+ print("Code Oracle Hook Status:")
385
+ print(f" Git Repository: {git_str}")
386
+ print(f" Hook Installed: {inst_str}")
387
+ print(f" Hook State: {state_str}")
388
+ print(f" Hook Mode: {mode_desc}")
389
+ print(f" Config File: {status_info['config_file']}")
390
+ return 0
391
+
392
+
393
+ def cmd_hook_run(args: argparse.Namespace) -> int:
394
+ """Execute pre-commit verification on staged files."""
395
+ ws = Path(args.workspace) if args.workspace else None
396
+ result = run_hook_verification(
397
+ workspace_root=ws,
398
+ mode_override=args.mode,
399
+ k=args.k,
400
+ files=args.files,
401
+ )
402
+ if getattr(args, "json", False):
403
+ print(json.dumps(result, indent=2))
404
+ else:
405
+ print(format_hook_output(result))
406
+ return result["exit_code"]
407
+
408
+
409
+ def cmd_dataset(args: argparse.Namespace) -> int:
410
+ """Execute dataset mining and synthetic generation."""
411
+ from code_oracle.dataset import DatasetGenerator
412
+
413
+ lang_list = [l.strip().lower() for l in args.languages.split(",") if l.strip()]
414
+ generator = DatasetGenerator(
415
+ languages=lang_list,
416
+ seed=args.seed,
417
+ positive_label=args.positive_label,
418
+ negative_label=args.negative_label,
419
+ filter_symbolic_gate=getattr(args, "filter_symbolic_gate", False),
420
+ )
421
+
422
+ out_dir = Path(args.output_dir)
423
+ train_count, val_count = generator.generate_and_export(
424
+ output_dir=out_dir,
425
+ num_samples=args.num_samples,
426
+ val_ratio=args.val_ratio,
427
+ repo_path=Path(args.repo) if args.repo else None,
428
+ include_subtle=getattr(args, "include_subtle", True),
429
+ )
430
+
431
+ if args.json:
432
+ print(json.dumps({
433
+ "status": "SUCCESS",
434
+ "output_dir": str(out_dir),
435
+ "train_samples": train_count,
436
+ "val_samples": val_count,
437
+ "total_samples": train_count + val_count,
438
+ "languages": lang_list,
439
+ }, indent=2))
440
+ else:
441
+ print(f"Generated {train_count} train samples, {val_count} val samples into {out_dir}")
442
+
443
+ return 0
444
+
445
+
446
+ def cmd_export_onnx(args: argparse.Namespace) -> int:
447
+ """Export ModernBERT PyTorch weights to ONNX FP32 and dynamic INT8 formats."""
448
+ from code_oracle.export_onnx import export_and_quantize
449
+
450
+ weights_dir = args.weights
451
+ if not weights_dir:
452
+ cand = Path.cwd() / "weights_base"
453
+ if cand.exists():
454
+ weights_dir = str(cand)
455
+ else:
456
+ from code_oracle.decision import LayaDecisionHead
457
+ head = LayaDecisionHead(enabled=True)
458
+ if head.weights_path:
459
+ weights_dir = str(head.weights_path)
460
+ else:
461
+ print("Error: Could not resolve model weights directory. Use --weights <path>.", file=sys.stderr)
462
+ return 1
463
+
464
+ out_dir = args.output_dir or weights_dir
465
+
466
+ try:
467
+ summary = export_and_quantize(
468
+ weights_path=weights_dir,
469
+ output_dir=out_dir,
470
+ quantize_int8=args.quantize_int8,
471
+ verify_parity=args.verify_parity,
472
+ opset_version=args.opset,
473
+ )
474
+ if args.json:
475
+ print(json.dumps(summary, indent=2))
476
+ else:
477
+ print("====================================================")
478
+ print(" ONNX EXPORT & QUANTIZATION REPORT")
479
+ print("====================================================")
480
+ print(f" Output Directory: {summary['output_dir']}")
481
+ print(f" Model ONNX (FP32): {summary['model_onnx']} ({summary['fp32_size_mb']:.2f} MB)")
482
+ if summary.get("model_int8_onnx"):
483
+ print(f" Model ONNX (INT8): {summary['model_int8_onnx']} ({summary['int8_size_mb']:.2f} MB)")
484
+ print(f" Total Elapsed Time: {summary['elapsed_seconds']:.2f} s")
485
+ parity = summary.get("parity")
486
+ if parity:
487
+ print("----------------------------------------------------")
488
+ print(f" Parity Status: {parity.get('status', 'UNKNOWN')}")
489
+ print(f" FP32 Parity Pass: {parity.get('fp32_parity_pass')}")
490
+ print(f" Max FP32 Risk Diff: {parity.get('max_fp32_risk_diff'):.6e}")
491
+ if summary.get("model_int8_onnx"):
492
+ print(f" INT8 Parity Pass: {parity.get('int8_parity_pass')}")
493
+ print(f" Max INT8 Risk Diff: {parity.get('max_int8_risk_diff'):.6f}")
494
+ print(f" Samples Evaluated: {parity.get('samples_tested')}")
495
+ print("====================================================")
496
+ return 0
497
+ except Exception as exc:
498
+ if args.json:
499
+ print(json.dumps({"status": "FAILED", "error": str(exc)}, indent=2))
500
+ else:
501
+ print(f"Error during ONNX export: {exc}", file=sys.stderr)
502
+ return 1
503
+
504
+
505
+ def build_parser() -> argparse.ArgumentParser:
506
+ """Build the CLI argument parser."""
507
+ parser = argparse.ArgumentParser(
508
+ prog="code-oracle",
509
+ description="Sub-50ms Neuro-Symbolic Verification Oracle for AI Coding Agents.",
510
+ )
511
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
512
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
513
+
514
+ # verify
515
+ p_verify = subparsers.add_parser("verify", help="Verify a code patch proposal")
516
+ p_verify.add_argument("file", help="Target source file being patched")
517
+ p_verify.add_argument("--patch", "-p", help="Patch diff string or path to diff file (use '-' for stdin)")
518
+ p_verify.add_argument("--k", type=int, default=1, help="k-hop neighborhood radius (default: 1)")
519
+ p_verify.add_argument("--workspace", "-w", help="Workspace root directory")
520
+ p_verify.add_argument(
521
+ "--neural",
522
+ dest="neural",
523
+ action="store_true",
524
+ default=None,
525
+ help="Enable Laya ModernBERT neural decision head and risk calibration",
526
+ )
527
+ p_verify.add_argument(
528
+ "--no-neural",
529
+ dest="neural",
530
+ action="store_false",
531
+ help="Disable Laya ModernBERT neural decision head (pure symbolic mode)",
532
+ )
533
+ p_verify.add_argument(
534
+ "--taxonomy-threshold",
535
+ type=float,
536
+ default=0.5,
537
+ help="Probability threshold to activate risk taxonomy categories (default: 0.5)",
538
+ )
539
+ p_verify.add_argument("--json", action="store_true", help="Output machine-readable JSON")
540
+ p_verify.set_defaults(func=cmd_verify)
541
+
542
+ # index
543
+ p_index = subparsers.add_parser("index", help="Index workspace symbols into .code_oracle/index.json")
544
+ p_index.add_argument("workspace", nargs="?", default=".", help="Workspace root directory")
545
+ p_index.add_argument("--force", "-f", action="store_true", help="Force re-indexing of all files")
546
+ p_index.add_argument("--json", action="store_true", help="Output machine-readable JSON")
547
+ p_index.set_defaults(func=cmd_index)
548
+
549
+ # slice
550
+ p_slice = subparsers.add_parser("slice", help="Extract k-hop neighborhood slice for a symbol")
551
+ p_slice.add_argument("file", help="Source file containing the symbol")
552
+ p_slice.add_argument("--symbol", "-s", required=True, help="Target symbol name")
553
+ p_slice.add_argument("--k", type=int, default=1, help="k-hop depth (1 or 2)")
554
+ p_slice.add_argument("--workspace", "-w", help="Workspace root directory")
555
+ p_slice.add_argument("--json", action="store_true", help="Output machine-readable JSON")
556
+ p_slice.set_defaults(func=cmd_slice)
557
+
558
+ # clean
559
+ p_clean = subparsers.add_parser("clean", help="Clean .code_oracle/ cache (Rollback Resilience)")
560
+ p_clean.add_argument("workspace", nargs="?", default=".", help="Workspace root directory")
561
+ p_clean.add_argument("--json", action="store_true", help="Output machine-readable JSON")
562
+ p_clean.set_defaults(func=cmd_clean)
563
+
564
+ # dead-code
565
+ p_dead = subparsers.add_parser(
566
+ "dead-code",
567
+ help="Detect unreachable and orphan symbols across workspace",
568
+ )
569
+ p_dead.add_argument(
570
+ "paths",
571
+ nargs="*",
572
+ default=[],
573
+ help="Optional target files or directories to filter report (default: workspace root)",
574
+ )
575
+ p_dead.add_argument("--workspace", "-w", help="Workspace root directory")
576
+ p_dead.add_argument(
577
+ "--format",
578
+ "-f",
579
+ choices=["table", "json", "text"],
580
+ default="table",
581
+ help="Output format (table, json, text)",
582
+ )
583
+ p_dead.add_argument(
584
+ "--min-lines",
585
+ type=int,
586
+ default=0,
587
+ help="Minimum line count threshold for reporting dead code (default: 0)",
588
+ )
589
+ p_dead.add_argument(
590
+ "--include-unexported",
591
+ action="store_true",
592
+ default=False,
593
+ help="Include unexported (private) symbols in dead code detection",
594
+ )
595
+ p_dead.add_argument(
596
+ "--semantic",
597
+ action="store_true",
598
+ default=False,
599
+ help="Enable embedded dead code semantics classification (Stage 1 pruner + Stage 2 classifier)",
600
+ )
601
+ p_dead.add_argument(
602
+ "--suppress-api",
603
+ "--suppress-public-api",
604
+ dest="suppress_api",
605
+ action="store_true",
606
+ default=False,
607
+ help="Suppress public library API surfaces from dead code report",
608
+ )
609
+ p_dead.add_argument(
610
+ "--json",
611
+ action="store_true",
612
+ help="Output machine-readable JSON (alias for --format json)",
613
+ )
614
+ p_dead.set_defaults(func=cmd_dead_code)
615
+
616
+ # perf-lint
617
+ p_perf = subparsers.add_parser(
618
+ "perf-lint",
619
+ help="Detect performance anti-patterns and resource leaks across workspace",
620
+ )
621
+ p_perf.add_argument(
622
+ "paths",
623
+ nargs="*",
624
+ default=[],
625
+ help="Optional target files or directories to lint (default: workspace root)",
626
+ )
627
+ p_perf.add_argument("--workspace", "-w", help="Workspace root directory")
628
+ p_perf.add_argument(
629
+ "--format",
630
+ "-f",
631
+ choices=["table", "json", "text"],
632
+ default="table",
633
+ help="Output format (table, json, text)",
634
+ )
635
+ p_perf.add_argument(
636
+ "--severity",
637
+ choices=["warn", "error"],
638
+ default="warn",
639
+ help="Minimum severity threshold to report (warn or error, default: warn)",
640
+ )
641
+ p_perf.add_argument(
642
+ "--max-loop-depth",
643
+ "--max-depth",
644
+ dest="max_loop_depth",
645
+ type=int,
646
+ default=2,
647
+ help="Loop depth threshold for PERF001 reporting (default: 2)",
648
+ )
649
+ p_perf.add_argument(
650
+ "--fail-on",
651
+ choices=["warn", "error", "none"],
652
+ default="error",
653
+ help="Exit with non-zero exit code if findings meet threshold (default: error)",
654
+ )
655
+ p_perf.add_argument(
656
+ "--json",
657
+ action="store_true",
658
+ help="Output machine-readable JSON (alias for --format json)",
659
+ )
660
+ p_perf.set_defaults(func=cmd_perf_lint)
661
+
662
+ # serve
663
+ p_serve = subparsers.add_parser("serve", help="Run the Lean FastMCP server")
664
+ p_serve.set_defaults(func=cmd_serve)
665
+
666
+ # dataset
667
+ p_dataset = subparsers.add_parser("dataset", help="Mine and generate multi-language training datasets")
668
+ p_dataset.add_argument("--repo", "-r", help="Path to existing repository to mine")
669
+ p_dataset.add_argument("--output-dir", "-o", default="./dataset_output", help="Output directory for JSONL datasets")
670
+ p_dataset.add_argument("--num-samples", "-n", type=int, default=100, help="Target total samples")
671
+ p_dataset.add_argument("--val-ratio", type=float, default=0.2, help="Validation ratio")
672
+ p_dataset.add_argument("--languages", "-l", default="python,typescript,go,rust", help="Comma-separated languages")
673
+ p_dataset.add_argument("--seed", type=int, default=42, help="Random seed")
674
+ p_dataset.add_argument("--positive-label", type=int, default=1, help="Positive label (default: 1)")
675
+ p_dataset.add_argument("--negative-label", type=int, default=0, help="Negative label (default: 0)")
676
+ p_dataset.add_argument("--filter-symbolic-gate", action="store_true", default=False, help="Filter out mutations that fail symbolic gate")
677
+ p_dataset.add_argument("--include-subtle", action="store_true", default=True, help="Include subtle gray-area semantic mutations across risk taxonomy")
678
+ p_dataset.add_argument("--no-subtle", dest="include_subtle", action="store_false", help="Disable subtle gray-area semantic mutations")
679
+ p_dataset.add_argument("--json", action="store_true", help="Output machine-readable JSON")
680
+ p_dataset.set_defaults(func=cmd_dataset)
681
+
682
+ # hook
683
+ p_hook = subparsers.add_parser("hook", help="Git pre-commit hook and toggle system")
684
+ p_hook_sub = p_hook.add_subparsers(dest="hook_command", help="Hook subcommands")
685
+
686
+ # hook install
687
+ p_h_install = p_hook_sub.add_parser("install", help="Safely install git pre-commit hook")
688
+ p_h_install.add_argument("--workspace", "-w", help="Workspace root directory")
689
+ p_h_install.add_argument("--mode", choices=["block", "warn"], default=None, help="Hook mode (block or warn)")
690
+ p_h_install.add_argument("--hook", choices=["pre-commit", "pre-push"], default="pre-commit", help="Target hook (default: pre-commit)")
691
+ p_h_install.add_argument("--json", action="store_true", help="Output machine-readable JSON")
692
+ p_h_install.set_defaults(func=cmd_hook_install)
693
+
694
+ # hook uninstall
695
+ p_h_uninstall = p_hook_sub.add_parser("uninstall", help="Safely uninstall git hook (Rollback Resilience)")
696
+ p_h_uninstall.add_argument("--workspace", "-w", help="Workspace root directory")
697
+ p_h_uninstall.add_argument("--hook", choices=["pre-commit", "pre-push"], default=None, help="Target hook (default: all installed)")
698
+ p_h_uninstall.add_argument("--json", action="store_true", help="Output machine-readable JSON")
699
+ p_h_uninstall.set_defaults(func=cmd_hook_uninstall)
700
+
701
+ # hook on / enable
702
+ p_h_on = p_hook_sub.add_parser("on", aliases=["enable"], help="Enable pre-commit verification")
703
+ p_h_on.add_argument("--workspace", "-w", help="Workspace root directory")
704
+ p_h_on.add_argument("--json", action="store_true", help="Output machine-readable JSON")
705
+ p_h_on.set_defaults(func=cmd_hook_on)
706
+
707
+ # hook off / disable
708
+ p_h_off = p_hook_sub.add_parser("off", aliases=["disable"], help="Disable pre-commit verification")
709
+ p_h_off.add_argument("--workspace", "-w", help="Workspace root directory")
710
+ p_h_off.add_argument("--json", action="store_true", help="Output machine-readable JSON")
711
+ p_h_off.set_defaults(func=cmd_hook_off)
712
+
713
+ # hook mode
714
+ p_h_mode = p_hook_sub.add_parser("mode", help="Switch mode between block and warn")
715
+ p_h_mode.add_argument("mode", choices=["block", "warn"], help="Hook mode: 'block' (strict exit 1) or 'warn' (advisory exit 0)")
716
+ p_h_mode.add_argument("--workspace", "-w", help="Workspace root directory")
717
+ p_h_mode.add_argument("--json", action="store_true", help="Output machine-readable JSON")
718
+ p_h_mode.set_defaults(func=cmd_hook_mode)
719
+
720
+ # hook status
721
+ p_h_status = p_hook_sub.add_parser("status", help="Show hook installation and configuration status")
722
+ p_h_status.add_argument("--workspace", "-w", help="Workspace root directory")
723
+ p_h_status.add_argument("--json", action="store_true", help="Output machine-readable JSON")
724
+ p_h_status.set_defaults(func=cmd_hook_status)
725
+
726
+ # hook run
727
+ p_h_run = p_hook_sub.add_parser("run", help="Run pre-commit hook verification")
728
+ p_h_run.add_argument("files", nargs="*", default=[], help="Optional files to verify")
729
+ p_h_run.add_argument("--workspace", "-w", help="Workspace root directory")
730
+ p_h_run.add_argument("--mode", choices=["block", "warn"], default=None, help="Override mode (block or warn)")
731
+ p_h_run.add_argument("--k", type=int, default=1, help="k-hop neighborhood radius (default: 1)")
732
+ p_h_run.add_argument("--json", action="store_true", help="Output machine-readable JSON")
733
+ p_h_run.set_defaults(func=cmd_hook_run)
734
+
735
+ # export-onnx
736
+ p_export = subparsers.add_parser(
737
+ "export-onnx",
738
+ help="Export ModernBERT PyTorch weights to ONNX FP32 and dynamic INT8 formats",
739
+ )
740
+ p_export.add_argument(
741
+ "--weights",
742
+ "-w",
743
+ default=None,
744
+ help="Source directory containing PyTorch model weights (default: weights_base or auto-resolved)",
745
+ )
746
+ p_export.add_argument(
747
+ "--output-dir",
748
+ "-o",
749
+ default=None,
750
+ help="Destination directory for exported ONNX models (default: weights directory)",
751
+ )
752
+ p_export.add_argument(
753
+ "--no-int8",
754
+ dest="quantize_int8",
755
+ action="store_false",
756
+ default=True,
757
+ help="Skip dynamic INT8 quantization",
758
+ )
759
+ p_export.add_argument(
760
+ "--no-verify",
761
+ dest="verify_parity",
762
+ action="store_false",
763
+ default=True,
764
+ help="Skip numeric parity verification against PyTorch",
765
+ )
766
+ p_export.add_argument(
767
+ "--opset",
768
+ type=int,
769
+ default=17,
770
+ help="ONNX opset version (default: 17)",
771
+ )
772
+ p_export.add_argument(
773
+ "--json",
774
+ action="store_true",
775
+ help="Output machine-readable JSON",
776
+ )
777
+ p_export.set_defaults(func=cmd_export_onnx)
778
+
779
+ return parser
780
+
781
+
782
+ def main() -> None:
783
+ """CLI entrypoint."""
784
+ parser = build_parser()
785
+ args = parser.parse_args()
786
+
787
+ if not hasattr(args, "func"):
788
+ parser.print_help(sys.stderr)
789
+ sys.exit(2)
790
+
791
+ sys.exit(args.func(args))
792
+
793
+
794
+ if __name__ == "__main__":
795
+ main()