gdk9-cli 0.3.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 (82) hide show
  1. examples/01_analyze.py +28 -0
  2. examples/02_profile.py +24 -0
  3. examples/03_assign.py +19 -0
  4. examples/04_dcg.py +41 -0
  5. examples/05_tokenize.py +22 -0
  6. examples/06_synthesize.py +20 -0
  7. examples/07_api.py +57 -0
  8. examples/08_kernel.py +35 -0
  9. examples/09_kernel_search_walkthrough.py +102 -0
  10. examples/10_conserve_vs_naive.py +100 -0
  11. gdk9/__init__.py +5 -0
  12. gdk9/ansi.py +57 -0
  13. gdk9/cli.py +1186 -0
  14. gdk9/crdt.py +88 -0
  15. gdk9/crypto.py +132 -0
  16. gdk9/data/ninefold.json +40 -0
  17. gdk9/data/official.example.json +16 -0
  18. gdk9/data/official.json +132 -0
  19. gdk9/data/subs.json +436 -0
  20. gdk9/dcg.py +356 -0
  21. gdk9/egglog_bridge/__init__.py +6 -0
  22. gdk9/egglog_bridge/dr_egglog.py +107 -0
  23. gdk9/egglog_bridge/fallback.py +34 -0
  24. gdk9/energy.py +211 -0
  25. gdk9/errors.py +15 -0
  26. gdk9/fmt.py +212 -0
  27. gdk9/imply.py +83 -0
  28. gdk9/io_utils.py +24 -0
  29. gdk9/kernel/__init__.py +24 -0
  30. gdk9/kernel/engine.py +123 -0
  31. gdk9/kernel/errors.py +17 -0
  32. gdk9/kernel/expression.py +43 -0
  33. gdk9/kernel/principle.py +43 -0
  34. gdk9/kernel/proof.py +24 -0
  35. gdk9/kernel/rule.py +30 -0
  36. gdk9/kernel/symbol.py +19 -0
  37. gdk9/kernel_cli.py +195 -0
  38. gdk9/log.py +30 -0
  39. gdk9/optimize.py +288 -0
  40. gdk9/parser.py +35 -0
  41. gdk9/plugins/__init__.py +9 -0
  42. gdk9/plugins/loader.py +346 -0
  43. gdk9/plugins/registry.py +42 -0
  44. gdk9/principles.py +131 -0
  45. gdk9/saturate.py +89 -0
  46. gdk9/state.py +138 -0
  47. gdk9/subs.py +108 -0
  48. gdk9/tokenize.py +386 -0
  49. gdk9/tui.py +115 -0
  50. gdk9/utilization.py +15 -0
  51. gdk9_cli-0.3.0.dist-info/METADATA +295 -0
  52. gdk9_cli-0.3.0.dist-info/RECORD +82 -0
  53. gdk9_cli-0.3.0.dist-info/WHEEL +5 -0
  54. gdk9_cli-0.3.0.dist-info/entry_points.txt +3 -0
  55. gdk9_cli-0.3.0.dist-info/licenses/LICENSE +661 -0
  56. gdk9_cli-0.3.0.dist-info/top_level.txt +4 -0
  57. scripts/batch_analyze.py +109 -0
  58. scripts/export_json.py +125 -0
  59. scripts/make_zip.py +57 -0
  60. scripts/profile_compare.py +165 -0
  61. scripts/watch_file.py +124 -0
  62. tests/conftest.py +26 -0
  63. tests/experiment/__init__.py +1 -0
  64. tests/experiment/test_conserve_vs_naive.py +287 -0
  65. tests/kernel/test_kernel_energy.py +18 -0
  66. tests/kernel/test_kernel_rules.py +58 -0
  67. tests/kernel/test_kernel_search_bound.py +34 -0
  68. tests/test_crypto.py +36 -0
  69. tests/test_crypto_secure.py +26 -0
  70. tests/test_dcg.py +318 -0
  71. tests/test_dcg_golden.py +67 -0
  72. tests/test_egglog_bridge.py +41 -0
  73. tests/test_energy.py +34 -0
  74. tests/test_kernel_cli_handbook.py +101 -0
  75. tests/test_optimize.py +21 -0
  76. tests/test_plugins.py +64 -0
  77. tests/test_rules.py +43 -0
  78. tests/test_rules_commit.py +40 -0
  79. tests/test_rules_reversibility.py +50 -0
  80. tests/test_state_crdt.py +87 -0
  81. tests/test_tokenize.py +38 -0
  82. tests/test_tokenize_metrics.py +33 -0
