algorithm-discovery-engine 1.0.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.
gui/__main__.py ADDED
@@ -0,0 +1,26 @@
1
+ """``python -m gui`` launcher. Runs without a display only when ``--selftest``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+
8
+ def main() -> int:
9
+ if "--selftest" in sys.argv:
10
+ from gui import core
11
+
12
+ print(f"gui selftest ok (catalog: {core.engine_catalog_counts()})")
13
+ return 0
14
+ try:
15
+ import tkinter # noqa: F401
16
+ except ImportError:
17
+ print("tkinter is not available in this Python build.", file=sys.stderr)
18
+ return 2
19
+ from gui.app import main as app_main
20
+
21
+ app_main()
22
+ return 0
23
+
24
+
25
+ if __name__ == "__main__":
26
+ raise SystemExit(main())
gui/app.py ADDED
@@ -0,0 +1,407 @@
1
+ """Tkinter desktop GUI for algorithm-discovery-engine.
2
+
3
+ Launch with ``python -m gui`` (or ``python engine/runner.py gui``). The UI is
4
+ pure stdlib — no third-party runtime dependencies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import queue
11
+ import subprocess
12
+ import sys
13
+ import threading
14
+ import tkinter as tk
15
+ import webbrowser
16
+ from collections.abc import Callable
17
+ from tkinter import ttk
18
+ from typing import Any, Literal
19
+
20
+ from gui import core
21
+
22
+ BG = "#0d1117"
23
+ PANEL = "#161b22"
24
+ BORDER = "#30363d"
25
+ TEXT = "#e6edf3"
26
+ MUTED = "#8b949e"
27
+ ACCENT = "#1f6feb"
28
+ GREEN = "#3fb950"
29
+ RED = "#f85149"
30
+
31
+ Report = dict[str, Any]
32
+ Message = tuple[Callable[[Any], None], Any]
33
+
34
+
35
+ class App:
36
+ """Main application window."""
37
+
38
+ def __init__(self, root: tk.Tk) -> None:
39
+ self.root = root
40
+ self.root.title("algorithm-discovery-engine")
41
+ self.root.geometry("980x640")
42
+ self.root.minsize(800, 540)
43
+ self.root.configure(bg=BG)
44
+ self._style()
45
+ self._messages: queue.Queue[Message] = queue.Queue()
46
+ self._busy = False
47
+ self._tasks: list[ttk.Button] = []
48
+ self.root.after(100, self._poll)
49
+ self._build()
50
+
51
+ # ------------------------------------------------------------------ style
52
+ def _style(self) -> None:
53
+ style = ttk.Style(self.root)
54
+ with contextlib.suppress(tk.TclError):
55
+ style.theme_use("clam")
56
+ style.configure(
57
+ "TNotebook", background=BG, borderwidth=0, tabmargins=(4, 4, 4, 0)
58
+ )
59
+ style.configure(
60
+ "TNotebook.Tab",
61
+ background=PANEL, foreground=MUTED, padding=(14, 8), borderwidth=0,
62
+ )
63
+ style.map(
64
+ "TNotebook.Tab",
65
+ background=[("selected", ACCENT)],
66
+ foreground=[("selected", "#ffffff")],
67
+ )
68
+ style.configure("TFrame", background=BG)
69
+ style.configure(
70
+ "Treeview",
71
+ background=PANEL, fieldbackground=PANEL, foreground=TEXT,
72
+ borderwidth=0, rowheight=24,
73
+ )
74
+ style.configure(
75
+ "Treeview.Heading",
76
+ background=BORDER, foreground=TEXT, relief="flat", padding=(6, 5),
77
+ )
78
+ style.map(
79
+ "Treeview",
80
+ background=[("selected", ACCENT)],
81
+ foreground=[("selected", "#ffffff")],
82
+ )
83
+ style.configure("TLabel", background=BG, foreground=TEXT)
84
+ style.configure("Muted.TLabel", background=BG, foreground=MUTED)
85
+ style.configure(
86
+ "TButton", background=PANEL, foreground=TEXT, borderwidth=1,
87
+ focusthickness=0, padding=(10, 6),
88
+ )
89
+ style.configure(
90
+ "Accent.TButton", background=ACCENT, foreground="#ffffff",
91
+ borderwidth=1, padding=(10, 6),
92
+ )
93
+ style.configure(
94
+ "TEntry", background=PANEL, foreground=TEXT, fieldbackground=PANEL,
95
+ insertcolor=TEXT, bordercolor=BORDER,
96
+ )
97
+
98
+ # ------------------------------------------------------------ background
99
+ def _button(
100
+ self,
101
+ parent: ttk.Frame,
102
+ text: str,
103
+ command: Callable[[], None],
104
+ accent: bool = False,
105
+ ) -> ttk.Button:
106
+ button = ttk.Button(
107
+ parent, text=text, command=command, takefocus=False,
108
+ style="Accent.TButton" if accent else "TButton",
109
+ )
110
+ self._tasks.append(button)
111
+ return button
112
+
113
+ def _set_busy(self, busy: bool) -> None:
114
+ self._busy = busy
115
+ for button in self._tasks:
116
+ button.configure(state=tk.DISABLED if busy else tk.NORMAL)
117
+
118
+ def _submit(
119
+ self,
120
+ task: Callable[[], Any],
121
+ on_done: Callable[[Any], None],
122
+ *labels: ttk.Label,
123
+ ) -> None:
124
+ def run() -> None:
125
+ try:
126
+ result = task()
127
+ except Exception as exc: # surfaced to the UI
128
+ result = exc
129
+ self._messages.put((on_done, result))
130
+
131
+ self._set_busy(True)
132
+ for label in labels:
133
+ label.configure(text="working…", foreground=ACCENT)
134
+ threading.Thread(target=run, daemon=True).start()
135
+
136
+ def _poll(self) -> None:
137
+ try:
138
+ while True:
139
+ callback, payload = self._messages.get_nowait()
140
+ callback(payload)
141
+ except queue.Empty:
142
+ pass
143
+ self.root.after(100, self._poll)
144
+
145
+ # ------------------------------------------------------------------ build
146
+ def _build(self) -> None:
147
+ root_frame = ttk.Frame(self.root, padding=(12, 10, 12, 8))
148
+ root_frame.pack(fill=tk.BOTH, expand=True)
149
+
150
+ self.status = ttk.Label(root_frame, text="ready", style="Muted.TLabel")
151
+ self.status.pack(side=tk.BOTTOM, anchor=tk.W, pady=(8, 0))
152
+
153
+ notebook = ttk.Notebook(root_frame)
154
+ notebook.pack(fill=tk.BOTH, expand=True)
155
+
156
+ discover = ttk.Frame(notebook, padding=10)
157
+ patterns = ttk.Frame(notebook, padding=10)
158
+ engine = ttk.Frame(notebook, padding=10)
159
+ notebook.add(discover, text=" Discover ")
160
+ notebook.add(patterns, text=" Pattern discovery ")
161
+ notebook.add(engine, text=" Engine ")
162
+
163
+ self._build_discover(discover)
164
+ self._build_patterns(patterns)
165
+ self._build_engine(engine)
166
+ self.refresh_report()
167
+
168
+ def _build_discover(self, parent: ttk.Frame) -> None:
169
+ top = ttk.Frame(parent)
170
+ top.pack(fill=tk.X, pady=(0, 8))
171
+ ttk.Label(top, text="Local algorithm synthesizer", font=("", 16, "bold")).pack(side=tk.LEFT)
172
+ self.summary_label = ttk.Label(top, text="", style="Muted.TLabel")
173
+ self.summary_label.pack(side=tk.RIGHT)
174
+
175
+ self._tree = ttk.Treeview(
176
+ parent, show="headings",
177
+ columns=("target", "kind", "status", "novelty", "ms", "strategy"),
178
+ )
179
+ tree_cols: list[tuple[str, str, int]] = [
180
+ ("target", "Target", 190),
181
+ ("kind", "Kind", 120),
182
+ ("status", "Status", 110),
183
+ ("novelty", "Novelty", 130),
184
+ ("ms", "Search (ms)", 90),
185
+ ("strategy", "Strategy", 120),
186
+ ]
187
+ for col, title, width in tree_cols:
188
+ self._tree.heading(col, text=title)
189
+ anchor: Literal["w", "center"] = (
190
+ "w" if col in ("target", "kind", "strategy") else "center"
191
+ )
192
+ self._tree.column(col, width=width, anchor=anchor)
193
+ self._tree.pack(fill=tk.BOTH, expand=True)
194
+
195
+ controls = ttk.Frame(parent)
196
+ controls.pack(fill=tk.X, pady=(8, 0))
197
+ self._button(controls, "Run smoke pass", self.run_smoke, accent=True).pack(side=tk.LEFT)
198
+ self._button(controls, "Run full pass", self.run_full).pack(side=tk.LEFT, padx=6)
199
+ self._button(controls, "Refresh", self.refresh_report).pack(side=tk.LEFT)
200
+ self._button(controls, "Open report (markdown)", self.open_report).pack(
201
+ side=tk.LEFT, padx=6
202
+ )
203
+ self._button(controls, "Open solutions folder", self.open_solutions).pack(side=tk.LEFT)
204
+
205
+ def _build_patterns(self, parent: ttk.Frame) -> None:
206
+ row = ttk.Frame(parent)
207
+ row.pack(fill=tk.X, pady=(0, 8))
208
+ ttk.Label(row, text="Integer sequence:").pack(side=tk.LEFT)
209
+ self.sequence_var = tk.StringVar(value="1 4 9 16 25")
210
+ entry = ttk.Entry(row, textvariable=self.sequence_var)
211
+ entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=8)
212
+ entry.bind("<Return>", lambda _event: self.run_patterns())
213
+ self._button(row, "Discover", self.run_patterns, accent=True).pack(side=tk.LEFT)
214
+ self.pattern_status = ttk.Label(row, text="", style="Muted.TLabel")
215
+ self.pattern_status.pack(side=tk.LEFT, padx=8)
216
+
217
+ self._ptree = ttk.Treeview(
218
+ parent, show="headings", columns=("name", "confidence", "prediction", "detail"),
219
+ )
220
+ pat_cols: list[tuple[str, str, int, Literal["w", "center"]]] = [
221
+ ("name", "Hypothesis", 200, "w"),
222
+ ("confidence", "Confidence", 130, "center"),
223
+ ("prediction", "Next term", 120, "center"),
224
+ ("detail", "Detail", 400, "w"),
225
+ ]
226
+ for col, title, width, anchor in pat_cols:
227
+ self._ptree.heading(col, text=title)
228
+ self._ptree.column(col, width=width, anchor=anchor)
229
+ self._ptree.pack(fill=tk.BOTH, expand=True)
230
+
231
+ def _build_engine(self, parent: ttk.Frame) -> None:
232
+ self.engine_summary = ttk.Label(
233
+ parent, text="", font=("", 12), style="Muted.TLabel", justify=tk.LEFT,
234
+ )
235
+ self.engine_summary.pack(anchor=tk.W, pady=(0, 10))
236
+
237
+ controls = ttk.Frame(parent)
238
+ controls.pack(fill=tk.X)
239
+ self._button(controls, "Check vectors", self.run_check, accent=True).pack(side=tk.LEFT)
240
+ self._button(controls, "Run pytest", self.run_pytest).pack(side=tk.LEFT, padx=6)
241
+ self._button(controls, "Run build", self.run_build).pack(side=tk.LEFT)
242
+ self._button(controls, "Benchmark", self.run_bench).pack(side=tk.LEFT, padx=6)
243
+ self._button(controls, "Open docs", self.open_docs).pack(side=tk.LEFT)
244
+ self._button(controls, "Open GitHub", self.open_github).pack(side=tk.LEFT, padx=6)
245
+
246
+ self.output = tk.Text(
247
+ parent, bg=PANEL, fg=TEXT, insertbackground=TEXT, relief=tk.FLAT,
248
+ wrap=tk.NONE, font=("DejaVu Sans Mono", 10), state=tk.DISABLED,
249
+ highlightthickness=1, highlightbackground=BORDER,
250
+ )
251
+ self.output.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
252
+
253
+ # ----------------------------------------------------------- discover tab
254
+ def _render_report(self, summary: dict[str, Any]) -> None:
255
+ for item in self._tree.get_children():
256
+ self._tree.delete(item)
257
+ for entry in summary.get("targets", []):
258
+ status = entry.get("status", "?")
259
+ self._tree.insert(
260
+ "", tk.END,
261
+ values=(
262
+ entry.get("id", ""),
263
+ entry.get("kind", ""),
264
+ status,
265
+ entry.get("novelty", ""),
266
+ entry.get("search_ms", ""),
267
+ entry.get("strategy", "") or entry.get("time_class", ""),
268
+ ),
269
+ tags=(status,),
270
+ )
271
+ self._tree.tag_configure("verified", foreground=GREEN)
272
+ self._tree.tag_configure("candidate-rejected", foreground=RED)
273
+ self._tree.tag_configure("no-candidate-found", foreground=MUTED)
274
+ if summary.get("exists"):
275
+ self.summary_label.configure(
276
+ text=f"verified {summary['verified']} · rejected {summary['rejected']} · "
277
+ f"missing {summary['missing']}"
278
+ )
279
+ else:
280
+ self.summary_label.configure(text="no report yet", foreground=MUTED)
281
+
282
+ def refresh_report(self) -> None:
283
+ self._render_report(core.synth_summary(load_report()))
284
+
285
+ def run_smoke(self) -> None:
286
+ self._submit(
287
+ lambda: core.run_synth(smoke=True),
288
+ self._synth_done,
289
+ self.summary_label,
290
+ )
291
+
292
+ def run_full(self) -> None:
293
+ self._submit(
294
+ lambda: core.run_synth(smoke=False),
295
+ self._synth_done,
296
+ self.summary_label,
297
+ )
298
+
299
+ def _synth_done(self, payload: Any) -> None:
300
+ self._set_busy(False)
301
+ if isinstance(payload, Exception):
302
+ self.summary_label.configure(text=str(payload), foreground=RED)
303
+ self._render_report(core.synth_summary())
304
+ else:
305
+ self._render_report(payload)
306
+ self.summary_label.configure(text="done", foreground=GREEN)
307
+ self.refresh_report()
308
+
309
+ def open_report(self) -> None:
310
+ if core.REPORT_MD.exists():
311
+ webbrowser.open(core.REPORT_MD.resolve().as_uri())
312
+
313
+ def open_solutions(self) -> None:
314
+ core.SOLUTIONS_DIR.mkdir(parents=True, exist_ok=True)
315
+ webbrowser.open(core.SOLUTIONS_DIR.resolve().as_uri())
316
+
317
+ # -------------------------------------------------------- patterns tab
318
+ def run_patterns(self) -> None:
319
+ self._submit(
320
+ lambda: core.run_patterns(self.sequence_var.get()),
321
+ self._patterns_done,
322
+ self.pattern_status,
323
+ )
324
+
325
+ def _patterns_done(self, payload: Any) -> None:
326
+ self._set_busy(False)
327
+ if isinstance(payload, Exception):
328
+ self.pattern_status.configure(text=str(payload), foreground=RED)
329
+ return
330
+ for item in self._ptree.get_children():
331
+ self._ptree.delete(item)
332
+ for row in payload:
333
+ self._ptree.insert(
334
+ "", tk.END,
335
+ values=(row["name"], f"{row['confidence']:.2f}", row["prediction"], row["detail"]),
336
+ )
337
+ self.pattern_status.configure(text=f"{len(payload)} hypotheses", foreground=GREEN)
338
+
339
+ # ----------------------------------------------------------- engine tab
340
+ def _run_cmd(self, argv: list[str]) -> dict[str, Any]:
341
+ result = subprocess.run(
342
+ argv, cwd=core.ROOT, capture_output=True, text=True, timeout=900, check=False
343
+ )
344
+ output = "".join((result.stdout or "")[-6000:]) + "\n" + "".join(
345
+ (result.stderr or "")[-6000:]
346
+ )
347
+ return {"ok": result.returncode == 0, "output": output.strip() or "(no output)"}
348
+
349
+ def run_check(self) -> None:
350
+ self._submit(
351
+ lambda: self._run_cmd([sys.executable, "engine/gen_tests.py", "--check"]),
352
+ self._engine_done,
353
+ )
354
+
355
+ def run_pytest(self) -> None:
356
+ self._submit(
357
+ lambda: self._run_cmd([sys.executable, "-m", "pytest", "-q"]),
358
+ self._engine_done,
359
+ )
360
+
361
+ def run_build(self) -> None:
362
+ self._submit(
363
+ lambda: self._run_cmd([sys.executable, "engine/runner.py", "build"]),
364
+ self._engine_done,
365
+ )
366
+
367
+ def run_bench(self) -> None:
368
+ self._submit(
369
+ lambda: self._run_cmd([sys.executable, "engine/runner.py", "bench"]),
370
+ self._engine_done,
371
+ )
372
+
373
+ def _engine_done(self, payload: Any) -> None:
374
+ self._set_busy(False)
375
+ self.output.configure(state=tk.NORMAL)
376
+ self.output.delete("1.0", tk.END)
377
+ if isinstance(payload, Exception):
378
+ self.output.insert(tk.END, str(payload))
379
+ self.output.configure(state=tk.DISABLED)
380
+ return
381
+ body = payload["output"]
382
+ self.output.insert(tk.END, body)
383
+ self.output.configure(state=tk.DISABLED)
384
+ self.status.configure(
385
+ text="OK" if payload["ok"] else "FAILED",
386
+ foreground=GREEN if payload["ok"] else RED,
387
+ )
388
+
389
+ def open_docs(self) -> None:
390
+ webbrowser.open("https://dsk-dev-ai.github.io/algorithm-discovery-engine/")
391
+
392
+ def open_github(self) -> None:
393
+ webbrowser.open("https://github.com/dsk-dev-ai/algorithm-discovery-engine")
394
+
395
+
396
+ def load_report() -> Report | None:
397
+ return core.load_report()
398
+
399
+
400
+ def main() -> None:
401
+ root = tk.Tk()
402
+ App(root)
403
+ root.mainloop()
404
+
405
+
406
+ if __name__ == "__main__":
407
+ main()
gui/core.py ADDED
@@ -0,0 +1,149 @@
1
+ """Headless logic powering the desktop GUI.
2
+
3
+ Everything here avoids ``tkinter`` so it can run (and be tested) without a
4
+ display. The Tk layer in :mod:`gui.app` only renders what these helpers return.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+
14
+ from algo_discovery import DiscoveryEngine
15
+ from synth import discovery as synth_discovery
16
+
17
+ ROOT: Path = Path(__file__).resolve().parent.parent.parent
18
+ CATALOG_PATH: Path = ROOT / "catalog" / "problems.json"
19
+ TARGETS_PATH: Path = ROOT / "catalog" / "discovery_targets.json"
20
+ DISCOVERIES_DIR: Path = ROOT / "catalog" / "discoveries"
21
+ REPORT_JSON: Path = DISCOVERIES_DIR / "report.json"
22
+ REPORT_MD: Path = DISCOVERIES_DIR / "report.md"
23
+ SOLUTIONS_DIR: Path = DISCOVERIES_DIR / "solutions"
24
+
25
+ _INT_RE = re.compile(r"-?\d+")
26
+
27
+
28
+ def _load_json(path: Path) -> dict[str, Any]:
29
+ return cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8")))
30
+
31
+
32
+ def version() -> str:
33
+ """Read the package version from the project metadata."""
34
+ try:
35
+ from importlib import metadata
36
+
37
+ return metadata.version("algorithm-discovery-engine")
38
+ except Exception: # pragma: no cover - source tree fallback
39
+ root_toml = ROOT / "pyproject.toml"
40
+ if root_toml.exists():
41
+ for line in root_toml.read_text(encoding="utf-8").splitlines():
42
+ line = line.strip()
43
+ if line.startswith("version"):
44
+ return line.split("=")[1].strip().strip('"')
45
+ return "0.0.0"
46
+
47
+
48
+ def catalog_summary() -> dict[str, Any]:
49
+ """Summary of the shared multi-language catalog."""
50
+ data = _load_json(CATALOG_PATH)
51
+ return {
52
+ "version": data.get("version", "?"),
53
+ "algorithms": list(data.get("algorithms", [])),
54
+ "data_structures": list(data.get("data_structures", [])),
55
+ "solved_problems": len(data.get("algorithms", [])),
56
+ }
57
+
58
+
59
+ def parse_sequence(text: str) -> list[int]:
60
+ """Parse a spaced/comma separated integer sequence for pattern discovery."""
61
+ terms = [int(m) for m in _INT_RE.findall(text)]
62
+ if not terms:
63
+ raise ValueError("Enter at least one integer (e.g. 1, 4, 9, 16).")
64
+ return terms
65
+
66
+
67
+ def run_patterns(text: str, top: int = 8) -> list[dict[str, Any]]:
68
+ """Discover ranked hypotheses for a user-supplied integer sequence."""
69
+ terms = parse_sequence(text)
70
+ result = DiscoveryEngine().discover(terms)
71
+ ranked = result.ranked[:top]
72
+ return [
73
+ {
74
+ "name": score.name,
75
+ "confidence": score.confidence,
76
+ "prediction": score.prediction,
77
+ "detail": score.detail,
78
+ }
79
+ for score in ranked
80
+ ]
81
+
82
+
83
+ def run_synth(
84
+ smoke: bool = True, fuzz_count: int | None = None, scan_budget: int | None = None
85
+ ) -> dict[str, Any]:
86
+ """Run the local algorithm synthesizer and return a summarized report.
87
+
88
+ Raises ``RuntimeError`` if any target is rejected or missing, mirroring the
89
+ CLI's exit-code contract.
90
+ """
91
+ report = synth_discovery.discover(
92
+ smoke=smoke, scan_budget=scan_budget, fuzz_count=fuzz_count
93
+ )
94
+ summary = synth_summary(report)
95
+ if summary["rejected"] or summary["missing"]:
96
+ raise RuntimeError(
97
+ f"Discovery incomplete: rejected={summary['rejected']} missing={summary['missing']}"
98
+ )
99
+ return summary
100
+
101
+
102
+ def synth_summary(report: dict[str, Any] | None = None) -> dict[str, Any]:
103
+ """Summarize a report dict (or the latest on disk)."""
104
+ if report is None:
105
+ report = load_report()
106
+ if report is None:
107
+ return {
108
+ "verified": 0,
109
+ "rejected": 0,
110
+ "missing": 0,
111
+ "targets": [],
112
+ "exists": False,
113
+ }
114
+ entries = report.get("targets", [])
115
+ verified = [e for e in entries if e.get("status") == "verified"]
116
+ return {
117
+ "verified": len(verified),
118
+ "rejected": sum(1 for e in entries if e.get("status") == "candidate-rejected"),
119
+ "missing": sum(
120
+ 1 for e in entries if e.get("status") == "no-candidate-found"
121
+ ),
122
+ "targets": entries,
123
+ "exists": True,
124
+ }
125
+
126
+
127
+ def load_report() -> dict[str, Any] | None:
128
+ """Load the latest discovery report from disk (``None`` if absent)."""
129
+ if not REPORT_JSON.exists():
130
+ return None
131
+ return _load_json(REPORT_JSON)
132
+
133
+
134
+ def discovery_targets() -> list[dict[str, Any]]:
135
+ """The configured discovery targets for display."""
136
+ data = _load_json(TARGETS_PATH)
137
+ return [
138
+ {"id": item["id"], "kind": item.get("kind", "?"), "signature": item.get("signature", "")}
139
+ for item in data["targets"]
140
+ ]
141
+
142
+
143
+ def engine_catalog_counts() -> dict[str, int]:
144
+ """Simple numeric summary for the engine tab."""
145
+ data = _load_json(CATALOG_PATH)
146
+ return {
147
+ "algorithms": len(data.get("algorithms", [])),
148
+ "data_structures": len(data.get("data_structures", [])),
149
+ }
synth/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Algorithm synthesis & discovery (local).
2
+
3
+ The :mod:`synth` package searches for candidate algorithms from input/output
4
+ examples:
5
+
6
+ * :mod:`synth.grammar` — expression tree language + Python rendering.
7
+ * :mod:`synth.search` — grammar-based scanner enumeration and strategy
8
+ templates (vote / seen / fib / circular-Kadane).
9
+ * :mod:`synth.corpus` — discovery targets, reference oracles, fuzz inputs.
10
+ * :mod:`synth.discovery` — orchestration, verification, novelty, reporting.
11
+
12
+ CLI:
13
+
14
+ python -m synth discover # full pass over every target
15
+ python -m synth discover --smoke # reduced budget (CI-friendly)
16
+ """
17
+
18
+ from synth.discovery import discover
19
+
20
+ __version__ = "1.0.0"
21
+
22
+ __all__ = ["discover"]
synth/__main__.py ADDED
@@ -0,0 +1,66 @@
1
+ """CLI: ``python -m synth discover [--smoke] [--scan-budget N] [--fuzz N]``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+
10
+ def main() -> int:
11
+ parser = argparse.ArgumentParser(
12
+ prog="synth", description="Local algorithm discovery"
13
+ )
14
+ sub = parser.add_subparsers(dest="command", required=True)
15
+
16
+ discover_parser = sub.add_parser("discover", help="run the discovery pass")
17
+ discover_parser.add_argument(
18
+ "--smoke", action="store_true", help="reduced budget (CI-friendly)"
19
+ )
20
+ discover_parser.add_argument(
21
+ "--scan-budget",
22
+ type=int,
23
+ default=None,
24
+ help="grammar search candidate cap",
25
+ )
26
+ discover_parser.add_argument(
27
+ "--fuzz", type=int, default=None, help="fuzz cases per target"
28
+ )
29
+ discover_parser.add_argument(
30
+ "--print", action="store_true", help="print report JSON to stdout"
31
+ )
32
+
33
+ args = parser.parse_args()
34
+
35
+ if args.command == "discover":
36
+ from synth.discovery import DISCOVERIES_DIR, discover
37
+
38
+ report = discover(
39
+ smoke=args.smoke,
40
+ scan_budget=args.scan_budget,
41
+ fuzz_count=args.fuzz,
42
+ )
43
+ verified = [
44
+ e for e in report["targets"] if e["status"] == "verified"
45
+ ]
46
+ rejected = [
47
+ e for e in report["targets"] if e["status"] == "candidate-rejected"
48
+ ]
49
+ missing = [
50
+ e for e in report["targets"] if e["status"] == "no-candidate-found"
51
+ ]
52
+ print(
53
+ f"targets={len(report['targets'])} verified={len(verified)} "
54
+ f"rejected={len(rejected)} missing={len(missing)}"
55
+ )
56
+ print(f"report: {DISCOVERIES_DIR / 'report.md'}")
57
+ if args.print:
58
+ print(json.dumps(report, indent=2))
59
+ return 0 if not missing and not rejected else 1
60
+
61
+ parser.print_help()
62
+ return 1
63
+
64
+
65
+ if __name__ == "__main__":
66
+ sys.exit(main())