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.
- examples/01_analyze.py +28 -0
- examples/02_profile.py +24 -0
- examples/03_assign.py +19 -0
- examples/04_dcg.py +41 -0
- examples/05_tokenize.py +22 -0
- examples/06_synthesize.py +20 -0
- examples/07_api.py +57 -0
- examples/08_kernel.py +35 -0
- examples/09_kernel_search_walkthrough.py +102 -0
- examples/10_conserve_vs_naive.py +100 -0
- gdk9/__init__.py +5 -0
- gdk9/ansi.py +57 -0
- gdk9/cli.py +1186 -0
- gdk9/crdt.py +88 -0
- gdk9/crypto.py +132 -0
- gdk9/data/ninefold.json +40 -0
- gdk9/data/official.example.json +16 -0
- gdk9/data/official.json +132 -0
- gdk9/data/subs.json +436 -0
- gdk9/dcg.py +356 -0
- gdk9/egglog_bridge/__init__.py +6 -0
- gdk9/egglog_bridge/dr_egglog.py +107 -0
- gdk9/egglog_bridge/fallback.py +34 -0
- gdk9/energy.py +211 -0
- gdk9/errors.py +15 -0
- gdk9/fmt.py +212 -0
- gdk9/imply.py +83 -0
- gdk9/io_utils.py +24 -0
- gdk9/kernel/__init__.py +24 -0
- gdk9/kernel/engine.py +123 -0
- gdk9/kernel/errors.py +17 -0
- gdk9/kernel/expression.py +43 -0
- gdk9/kernel/principle.py +43 -0
- gdk9/kernel/proof.py +24 -0
- gdk9/kernel/rule.py +30 -0
- gdk9/kernel/symbol.py +19 -0
- gdk9/kernel_cli.py +195 -0
- gdk9/log.py +30 -0
- gdk9/optimize.py +288 -0
- gdk9/parser.py +35 -0
- gdk9/plugins/__init__.py +9 -0
- gdk9/plugins/loader.py +346 -0
- gdk9/plugins/registry.py +42 -0
- gdk9/principles.py +131 -0
- gdk9/saturate.py +89 -0
- gdk9/state.py +138 -0
- gdk9/subs.py +108 -0
- gdk9/tokenize.py +386 -0
- gdk9/tui.py +115 -0
- gdk9/utilization.py +15 -0
- gdk9_cli-0.3.0.dist-info/METADATA +295 -0
- gdk9_cli-0.3.0.dist-info/RECORD +82 -0
- gdk9_cli-0.3.0.dist-info/WHEEL +5 -0
- gdk9_cli-0.3.0.dist-info/entry_points.txt +3 -0
- gdk9_cli-0.3.0.dist-info/licenses/LICENSE +661 -0
- gdk9_cli-0.3.0.dist-info/top_level.txt +4 -0
- scripts/batch_analyze.py +109 -0
- scripts/export_json.py +125 -0
- scripts/make_zip.py +57 -0
- scripts/profile_compare.py +165 -0
- scripts/watch_file.py +124 -0
- tests/conftest.py +26 -0
- tests/experiment/__init__.py +1 -0
- tests/experiment/test_conserve_vs_naive.py +287 -0
- tests/kernel/test_kernel_energy.py +18 -0
- tests/kernel/test_kernel_rules.py +58 -0
- tests/kernel/test_kernel_search_bound.py +34 -0
- tests/test_crypto.py +36 -0
- tests/test_crypto_secure.py +26 -0
- tests/test_dcg.py +318 -0
- tests/test_dcg_golden.py +67 -0
- tests/test_egglog_bridge.py +41 -0
- tests/test_energy.py +34 -0
- tests/test_kernel_cli_handbook.py +101 -0
- tests/test_optimize.py +21 -0
- tests/test_plugins.py +64 -0
- tests/test_rules.py +43 -0
- tests/test_rules_commit.py +40 -0
- tests/test_rules_reversibility.py +50 -0
- tests/test_state_crdt.py +87 -0
- tests/test_tokenize.py +38 -0
- 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())
|