gdk9/cli.py ADDED
@@ -0,0 +1,1186 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from typing import Any, Dict
7
+ from textwrap import dedent
8
+
9
+ from .energy import analyze_text, energy_profile, string_energy, vector_energy, harmonic_triads
10
+ from .errors import Gdk9Error, InputError, ConfigError, OptimizationError
11
+ from .io_utils import read_input
12
+ from .principles import Principle, load_principle
13
+ from .utilization import attune, sigil
14
+ from .tokenize import summarize_tokens_table, to_json_payload, annotate_text, delimiter_set
15
+ from .optimize import optimize_attunement, apply_plan, Plan, optimize_substitution, EditPlan, apply_edit_plan
16
+ from .log import logger
17
+ from .ansi import supports_color, colorize
18
+ from .fmt import Box, section, kv, fmt_dr, fmt_float_e, fmt_class, fmt_valid, fmt_form, energy_bar, vlen
19
+ from .state import load_state, save_state, list_symbols, set_symbol, merge_state, set_rule
20
+ from .imply import make_fusion, make_split, apply_rule, Rule
21
+ from .kernel_cli import run_kernel
22
+ from . import __version__
23
+ from .plugins.loader import (
24
+ list_available as plugins_list_available,
25
+ find_plugin as plugins_find,
26
+ load_plugin as plugins_load,
27
+ apply_plugin as plugins_apply,
28
+ enable_plugin as plugins_enable,
29
+ disable_plugin as plugins_disable,
30
+ auto_boot as plugins_auto_boot,
31
+ reset_config as plugins_reset_config,
32
+ )
33
+
34
+ class SmartFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
35
+ pass
36
+
37
+
38
+ def print_table(rows: list[list[str]], use_color: bool = False) -> None:
39
+ """Render rows[0] as header, remaining as data, with Unicode borders."""
40
+ if not rows:
41
+ return
42
+ t = Box(rows[0], enabled=use_color)
43
+ for row in rows[1:]:
44
+ t.row(row)
45
+ print(t.render())
46
+
47
+
48
+ def cmd_analyze(args: argparse.Namespace, principle: Principle, use_color: bool) -> int:
49
+ text = read_input(args.text, args.file)
50
+ result = analyze_text(text, principle)
51
+ if args.format == "json":
52
+ out: Dict[str, Any] = {}
53
+ for k, units in result.items():
54
+ out[k] = [u.__dict__ for u in units]
55
+ if args.mode == "extended":
56
+ out["vector"] = vector_energy(text, principle)
57
+ out["harmonics"] = harmonic_triads(text, principle)
58
+ print(json.dumps(out, ensure_ascii=False, indent=2))
59
+ return 0
60
+ # Table
61
+ _unit_color = {"document": "bold", "paragraph": "white", "sentence": "dim", "word": None}
62
+ t = Box(["unit", "value", "energy", "total"],
63
+ col_align=["l", "l", "r", "r"], enabled=use_color)
64
+ for k in ["document", "paragraphs", "sentences", "words"]:
65
+ for u in result.get(k, []):
66
+ val = (u.value[:48] + "…") if len(u.value) > 48 else u.value
67
+ unit_s = colorize(u.unit, _unit_color.get(u.unit), use_color)
68
+ e_s = fmt_dr(u.energy, use_color)
69
+ t.row([unit_s, val, e_s, str(u.total)])
70
+ print(t.render())
71
+ if args.mode == "extended":
72
+ vec = vector_energy(text, principle)
73
+ harm = harmonic_triads(text, principle)
74
+ print()
75
+ tv = Box(["vector", "letters", "digits", "symbols"],
76
+ col_align=["l", "r", "r", "r"], enabled=use_color)
77
+ tv.row(["sum", str(vec["sum"]["letters"]), str(vec["sum"]["digits"]), str(vec["sum"]["symbols"])])
78
+ tv.row(["dr",
79
+ fmt_dr(vec["dr"]["letters"], use_color),
80
+ fmt_dr(vec["dr"]["digits"], use_color),
81
+ fmt_dr(vec["dr"]["symbols"], use_color)])
82
+ print(tv.render())
83
+ print()
84
+ th = Box(["harmonics", "root", "wave", "peak"],
85
+ col_align=["l", "r", "r", "r"], enabled=use_color)
86
+ th.row(["counts",
87
+ colorize(str(harm["root"]), "cyan", use_color),
88
+ colorize(str(harm["wave"]), "yellow", use_color),
89
+ colorize(str(harm["peak"]), "magenta", use_color)])
90
+ print(th.render())
91
+ return 0
92
+
93
+
94
+ def cmd_profile(args: argparse.Namespace, principle: Principle, use_color: bool) -> int:
95
+ text = read_input(args.text, args.file)
96
+ prof = energy_profile(text, principle)
97
+ total, dr = string_energy(text, principle)
98
+ if args.format == "json":
99
+ print(json.dumps({"profile": prof, "total": total, "dr": dr}, indent=2))
100
+ return 0
101
+ max_cnt = max(int(v) for v in prof.values()) or 1
102
+ t = Box(["dr", "cnt", ""], col_align=["c", "r", "l"], enabled=use_color)
103
+ for i in range(1, 10):
104
+ cnt = int(prof[str(i)])
105
+ dr_s = fmt_dr(i, use_color)
106
+ cnt_s = colorize(str(cnt), "dim" if cnt == 0 else None, use_color)
107
+ bar = energy_bar(i, cnt, max_cnt, bar_width=22, enabled=use_color)
108
+ t.row([dr_s, cnt_s, bar])
109
+ print(t.render())
110
+ print(kv("total", str(total), use_color) + " " + kv("dr", fmt_dr(dr, use_color), use_color))
111
+ return 0
112
+
113
+
114
+ def cmd_assign(args: argparse.Namespace, principle: Principle, use_color: bool) -> int:
115
+ text = read_input(args.text, args.file)
116
+ from .energy import char_energy
117
+ t = Box(["char", "energy"], col_align=["c", "r"], enabled=use_color)
118
+ for ch in text:
119
+ if ch == "\n":
120
+ continue
121
+ e = char_energy(ch, principle)
122
+ t.row([colorize(repr(ch)[1:-1], "bold", use_color), fmt_dr(e, use_color)])
123
+ print(t.render())
124
+ return 0
125
+
126
+
127
+ def cmd_attune(args: argparse.Namespace, principle: Principle, use_color: bool) -> int:
128
+ text = read_input(args.text, args.file)
129
+ try:
130
+ if args.method in {"append", "prepend", "intersperse"}:
131
+ plan = optimize_attunement(
132
+ text,
133
+ args.target,
134
+ principle,
135
+ allowed_symbols=args.allowed,
136
+ method=args.method,
137
+ max_steps=args.max_steps,
138
+ )
139
+ logger.debug("plan", plan)
140
+ out_text = apply_plan(text, plan, spread=args.spread)
141
+ elif args.method in {"substitute", "edit"}:
142
+ subs = None
143
+ if args.subs_file:
144
+ import pathlib
145
+ subs_data = json.loads(pathlib.Path(args.subs_file).read_text(encoding='utf-8'))
146
+ subs = subs_data.get('subs') if isinstance(subs_data, dict) else None
147
+ allowed_inserts = subs_data.get('allowed_inserts') if isinstance(subs_data, dict) else None
148
+ else:
149
+ allowed_inserts = args.allowed
150
+ eplan = optimize_substitution(
151
+ text,
152
+ args.target,
153
+ principle,
154
+ subs=subs,
155
+ allow_delete=bool(args.allow_delete),
156
+ allowed_inserts=allowed_inserts,
157
+ max_edits=args.max_steps,
158
+ )
159
+ logger.debug("edit-plan", eplan)
160
+ out_text = apply_edit_plan(text, eplan)
161
+ else:
162
+ # Fallback to legacy single-symbol append
163
+ new_text, total, dr, count = attune(text, args.target, principle, symbol=args.symbol)
164
+ plan = Plan("append", args.target, [(args.symbol, count)], total - (count * 0), 0, total, dr)
165
+ out_text = new_text
166
+ except ValueError as exc:
167
+ raise InputError(str(exc))
168
+ if args.in_place and args.file:
169
+ import pathlib
170
+ pathlib.Path(args.file).write_text(out_text, encoding="utf-8")
171
+ if args.output == "text":
172
+ print(out_text)
173
+ return 0
174
+ if args.method in {"append", "prepend", "intersperse"}:
175
+ payload = {
176
+ "plan": {"method": plan.method, "target": plan.target, "steps": plan.steps},
177
+ "before": {"total": plan.total_before, "dr": plan.dr_before},
178
+ "after": {"total": plan.total_after, "dr": plan.dr_after},
179
+ "attuned": out_text if args.include_text else None,
180
+ }
181
+ else:
182
+ # Edit plan payload
183
+ total_before, dr_before = string_energy(text, principle)
184
+ total_after, dr_after = string_energy(out_text, principle)
185
+ payload = {
186
+ "plan": {
187
+ "method": "edit",
188
+ "ops": [op.__dict__ for op in eplan.ops],
189
+ },
190
+ "before": {"total": total_before, "dr": dr_before},
191
+ "after": {"total": total_after, "dr": dr_after},
192
+ "attuned": out_text if args.include_text else None,
193
+ }
194
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
195
+ return 0
196
+
197
+
198
+ def cmd_compare(args: argparse.Namespace, principle: Principle, use_color: bool) -> int:
199
+ left = read_input(args.left, args.left_file)
200
+ right = read_input(args.right, args.right_file)
201
+ ltot, ldr = string_energy(left, principle)
202
+ rtot, rdr = string_energy(right, principle)
203
+ delta = rtot - ltot
204
+ t = Box(["side", "total", "dr"], col_align=["l", "r", "r"], enabled=use_color)
205
+ t.row([colorize("left", "dim", use_color), str(ltot), fmt_dr(ldr, use_color)])
206
+ t.row([colorize("right", "bold", use_color), str(rtot), fmt_dr(rdr, use_color)])
207
+ print(t.render())
208
+ if delta == 0:
209
+ delta_s = colorize("0", "bright_green", use_color)
210
+ elif delta > 0:
211
+ delta_s = colorize(f"+{delta}", "yellow", use_color)
212
+ else:
213
+ delta_s = colorize(str(delta), "red", use_color)
214
+ print(kv("delta", delta_s, use_color))
215
+ return 0
216
+
217
+
218
+ def cmd_encode(args: argparse.Namespace, principle: Principle) -> int:
219
+ text = read_input(args.text, args.file)
220
+ from .energy import char_energy
221
+
222
+ if args.style == "annotate":
223
+ out = []
224
+ for ch in text:
225
+ e = char_energy(ch, principle)
226
+ out.append(f"{ch}[{e}]")
227
+ print("".join(out))
228
+ return 0
229
+ if args.style == "json":
230
+ from .energy import analyze_text as at
231
+
232
+ res = at(text, principle)
233
+ payload = {k: [u.__dict__ for u in v] for k, v in res.items()}
234
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
235
+ return 0
236
+ raise InputError("Unknown encode style.")
237
+
238
+
239
+ def cmd_decode(args: argparse.Namespace, principle: Principle) -> int: # pragma: no cover - trivial
240
+ text = read_input(args.text, args.file)
241
+ # Remove simple [n] annotations
242
+ import re
243
+
244
+ print(re.sub(r"\[(?:[1-9])\]", "", text))
245
+ return 0
246
+
247
+
248
+ def cmd_synthesize(args: argparse.Namespace, principle: Principle, use_color: bool) -> int:
249
+ text = read_input(args.text, args.file)
250
+ raw = sigil(text, principle, style=args.style)
251
+ total, dr = string_energy(text, principle)
252
+ if not use_color:
253
+ print(raw)
254
+ return 0
255
+ lines = raw.splitlines()
256
+ # Colour the header line (DR= TOTAL=) and each cell value
257
+ out = []
258
+ for line in lines:
259
+ if line.startswith("DR="):
260
+ parts = line.split()
261
+ header = " ".join(
262
+ kv(p.split("=")[0], colorize(p.split("=")[1], "bright_white", True), True)
263
+ for p in parts if "=" in p
264
+ )
265
+ out.append(header)
266
+ else:
267
+ # Colour each digit in grid/bar lines by its DR value
268
+ coloured = ""
269
+ for ch in line:
270
+ if ch.isdigit() and ch != "0":
271
+ coloured += fmt_dr(int(ch), True)
272
+ elif ch == "0":
273
+ coloured += colorize("0", "dim", True)
274
+ elif ch == "#":
275
+ dr_v = int(line[0]) if line and line[0].isdigit() else 9
276
+ coloured += colorize(ch, "bright_white", True)
277
+ else:
278
+ coloured += colorize(ch, "dim", True) if ch in ":-" else ch
279
+ out.append(coloured)
280
+ # Frame the sigil
281
+ inner_w = max(vlen(l) for l in out) if out else 20
282
+ h_bar = "─" * (inner_w + 4)
283
+ b = lambda s: colorize(s, "dim", True)
284
+ print(b("┌" + h_bar + "┐"))
285
+ for l in out:
286
+ pad_r = " " * max(0, inner_w - vlen(l))
287
+ print(b("│") + " " + l + pad_r + " " + b("│"))
288
+ print(b("└" + h_bar + "┘"))
289
+ return 0
290
+
291
+
292
+ def cmd_principles(args: argparse.Namespace, current: Principle) -> int:
293
+ if args.action == "show":
294
+ payload = {
295
+ "name": current.name,
296
+ "description": current.description,
297
+ "letter_mode": current.letter_mode,
298
+ "number_mode": current.number_mode,
299
+ "normalize_zero_to_nine": current.normalize_zero_to_nine,
300
+ "symbol_energy": current.symbol_energy,
301
+ }
302
+ print(json.dumps(payload, indent=2))
303
+ return 0
304
+ if args.action == "validate":
305
+ try:
306
+ _ = load_principle(args.file)
307
+ except ConfigError as exc:
308
+ print(f"invalid: {exc}")
309
+ return 2
310
+ print("ok")
311
+ return 0
312
+ if args.action == "install":
313
+ if not args.file:
314
+ raise InputError("--file is required for install")
315
+ import shutil
316
+ from pathlib import Path
317
+ try:
318
+ # Validate first
319
+ _ = load_principle(args.file)
320
+ here = Path(__file__).parent
321
+ dst = here / 'data' / 'official.json'
322
+ dst.write_text(Path(args.file).read_text(encoding='utf-8'), encoding='utf-8')
323
+ print(str(dst))
324
+ return 0
325
+ except Exception as exc:
326
+ raise ConfigError(f"Install failed: {exc}")
327
+ raise InputError("Unsupported principles action")
328
+
329
+
330
+ def cmd_optimize(args: argparse.Namespace, principle: Principle) -> int:
331
+ text = read_input(args.text, args.file)
332
+ try:
333
+ plan = optimize_attunement(
334
+ text,
335
+ args.target,
336
+ principle,
337
+ allowed_symbols=args.allowed,
338
+ method=args.method,
339
+ max_steps=args.max_steps,
340
+ )
341
+ except ValueError as exc:
342
+ raise OptimizationError(str(exc))
343
+ print(
344
+ json.dumps(
345
+ {
346
+ "method": plan.method,
347
+ "target": plan.target,
348
+ "steps": plan.steps,
349
+ "before": {"total": plan.total_before, "dr": plan.dr_before},
350
+ "after": {"total": plan.total_after, "dr": plan.dr_after},
351
+ },
352
+ ensure_ascii=False,
353
+ indent=2,
354
+ )
355
+ )
356
+ return 0
357
+
358
+
359
+ def build_parser() -> argparse.ArgumentParser:
360
+ p = argparse.ArgumentParser(
361
+ prog="gdk9",
362
+ description=(
363
+ "Gdk9 — symbolic energy tools: analyze, tokenize, attune, and compose rules."
364
+ ),
365
+ formatter_class=SmartFormatter,
366
+ epilog=dedent(
367
+ """
368
+ Examples:
369
+ gdk9 an "Hello, world" # quick analysis
370
+ gdk9 tok "<alpha|beta>" -e 1 -F table # tokenize by energy-1 delimiters
371
+ gdk9 att "phrase" -t 7 -m append -a ".!?" --include-text
372
+ gdk9 sym add NAME 12.5 -S ./state.json # manage symbols
373
+ gdk9 im ds SPL X1 X2 0.5 # define split rule
374
+ gdk9 im ap SPL X --commit # apply rule and persist
375
+ gdk9 state merge -i peer.json # CRDT-merge a peer state
376
+ """
377
+ ),
378
+ )
379
+ p.add_argument(
380
+ "--principle",
381
+ "-P",
382
+ help="Principle file (JSON/YAML). Defaults to built-in Ninefold Grid.",
383
+ )
384
+ p.add_argument(
385
+ "--state",
386
+ "-S",
387
+ help="State file for symbols/rules (default ~/.gdk9/state.json)",
388
+ )
389
+ p.add_argument("--debug", "-d", action="store_true", help="Enable debug logging")
390
+ p.add_argument(
391
+ "--version",
392
+ action="version",
393
+ version=(
394
+ f"gdk9 {__version__} — License: AGPL-3.0-or-later\n"
395
+ f"Copyright Adam Grange | adamgrange@proton.me"
396
+ ),
397
+ )
398
+ color_group = p.add_mutually_exclusive_group()
399
+ color_group.add_argument("--color", "-C", action="store_true", help="Force color output")
400
+ color_group.add_argument("--no-color", "-N", action="store_true", help="Disable color output")
401
+ sub = p.add_subparsers(dest="cmd", required=True)
402
+
403
+ a = sub.add_parser("analyze", aliases=["an"], help="Analyze text into energies (document→paragraphs→sentences→words)")
404
+ a.add_argument("text", nargs="?", help="Inline text (or use --file or stdin)")
405
+ a.add_argument("--file", "-f", help="Read text from file path")
406
+ a.add_argument("--format", "-F", choices=["table", "json"], default="table", help="Output format")
407
+ a.add_argument("--mode", "-m", choices=["core", "extended"], default="core", help="Include vectors/harmonics in extended mode")
408
+
409
+ pr = sub.add_parser("profile", aliases=["prof"], help="Show energy histogram and totals for text")
410
+ pr.add_argument("text", nargs="?")
411
+ pr.add_argument("--file", "-f")
412
+ pr.add_argument("--format", "-F", choices=["table", "json"], default="table", help="Output format")
413
+
414
+ asg = sub.add_parser("assign", aliases=["asg"], help="List per-character energy assignments for given text")
415
+ asg.add_argument("text", nargs="?")
416
+ asg.add_argument("--file", "-f")
417
+
418
+ att = sub.add_parser("attune", aliases=["att"], help="Adjust text to a target digital-root energy")
419
+ att.add_argument("text", nargs="?")
420
+ att.add_argument("--file", "-f")
421
+ att.add_argument("--target", "-t", type=int, required=True, help="Target energy 1..9")
422
+ att.add_argument("--method", "-m", choices=["append", "prepend", "intersperse", "substitute", "edit"], default="append", help="Attunement method")
423
+ att.add_argument("--allowed", "-a", help="Allowed symbols to use (e.g. '.!?*')")
424
+ att.add_argument("--symbol", default=".", help="Legacy single-symbol fallback")
425
+ att.add_argument("--max-steps", "-s", type=int, default=64, help="Max steps/edits to consider")
426
+ att.add_argument("--spread", "-p", type=int, help="For intersperse, approximate insertion interval")
427
+ att.add_argument("--subs-file", "-S", help="JSON file mapping chars to replacement arrays for substitution/edit")
428
+ att.add_argument("--allow-delete", action="store_true", help="Allow deletions in edit planning")
429
+ att.add_argument("--output", "-o", choices=["json", "text"], default="json", help="Result output type")
430
+ att.add_argument("--include-text", "-I", action="store_true", help="Include attuned text in JSON output")
431
+ att.add_argument("--in-place", "-w", action="store_true", help="Write result back to --file in-place")
432
+
433
+ cmp_p = sub.add_parser("compare", aliases=["cmp"], help="Compare total and DR between two inputs")
434
+ cmp_p.add_argument("left", nargs="?")
435
+ cmp_p.add_argument("--left-file", "-L")
436
+ cmp_p.add_argument("right", nargs="?")
437
+ cmp_p.add_argument("--right-file", "-R")
438
+
439
+ enc = sub.add_parser("encode", aliases=["enc"], help="Encode text with energy annotations")
440
+ enc.add_argument("text", nargs="?")
441
+ enc.add_argument("--file", "-f")
442
+ enc.add_argument("--style", "-s", choices=["annotate", "json"], default="annotate")
443
+
444
+ dec = sub.add_parser("decode", aliases=["dec"], help="Strip simple energy annotations like [3]")
445
+ dec.add_argument("text", nargs="?")
446
+ dec.add_argument("--file", "-f")
447
+
448
+ syn = sub.add_parser("synthesize", aliases=["sig"], help="Create a sigil/visual from energy profile")
449
+ syn.add_argument("text", nargs="?")
450
+ syn.add_argument("--file", "-f")
451
+ syn.add_argument("--style", "-s", choices=["grid", "bar"], default="grid")
452
+
453
+ tok = sub.add_parser("tokenize", aliases=["tok"], help="Tokenize text by symbol-energy delimiters")
454
+ tok.add_argument("text", nargs="?")
455
+ tok.add_argument("--file", "-f")
456
+ tok.add_argument("--energy", "-e", type=int, default=1, help="Delimiter energy value to use")
457
+ tok.add_argument("--delims", "-d", help="Override delimiter set (e.g., '<>|')")
458
+ tok.add_argument("--keep-delims", "-k", action="store_true", help="Keep delimiters in output tokens")
459
+ tok.add_argument("--drop-delims", "-D", action="store_true", help="Drop delimiters from output tokens")
460
+ tok.add_argument("--no-strip", "-n", action="store_true", help="Do not strip whitespace from tokens")
461
+ tok.add_argument("--no-footer", "-x", action="store_true", help="Suppress summary and delimiter footer")
462
+ tok.add_argument("--summary-only", "-u", action="store_true", help="Only print summary/metrics (no table body)")
463
+ tok.add_argument(
464
+ "-F", "--format",
465
+ choices=["table", "json", "annotate"],
466
+ default="table",
467
+ help="Output format",
468
+ )
469
+
470
+ prc = sub.add_parser("principles", aliases=["prin"], help="Work with principles (show/validate/install)")
471
+ prc.add_argument("action", choices=["show", "validate", "install"])
472
+ prc.add_argument("--file", help="Principle file for validation/installation")
473
+
474
+ opt = sub.add_parser("optimize", aliases=["opt"], help="Compute minimal-step plan to reach target")
475
+ opt.add_argument("text", nargs="?")
476
+ opt.add_argument("--file", "-f")
477
+ opt.add_argument("--target", type=int, required=True)
478
+ opt.add_argument("--method", choices=["append", "prepend", "intersperse"], default="append")
479
+ opt.add_argument("--allowed", help="Allowed symbols (e.g., '.!?*+')")
480
+ opt.add_argument("--max-steps", type=int, default=64)
481
+
482
+ tui = sub.add_parser("tui", aliases=["ui"], help="Interactive live analysis (curses-based)")
483
+ tui.add_argument("--target", type=int, default=9)
484
+ tui.add_argument("--allowed", default=".!?*+")
485
+ tui.add_argument("--method", choices=["append", "prepend", "intersperse"], default="append")
486
+
487
+ subs = sub.add_parser("subs", aliases=["sub"], help="Work with substitution profiles")
488
+ subs.add_argument("action", choices=["generate", "example"])
489
+ subs.add_argument("--output", "-o", help="Write to file instead of stdout")
490
+ subs.add_argument("--limit", "-l", type=int, default=3, help="Max suggestions per char")
491
+
492
+ sym = sub.add_parser("symbol", aliases=["sym"], help="Manage symbols")
493
+ sym_sub = sym.add_subparsers(dest="symbol_cmd", required=True)
494
+ sym_add = sym_sub.add_parser("add", help="Add or update a symbol")
495
+ sym_add.add_argument("name")
496
+ sym_add.add_argument("energy", type=float)
497
+ sym_list = sym_sub.add_parser("list", aliases=["ls"], help="List symbols")
498
+
499
+ imp = sub.add_parser("imply", aliases=["im"], help="Define and apply rules")
500
+ imp_sub = imp.add_subparsers(dest="imply_cmd", required=True)
501
+ imp_df = imp_sub.add_parser("define-fusion", aliases=["df", "fuse"], help="Define fusion rule")
502
+ imp_df.add_argument("rule")
503
+ imp_df.add_argument("out_name")
504
+ imp_df.add_argument("arity", type=int)
505
+ imp_ds = imp_sub.add_parser("define-split", aliases=["ds", "split"], help="Define split rule")
506
+ imp_ds.add_argument("rule")
507
+ imp_ds.add_argument("out_a")
508
+ imp_ds.add_argument("out_b")
509
+ imp_ds.add_argument("ratio", type=float)
510
+ imp_list = imp_sub.add_parser("list", aliases=["ls"], help="List rules")
511
+ imp_ap = imp_sub.add_parser("apply", aliases=["ap"], help="Apply rule to input symbol names")
512
+ imp_ap.add_argument("rule")
513
+ imp_ap.add_argument("inputs", nargs="+")
514
+ imp_ap.add_argument("--commit", action="store_true", help="Persist outputs into state symbols")
515
+
516
+ ker = sub.add_parser(
517
+ "kernel",
518
+ aliases=["kn"],
519
+ help="Pure kernel smoke path: eval / apply / search (no state I/O)",
520
+ )
521
+ ker_sub = ker.add_subparsers(dest="kernel_cmd", required=True)
522
+ ker_eval = ker_sub.add_parser("eval", aliases=["ev"], help="Evaluate text under KernelPrinciple")
523
+ ker_eval.add_argument("text", help="Input text (each character becomes a symbol)")
524
+ ker_ap = ker_sub.add_parser("apply", aliases=["ap"], help="Apply a builtin fuse/split rule")
525
+ ker_ap.add_argument("rule", help="Builtin rule name: fuse | split")
526
+ ker_ap.add_argument("symbols", nargs="+", help="Ordered symbol names to transform")
527
+ ker_ap.add_argument("--parts", help="Comma-separated split output names (split only)")
528
+ ker_ap.add_argument("--energies", help="Comma-separated split output energies (split only)")
529
+ ker_se = ker_sub.add_parser("search", aliases=["find"], help="Bounded implication search (engine.infer)")
530
+ ker_se.add_argument("source", nargs="+", help="Source symbol names")
531
+ ker_se.add_argument("--target", nargs="+", required=True, help="Target symbol names")
532
+ ker_se.add_argument("--max-depth", type=int, default=4, help="Maximum implication depth")
533
+ ker_se.add_argument(
534
+ "--rules",
535
+ nargs="+",
536
+ default=["fuse"],
537
+ help="Builtin rules to enable (default: fuse)",
538
+ )
539
+ ker_se.add_argument("--parts", help="Comma-separated split output names when using split")
540
+ ker_se.add_argument("--energies", help="Comma-separated split output energies when using split")
541
+
542
+ repl = sub.add_parser("repl", aliases=["sh"], help="Interactive REPL for symbols and imply")
543
+
544
+ # plugin management
545
+ pl = sub.add_parser("plugin", aliases=["pl"], help="Manage and load plugins (rule packs/grammars)")
546
+ pl_sub = pl.add_subparsers(dest="plugin_cmd", required=True)
547
+ pl_list = pl_sub.add_parser("list", help="List discovered plugins in search paths")
548
+ pl_val = pl_sub.add_parser("validate", help="Validate a plugin pack (YAML/JSON/Python)")
549
+ pl_val.add_argument("name_or_path")
550
+ pl_info = pl_sub.add_parser("info", help="Show plugin metadata")
551
+ pl_info.add_argument("name_or_path")
552
+ pl_load = pl_sub.add_parser("load", help="Load plugin into current state and enable for auto-boot")
553
+ pl_load.add_argument("name_or_path")
554
+ pl_load.add_argument("--no-enable", action="store_true", help="Do not enable for auto-boot")
555
+ pl_en = pl_sub.add_parser("enable", help="Enable plugin for auto-boot by name or path")
556
+ pl_en.add_argument("name_or_path")
557
+ pl_dis = pl_sub.add_parser("disable", help="Disable plugin by name")
558
+ pl_dis.add_argument("name")
559
+
560
+ # rich help
561
+ hp = sub.add_parser("help", aliases=["h"], help="Show help or subcommand help (e.g. 'gdk9 help plugin')")
562
+ hp.add_argument("topic", nargs="?", help="Optional topic or subcommand name")
563
+ hb = sub.add_parser("handbook", aliases=["hb"], help="Print comprehensive handbook (EN + RU)")
564
+
565
+ # reset/clear
566
+ rst = sub.add_parser("reset", aliases=["clear"], help="Reset state/config to defaults for a fresh start")
567
+ rst.add_argument("--state", "-S", help="State file to reset (default ~/.gdk9/state.json)")
568
+ rst.add_argument("--plugins", action="store_true", help="Reset plugin auto-boot config only (unless combined)")
569
+ rst.add_argument("--all", action="store_true", help="Reset both state and plugins")
570
+ rst.add_argument("--rules-only", action="store_true", help="Only clear rules in state (keep symbols)")
571
+ rst.add_argument("--symbols-only", action="store_true", help="Only clear symbols in state (keep rules)")
572
+ rst.add_argument("--yes", "-y", action="store_true", help="Do not prompt for confirmation")
573
+
574
+ st = sub.add_parser("state", help="CRDT-aware state management")
575
+ st_sub = st.add_subparsers(dest="state_cmd", required=True)
576
+ st_merge = st_sub.add_parser("merge", help="Merge another state file into the current one using LWW CRDTs")
577
+ st_merge.add_argument("--incoming", "-i", required=True, help="Path to incoming state file")
578
+ st_merge.add_argument("--dry-run", action="store_true", help="Show merge result without writing")
579
+
580
+ # dcg — Directed Cognition Graph
581
+ dcg = sub.add_parser("dcg", help="Directed Cognition Graph: classify, path, vector, shortest, homotopy, info")
582
+ dcg_sub = dcg.add_subparsers(dest="dcg_cmd", required=True)
583
+
584
+ dcg_cls = dcg_sub.add_parser("classify", aliases=["cls"], help="Show symmetry class and SymPhi energy for each letter")
585
+ dcg_cls.add_argument("text", help="Text to classify (only alphabetic characters are processed)")
586
+ dcg_cls.add_argument("--format", "-F", choices=["table", "json"], default="table")
587
+
588
+ dcg_path = dcg_sub.add_parser("path", aliases=["p"], help="Analyse word as a DCG path and show per-step edge validity")
589
+ dcg_path.add_argument("word")
590
+ dcg_path.add_argument("--format", "-F", choices=["table", "json"], default="table")
591
+
592
+ dcg_vec = dcg_sub.add_parser("vector", aliases=["vec"], help="Show 4D vector sum for a word")
593
+ dcg_vec.add_argument("word")
594
+ dcg_vec.add_argument("--format", "-F", choices=["table", "json"], default="table")
595
+
596
+ dcg_sp = dcg_sub.add_parser("shortest", aliases=["sp"], help="Shortest DCG path between two letters (Dijkstra)")
597
+ dcg_sp.add_argument("src", help="Source letter")
598
+ dcg_sp.add_argument("dst", help="Destination letter")
599
+ dcg_sp.add_argument("--format", "-F", choices=["table", "json"], default="table")
600
+
601
+ dcg_hom = dcg_sub.add_parser("homotopy", aliases=["hom"], help="Check homotopy equivalence of two words")
602
+ dcg_hom.add_argument("word_a")
603
+ dcg_hom.add_argument("word_b")
604
+ dcg_hom.add_argument("--tol", type=float, default=1.0, help="Energy and vector-distance tolerance (default 1.0)")
605
+ dcg_hom.add_argument("--format", "-F", choices=["table", "json"], default="table")
606
+
607
+ dcg_info = dcg_sub.add_parser("info", help="Show DCG graph statistics")
608
+ dcg_info.add_argument("--format", "-F", choices=["table", "json"], default="table")
609
+
610
+ # crypto (experimental)
611
+ cr = sub.add_parser("crypto", aliases=["crypt"], help="Experimental ciphers: edpc (playful) or secure (requires 'cryptography')")
612
+ cr_sub = cr.add_subparsers(dest="crypto_cmd", required=True)
613
+ cr_enc = cr_sub.add_parser("encrypt", help="Encrypt text with a key")
614
+ cr_enc.add_argument("text", nargs="?")
615
+ cr_enc.add_argument("--file", "-f")
616
+ cr_enc.add_argument("--key", "-k", required=True)
617
+ cr_enc.add_argument("--mode", "-m", choices=["edpc", "secure"], default="edpc")
618
+ cr_dec = cr_sub.add_parser("decrypt", help="Decrypt text with a key")
619
+ cr_dec.add_argument("text", nargs="?")
620
+ cr_dec.add_argument("--file", "-f")
621
+ cr_dec.add_argument("--key", "-k", required=True)
622
+ cr_dec.add_argument("--mode", "-m", choices=["edpc", "secure"], default="edpc")
623
+
624
+ return p
625
+
626
+
627
+ def main(argv: list[str] | None = None) -> int:
628
+ parser = build_parser()
629
+ args = parser.parse_args(argv)
630
+ try:
631
+ logger.set_enabled(bool(args.debug))
632
+ use_color = (getattr(args, "color", False)) or (supports_color() and not getattr(args, "no_color", False))
633
+ principle = load_principle(args.principle)
634
+ state_path = getattr(args, "state", None)
635
+ # Auto-boot enabled plugins (merge symbol_energy and rules into state/principle)
636
+ state_aut = load_state(state_path)
637
+ principle, state_aut, loaded_plugins = plugins_auto_boot(principle, state_aut)
638
+ if loaded_plugins:
639
+ save_state(state_aut, state_path)
640
+ if args.cmd == "analyze":
641
+ return cmd_analyze(args, principle, use_color)
642
+ if args.cmd == "profile":
643
+ return cmd_profile(args, principle, use_color)
644
+ if args.cmd == "assign":
645
+ return cmd_assign(args, principle, use_color)
646
+ if args.cmd == "attune":
647
+ return cmd_attune(args, principle, use_color)
648
+ if args.cmd == "compare":
649
+ return cmd_compare(args, principle, use_color)
650
+ if args.cmd == "encode":
651
+ return cmd_encode(args, principle)
652
+ if args.cmd == "decode":
653
+ return cmd_decode(args, principle)
654
+ if args.cmd == "synthesize":
655
+ return cmd_synthesize(args, principle, use_color)
656
+ if args.cmd == "dcg":
657
+ from .dcg import (
658
+ get_dcg, symmetry_class, sym_energy, vectorize,
659
+ word_sym_energy, word_vector, homotopy_equivalent,
660
+ )
661
+ g = get_dcg()
662
+ fmt = getattr(args, "format", "table")
663
+
664
+ # ── classify ──────────────────────────────────────────────────────────
665
+ if args.dcg_cmd in ("classify", "cls"):
666
+ letters = [c for c in args.text if c.isalpha()]
667
+ if not letters:
668
+ raise InputError("No alphabetic characters in input")
669
+ records = []
670
+ for ch in letters:
671
+ sc = symmetry_class(ch)
672
+ e = sym_energy(ch)
673
+ vec = vectorize(ch)
674
+ records.append({"char": ch, "class": sc, "energy": round(e, 6),
675
+ "vector": [round(x, 6) for x in vec]})
676
+ if fmt == "json":
677
+ print(json.dumps(records, ensure_ascii=False, indent=2))
678
+ return 0
679
+ print(section("dcg · classify", enabled=use_color))
680
+ t = Box(["char", "form", "class", "energy",
681
+ "v₀ pos", "v₁ tid", "v₂ √E", "v₃ sinθ"],
682
+ col_align=["c", "c", "l", "r", "r", "r", "r", "r"],
683
+ enabled=use_color)
684
+ for r in records:
685
+ ch = r["char"]
686
+ sc = r["class"]
687
+ e = r["energy"]
688
+ vec = r["vector"]
689
+ form = "DC" if ch.isupper() else "ac"
690
+ form_s = colorize("DC", "bold", use_color) if ch.isupper() \
691
+ else colorize("ac", "dim", use_color)
692
+ t.row([colorize(ch, "bold", use_color),
693
+ form_s,
694
+ fmt_class(sc, use_color),
695
+ fmt_float_e(e, use_color),
696
+ colorize(f"{vec[0]:.1f}", "dim", use_color),
697
+ colorize(f"{vec[1]:.0f}", "dim", use_color),
698
+ colorize(f"{vec[2]:.4f}", "cyan", use_color),
699
+ colorize(f"{vec[3]:.4f}", "yellow",use_color)])
700
+ print(t.render())
701
+ return 0
702
+
703
+ # ── path ──────────────────────────────────────────────────────────────
704
+ if args.dcg_cmd in ("path", "p"):
705
+ info = g.word_path_info(args.word)
706
+ if fmt == "json":
707
+ print(json.dumps(info, ensure_ascii=False, indent=2))
708
+ return 0
709
+ print(section("dcg · path", enabled=use_color))
710
+ steps = info["steps"]
711
+ t = Box(["#", "char", "class", "energy", "from", "✓"],
712
+ col_align=["r", "c", "l", "r", "c", "c"], enabled=use_color)
713
+ invalid_count = 0
714
+ for i, s in enumerate(steps):
715
+ ef = colorize(s.get("edge_from", "—"), "dim", use_color)
716
+ if "valid_step" not in s:
717
+ val_s = colorize("—", "dim", use_color)
718
+ else:
719
+ v = s["valid_step"]
720
+ val_s = fmt_valid(v, use_color)
721
+ if not v:
722
+ invalid_count += 1
723
+ t.row([colorize(str(i), "dim", use_color),
724
+ colorize(s["char"], "bold", use_color),
725
+ fmt_class(s["class"], use_color),
726
+ fmt_float_e(s["energy"], use_color),
727
+ ef, val_s])
728
+ print(t.render())
729
+ if info["is_valid_path"]:
730
+ path_s = fmt_valid(True, use_color) + colorize(" valid path", "bright_green", use_color)
731
+ else:
732
+ path_s = fmt_valid(False, use_color) + colorize(f" {invalid_count} invalid step(s)", "bright_red", use_color)
733
+ e_s = colorize(f"{info['total_energy']:.4f}", "bright_white", use_color)
734
+ print(kv("energy", e_s, use_color) + " " + path_s)
735
+ print(kv("vector", colorize(str([round(x,3) for x in info["vector_sum"]]), "dim", use_color), use_color))
736
+ return 0
737
+
738
+ # ── vector ────────────────────────────────────────────────────────────
739
+ if args.dcg_cmd in ("vector", "vec"):
740
+ vec = word_vector(args.word)
741
+ e = word_sym_energy(args.word)
742
+ if fmt == "json":
743
+ print(json.dumps({"word": args.word, "total_energy": round(e, 6),
744
+ "vector": [round(x, 6) for x in vec]}, indent=2))
745
+ return 0
746
+ print(section("dcg · vector", enabled=use_color))
747
+ labels = ["pos_sum", "type_id_sum", "√E_sum", "sinθ_sum"]
748
+ colors = ["white", "dim", "cyan", "yellow"]
749
+ t = Box(["dim", "label", "value"], col_align=["c", "l", "r"], enabled=use_color)
750
+ for i, (lbl, col, val) in enumerate(zip(labels, colors, vec)):
751
+ t.row([colorize(str(i), "dim", use_color),
752
+ colorize(lbl, col, use_color),
753
+ colorize(f"{val:.4f}", "bright_white", use_color)])
754
+ print(t.render())
755
+ print(kv("total_energy", fmt_float_e(e, use_color), use_color))
756
+ return 0
757
+
758
+ # ── shortest ──────────────────────────────────────────────────────────
759
+ if args.dcg_cmd in ("shortest", "sp"):
760
+ if len(args.src) != 1 or not args.src.isalpha():
761
+ raise InputError("src must be a single alphabetic character")
762
+ if len(args.dst) != 1 or not args.dst.isalpha():
763
+ raise InputError("dst must be a single alphabetic character")
764
+ path = g.shortest_path(args.src, args.dst)
765
+ if path is None:
766
+ raise InputError(f"No path from {args.src!r} to {args.dst!r}")
767
+ cost = g.shortest_path_cost(args.src, args.dst)
768
+ if fmt == "json":
769
+ detail = [{"char": c, "class": symmetry_class(c), "energy": round(sym_energy(c), 6)}
770
+ for c in path]
771
+ print(json.dumps({"src": args.src, "dst": args.dst, "path": detail,
772
+ "length": len(path), "cost": cost}, indent=2))
773
+ return 0
774
+ print(section("dcg · shortest path", enabled=use_color))
775
+ t = Box(["#", "char", "class", "energy", "cost"],
776
+ col_align=["r", "c", "l", "r", "r"], enabled=use_color)
777
+ for i, ch in enumerate(path):
778
+ w = g.edge_weight(path[i-1], ch) if i > 0 else None
779
+ w_s = colorize(f"{w:.1f}", "dim", use_color) if w else colorize("—", "dim", use_color)
780
+ t.row([colorize(str(i), "dim", use_color),
781
+ colorize(ch, "bold", use_color),
782
+ fmt_class(symmetry_class(ch), use_color),
783
+ fmt_float_e(sym_energy(ch), use_color),
784
+ w_s])
785
+ print(t.render())
786
+ # Arrow chain
787
+ arrow = colorize(" → ", "dim", use_color)
788
+ chain = arrow.join(colorize(c, "bold", use_color) for c in path)
789
+ print(chain)
790
+ print(kv("length", colorize(str(len(path)), "bright_white", use_color), use_color)
791
+ + " " + kv("cost", colorize(f"{cost:.1f}", "yellow", use_color), use_color))
792
+ return 0
793
+
794
+ # ── homotopy ──────────────────────────────────────────────────────────
795
+ if args.dcg_cmd in ("homotopy", "hom"):
796
+ equiv, detail = homotopy_equivalent(args.word_a, args.word_b, tol=args.tol)
797
+ if fmt == "json":
798
+ print(json.dumps(detail, indent=2))
799
+ return 0
800
+ print(section("dcg · homotopy", enabled=use_color))
801
+ wa_s = colorize(args.word_a, "bold", use_color)
802
+ wb_s = colorize(args.word_b, "cyan", use_color)
803
+ t = Box(["metric", wa_s, wb_s, ""],
804
+ col_align=["l", "r", "r", "l"], enabled=use_color)
805
+ t.row([colorize("energy", "dim", use_color),
806
+ colorize(str(detail["word_a"]["energy"]), "cyan", use_color),
807
+ colorize(str(detail["word_b"]["energy"]), "cyan", use_color), ""])
808
+ t.divider()
809
+ t.row([colorize("energy_diff", "dim", use_color),
810
+ colorize(str(detail["energy_diff"]), "yellow", use_color), "", ""])
811
+ t.row([colorize("vector_distance", "dim", use_color),
812
+ colorize(str(detail["vector_distance"]), "yellow", use_color), "", ""])
813
+ print(t.render())
814
+ if equiv:
815
+ verdict = (fmt_valid(True, use_color) + " "
816
+ + colorize("EQUIVALENT", "bright_green", use_color)
817
+ + colorize(f" (tol={args.tol})", "dim", use_color))
818
+ else:
819
+ verdict = (fmt_valid(False, use_color) + " "
820
+ + colorize("NOT equivalent", "bright_red", use_color)
821
+ + colorize(f" (tol={args.tol})", "dim", use_color))
822
+ print(" " + verdict)
823
+ return 0
824
+
825
+ # ── info ──────────────────────────────────────────────────────────────
826
+ if args.dcg_cmd == "info":
827
+ nc = g.node_count()
828
+ ec = g.edge_count()
829
+ by_type = g.edge_count_by_type()
830
+ if fmt == "json":
831
+ print(json.dumps({"nodes": nc, "edges": ec, "edges_by_type": by_type}, indent=2))
832
+ return 0
833
+ print(section("dcg · info", enabled=use_color))
834
+ _type_color = {
835
+ "dc_ac": "bright_cyan",
836
+ "within_idempotent": "cyan", "within_biphasic": "yellow",
837
+ "within_involutive": "magenta", "within_asymmetric": "red",
838
+ "asymmetric_to_biphasic": "bright_red", "biphasic_to_idempotent": "bright_yellow",
839
+ "idempotent_to_involutive": "bright_cyan", "involutive_to_asymmetric": "bright_magenta",
840
+ }
841
+ t = Box(["edge type", "count"], col_align=["l", "r"], enabled=use_color)
842
+ for k, v in sorted(by_type.items()):
843
+ col = _type_color.get(k)
844
+ t.row([colorize(k, col, use_color), colorize(str(v), "bright_white", use_color)])
845
+ print(t.render())
846
+ print(kv("nodes", colorize(str(nc), "bright_white", use_color), use_color)
847
+ + " " + kv("edges", colorize(str(ec), "bright_white", use_color), use_color))
848
+ return 0
849
+
850
+ if args.cmd == "crypto":
851
+ from .crypto import encrypt as c_encrypt, decrypt as c_decrypt, encrypt_secure, decrypt_secure
852
+ text = read_input(getattr(args, 'text', None), getattr(args, 'file', None))
853
+ mode = getattr(args, 'mode', 'edpc')
854
+ if args.crypto_cmd == 'encrypt':
855
+ print(c_encrypt(text, args.key, principle) if mode == 'edpc' else encrypt_secure(text, args.key))
856
+ return 0
857
+ if args.crypto_cmd == 'decrypt':
858
+ print(c_decrypt(text, args.key, principle) if mode == 'edpc' else decrypt_secure(text, args.key))
859
+ return 0
860
+ if args.cmd == "reset":
861
+ target_state = getattr(args, "state", None)
862
+ only_rules = bool(args.rules_only)
863
+ only_symbols = bool(args.symbols_only)
864
+ do_plugins = bool(args.plugins or args.all)
865
+ # Confirmation unless --yes
866
+ if not getattr(args, "yes", False):
867
+ print("This will reset your Gdk9 environment:")
868
+ print(f"- State file: {'default (~/.gdk9/state.json)' if not target_state else target_state}")
869
+ print(f"- State scope: {'rules-only' if only_rules else ('symbols-only' if only_symbols else 'full')}")
870
+ print(f"- Plugins config: {'yes' if do_plugins else 'no'}")
871
+ resp = input("Proceed? type 'yes' to confirm: ").strip().lower()
872
+ if resp != 'yes':
873
+ print("aborted")
874
+ return 2
875
+ # Reset state
876
+ if only_rules or only_symbols:
877
+ cur = load_state(target_state)
878
+ if only_rules:
879
+ cur['rules'] = {}
880
+ if only_symbols:
881
+ cur['symbols'] = {}
882
+ save_state(cur, target_state)
883
+ else:
884
+ save_state({"symbols": {}, "rules": {}}, target_state)
885
+ # Reset plugins if requested
886
+ if do_plugins:
887
+ from pathlib import Path
888
+ plugins_reset_config()
889
+ print(json.dumps({"ok": True, "reset": {"state": True, "plugins": do_plugins, "scope": ("rules-only" if only_rules else ("symbols-only" if only_symbols else "full")) }}, indent=2))
890
+ return 0
891
+ if args.cmd == "state":
892
+ base = load_state(state_path)
893
+ incoming = load_state(args.incoming)
894
+ merged, stats = merge_state(base, incoming)
895
+ if not args.dry_run:
896
+ save_state(merged, state_path)
897
+ print(
898
+ json.dumps(
899
+ {
900
+ "ok": True,
901
+ "state": state_path or "~/.gdk9/state.json",
902
+ "dry_run": bool(args.dry_run),
903
+ "stats": stats,
904
+ },
905
+ indent=2,
906
+ )
907
+ )
908
+ return 0
909
+ if args.cmd == "tokenize":
910
+ text = read_input(args.text, args.file)
911
+ keep = bool(getattr(args, "keep_delims", False)) or (not bool(getattr(args, "drop_delims", False)))
912
+ strip_tokens = not bool(args.no_strip)
913
+ if args.format == "json":
914
+ print(
915
+ to_json_payload(
916
+ text,
917
+ principle,
918
+ energy=args.energy,
919
+ delims=args.delims,
920
+ keep_delims=keep,
921
+ strip_tokens=strip_tokens,
922
+ )
923
+ )
924
+ return 0
925
+ if args.format == "annotate":
926
+ print(
927
+ annotate_text(
928
+ text,
929
+ principle,
930
+ energy=args.energy,
931
+ delims=args.delims,
932
+ keep_delims=keep,
933
+ strip_tokens=strip_tokens,
934
+ )
935
+ )
936
+ return 0
937
+ # table
938
+ if not getattr(args, "summary_only", False):
939
+ rows = summarize_tokens_table(
940
+ text,
941
+ principle,
942
+ energy=args.energy,
943
+ delims=args.delims,
944
+ keep_delims=keep,
945
+ strip_tokens=strip_tokens,
946
+ use_color=(getattr(args, "color", False)) or (supports_color() and not getattr(args, "no_color", False)),
947
+ )
948
+ print_table(rows, use_color)
949
+ # footer with metrics and delimiter set unless suppressed
950
+ if not getattr(args, "no_footer", False):
951
+ dset = args.delims if args.delims is not None else delimiter_set(principle, energy=args.energy)
952
+ from .tokenize import tokens_with_energy, summarize_metrics_lines
953
+ toks = tokens_with_energy(
954
+ text,
955
+ principle,
956
+ energy=args.energy,
957
+ delims=args.delims,
958
+ keep_delims=keep,
959
+ strip_tokens=strip_tokens,
960
+ )
961
+ for line in summarize_metrics_lines(text, toks, principle, dset, use_color):
962
+ print(line)
963
+ return 0
964
+ if args.cmd == "principles":
965
+ return cmd_principles(args, principle)
966
+ if args.cmd == "optimize":
967
+ return cmd_optimize(args, principle)
968
+ if args.cmd == "help":
969
+ topic = getattr(args, "topic", None)
970
+ if not topic:
971
+ parser.print_help()
972
+ return 0
973
+ # try subparser help
974
+ try:
975
+ sub = parser._subparsers._group_actions[0].choices[topic] # type: ignore[attr-defined]
976
+ except Exception:
977
+ # try alias resolution
978
+ for k, v in parser._subparsers._group_actions[0].choices.items(): # type: ignore[attr-defined]
979
+ if hasattr(v, 'aliases') and topic in getattr(v, 'aliases', []):
980
+ sub = v
981
+ break
982
+ else:
983
+ print(f"Unknown help topic: {topic}")
984
+ return 2
985
+ sub.print_help()
986
+ return 0
987
+ if args.cmd == "handbook":
988
+ from pathlib import Path
989
+ here = Path(__file__).resolve().parent
990
+ hb = here.parent / 'docs' / 'HANDBOOK.md'
991
+ if hb.exists():
992
+ print(hb.read_text(encoding='utf-8'))
993
+ else:
994
+ print("Handbook not found in package; refer to docs/HANDBOOK.md in the repository.")
995
+ return 0
996
+ if args.cmd == "tui":
997
+ from .tui import start, TuiOptions
998
+ return start(principle, TuiOptions(target=args.target, allowed=args.allowed, method=args.method))
999
+ if args.cmd == "subs":
1000
+ if args.action == "example":
1001
+ print(json.dumps({"subs": {"i": ["1", "!"], "s": ["$"], "e": ["3"]}, "allowed_inserts": ".!?+"}, indent=2))
1002
+ return 0
1003
+ if args.action == "generate":
1004
+ from .subs import build_subs_json
1005
+ payload = build_subs_json(principle, limit=args.limit)
1006
+ if args.output:
1007
+ from pathlib import Path
1008
+ Path(args.output).write_text(payload, encoding='utf-8')
1009
+ print(args.output)
1010
+ else:
1011
+ print(payload)
1012
+ return 0
1013
+ if args.cmd == "symbol":
1014
+ state = load_state(state_path)
1015
+ if args.symbol_cmd == "add":
1016
+ from .imply import validate_symbol_name
1017
+ validate_symbol_name(args.name)
1018
+ set_symbol(state, args.name, float(args.energy))
1019
+ save_state(state, state_path)
1020
+ print(json.dumps({"ok": True, "symbol": {args.name: float(args.energy)}}, indent=2))
1021
+ return 0
1022
+ if args.symbol_cmd == "list":
1023
+ print(json.dumps({"symbols": list_symbols(load_state(state_path))}, indent=2))
1024
+ return 0
1025
+ if args.cmd == "kernel":
1026
+ return run_kernel(args, principle)
1027
+ if args.cmd == "imply":
1028
+ state = load_state(state_path)
1029
+ rules = state.setdefault("rules", {})
1030
+ if args.imply_cmd == "define-fusion":
1031
+ r = make_fusion(args.rule, args.out_name, args.arity)
1032
+ set_rule(state, r.name, r.to_json())
1033
+ save_state(state, state_path)
1034
+ print(json.dumps({"ok": True, "rule": r.to_json()}, indent=2))
1035
+ return 0
1036
+ if args.imply_cmd == "define-split":
1037
+ r = make_split(args.rule, args.out_a, args.out_b, args.ratio)
1038
+ set_rule(state, r.name, r.to_json())
1039
+ save_state(state, state_path)
1040
+ print(json.dumps({"ok": True, "rule": r.to_json()}, indent=2))
1041
+ return 0
1042
+ if args.imply_cmd == "list":
1043
+ print(json.dumps({"rules": rules}, indent=2))
1044
+ return 0
1045
+ if args.imply_cmd == "apply":
1046
+ if args.rule not in rules:
1047
+ raise InputError(f"Unknown rule: {args.rule}")
1048
+ rj = rules[args.rule]
1049
+ r = Rule(name=rj["name"], type=rj["type"], arity=int(rj["arity"]), params=rj.get("params", {}))
1050
+ tol = 1e-9
1051
+ if isinstance(principle.description, str) and "tolerance" in principle.description:
1052
+ pass
1053
+ res = apply_rule(r, state.get("symbols", {}), args.inputs, tol=tol)
1054
+ if args.commit:
1055
+ # write outputs back into symbols
1056
+ for o in res.get("outputs", []):
1057
+ set_symbol(state, o["name"], float(o["energy"]))
1058
+ save_state(state, state_path)
1059
+ res = {**res, "committed": True}
1060
+ print(json.dumps(res, indent=2))
1061
+ return 0
1062
+ if args.cmd == "plugin":
1063
+ if args.plugin_cmd == "list":
1064
+ print(json.dumps({"plugins": plugins_list_available()}, indent=2))
1065
+ return 0
1066
+ if args.plugin_cmd == "validate":
1067
+ ppath = plugins_find(args.name_or_path)
1068
+ _ = plugins_load(ppath)
1069
+ print(json.dumps({"ok": True, "path": str(ppath)}, indent=2))
1070
+ return 0
1071
+ if args.plugin_cmd == "info":
1072
+ ppath = plugins_find(args.name_or_path)
1073
+ plg = plugins_load(ppath)
1074
+ print(json.dumps({
1075
+ "name": plg.name,
1076
+ "version": plg.version,
1077
+ "description": plg.description,
1078
+ "rules": [r.to_json() for r in plg.rules],
1079
+ "symbol_energy": plg.symbol_energy,
1080
+ "symbols": plg.symbols,
1081
+ "source": str(plg.source),
1082
+ }, ensure_ascii=False, indent=2))
1083
+ return 0
1084
+ if args.plugin_cmd == "load":
1085
+ ppath = plugins_find(args.name_or_path)
1086
+ plg = plugins_load(ppath)
1087
+ pr, st, stats = plugins_apply(plg, principle, load_state(state_path))
1088
+ save_state(st, state_path)
1089
+ if not args.no_enable:
1090
+ plugins_enable(plg.name, ppath)
1091
+ print(json.dumps({"ok": True, "name": plg.name, "applied": stats, "enabled": (not args.no_enable)}, indent=2))
1092
+ return 0
1093
+ if args.plugin_cmd == "enable":
1094
+ ppath = plugins_find(args.name_or_path)
1095
+ plg = plugins_load(ppath)
1096
+ plugins_enable(plg.name, ppath)
1097
+ print(json.dumps({"ok": True, "enabled": plg.name}, indent=2))
1098
+ return 0
1099
+ if args.plugin_cmd == "disable":
1100
+ plugins_disable(args.name)
1101
+ print(json.dumps({"ok": True, "disabled": args.name}, indent=2))
1102
+ return 0
1103
+ if args.cmd == "repl":
1104
+ # Minimal REPL
1105
+ spath = state_path
1106
+ print("gdk9 repl. Type 'help' or 'quit'.")
1107
+ while True:
1108
+ try:
1109
+ line = input("gdk9> ").strip()
1110
+ except EOFError:
1111
+ break
1112
+ if not line:
1113
+ continue
1114
+ if line in {"quit", "exit"}:
1115
+ break
1116
+ if line == "help":
1117
+ print("commands: symbol add <NAME> <E>, symbol list, imply define-fusion <RULE> <OUT> <ARITY>, imply define-split <RULE> <A> <B> <RATIO>, imply list, imply apply <RULE> <INPUTS...>")
1118
+ continue
1119
+ try:
1120
+ parts = line.split()
1121
+ if parts[:2] == ["symbol", "add"] and len(parts) == 4:
1122
+ ns, e = parts[2], float(parts[3])
1123
+ st = load_state(spath)
1124
+ from .imply import validate_symbol_name
1125
+ validate_symbol_name(ns)
1126
+ set_symbol(st, ns, e)
1127
+ save_state(st, spath)
1128
+ print("ok")
1129
+ continue
1130
+ if parts[:2] == ["symbol", "list"]:
1131
+ print(json.dumps({"symbols": list_symbols(load_state(spath))}, indent=2))
1132
+ continue
1133
+ if parts[:3] == ["imply", "define-fusion",] and len(parts) == 5:
1134
+ _, _, rn, out, ar = parts
1135
+ st = load_state(spath)
1136
+ r = make_fusion(rn, out, int(ar))
1137
+ set_rule(st, rn, r.to_json())
1138
+ save_state(st, spath)
1139
+ print("ok")
1140
+ continue
1141
+ if parts[:3] == ["imply", "define-split"] and len(parts) == 6:
1142
+ _, _, rn, a, b, ratio = parts
1143
+ st = load_state(spath)
1144
+ r = make_split(rn, a, b, float(ratio))
1145
+ set_rule(st, rn, r.to_json())
1146
+ save_state(st, spath)
1147
+ print("ok")
1148
+ continue
1149
+ if parts[:2] == ["imply", "list"]:
1150
+ print(json.dumps({"rules": load_state(spath).get("rules", {})}, indent=2))
1151
+ continue
1152
+ if parts[:2] == ["imply", "apply"] and len(parts) >= 4:
1153
+ _, _, rn, *ins = parts
1154
+ commit = False
1155
+ if ins and ins[-1] in {"--commit", "commit"}:
1156
+ commit = True
1157
+ ins = ins[:-1]
1158
+ st = load_state(spath)
1159
+ rj = st.get("rules", {}).get(rn)
1160
+ if not rj:
1161
+ print("error: unknown rule")
1162
+ continue
1163
+ r = Rule(name=rj["name"], type=rj["type"], arity=int(rj["arity"]), params=rj.get("params", {}))
1164
+ res = apply_rule(r, st.get("symbols", {}), ins, tol=1e-9)
1165
+ if commit:
1166
+ for o in res.get("outputs", []):
1167
+ set_symbol(st, o["name"], float(o["energy"]))
1168
+ save_state(st, spath)
1169
+ res = {**res, "committed": True}
1170
+ print(json.dumps(res, indent=2))
1171
+ continue
1172
+ print("error: unknown command")
1173
+ except Exception as exc:
1174
+ print(f"error: {exc}")
1175
+ return 0
1176
+ parser.print_help()
1177
+ return 0
1178
+ except (Gdk9Error, ConfigError, InputError, OptimizationError) as exc:
1179
+ print(f"error: {exc}", file=sys.stderr)
1180
+ return 2
1181
+ except BrokenPipeError:
1182
+ return 0
1183
+
1184
+
1185
+ if __name__ == "__main__": # pragma: no cover
1186
+ raise SystemExit(main())