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/energy.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Dict, Iterable, List, Tuple
|
|
6
|
+
|
|
7
|
+
from .principles import Principle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def digital_root(n: int, zero_to_nine: bool = True) -> int:
|
|
11
|
+
n = abs(int(n))
|
|
12
|
+
if n == 0:
|
|
13
|
+
return 9 if zero_to_nine else 0
|
|
14
|
+
return 9 if (n % 9 == 0) else (n % 9)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def letter_value(ch: str) -> int:
|
|
18
|
+
c = ch.upper()
|
|
19
|
+
if "A" <= c <= "Z":
|
|
20
|
+
return ord(c) - ord("A") + 1
|
|
21
|
+
return ord(c)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def char_energy(ch: str, principle: Principle) -> int:
|
|
25
|
+
if ch.isspace():
|
|
26
|
+
return 0
|
|
27
|
+
if ch.isalpha():
|
|
28
|
+
base = letter_value(ch) if principle.letter_mode == "a1z26" else ord(ch)
|
|
29
|
+
e = digital_root(base, principle.normalize_zero_to_nine)
|
|
30
|
+
return digital_root(e * principle.weights.get("letter", 1), principle.normalize_zero_to_nine)
|
|
31
|
+
if ch.isdigit():
|
|
32
|
+
base = int(ch)
|
|
33
|
+
e = digital_root(base, principle.normalize_zero_to_nine)
|
|
34
|
+
return digital_root(e * principle.weights.get("digit", 1), principle.normalize_zero_to_nine)
|
|
35
|
+
if ch in principle.symbol_energy:
|
|
36
|
+
e = digital_root(principle.symbol_energy[ch], principle.normalize_zero_to_nine)
|
|
37
|
+
return digital_root(e * principle.weights.get("symbol", 1), principle.normalize_zero_to_nine)
|
|
38
|
+
# Fallback for other unicode symbols
|
|
39
|
+
e = digital_root(ord(ch), principle.normalize_zero_to_nine)
|
|
40
|
+
return digital_root(e * principle.weights.get("symbol", 1), principle.normalize_zero_to_nine)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def string_energy(text: str, principle: Principle) -> Tuple[int, int]:
|
|
44
|
+
total = sum(char_energy(ch, principle) for ch in text)
|
|
45
|
+
return total, digital_root(total, principle.normalize_zero_to_nine)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tokenize_sentences(text: str) -> List[str]:
|
|
49
|
+
# Simple sentence split on .!? while retaining textual coherence
|
|
50
|
+
parts = re.split(r"(?<=[.!?])\s+", text.strip())
|
|
51
|
+
return [p for p in parts if p]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def tokenize_words(text: str) -> List[str]:
|
|
55
|
+
return re.findall(r"\b\w+\b", text, flags=re.UNICODE)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class UnitEnergy:
|
|
60
|
+
unit: str
|
|
61
|
+
value: str
|
|
62
|
+
energy: int
|
|
63
|
+
total: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def analyze_text(text: str, principle: Principle) -> Dict[str, Iterable[UnitEnergy]]:
|
|
67
|
+
chars: List[UnitEnergy] = []
|
|
68
|
+
for ch in text:
|
|
69
|
+
e = char_energy(ch, principle)
|
|
70
|
+
total = e
|
|
71
|
+
chars.append(UnitEnergy("char", ch, e, total))
|
|
72
|
+
|
|
73
|
+
words: List[UnitEnergy] = []
|
|
74
|
+
for w in tokenize_words(text):
|
|
75
|
+
total, dr = string_energy(w, principle)
|
|
76
|
+
words.append(UnitEnergy("word", w, dr, total))
|
|
77
|
+
|
|
78
|
+
sentences: List[UnitEnergy] = []
|
|
79
|
+
for s in tokenize_sentences(text):
|
|
80
|
+
total, dr = string_energy(s, principle)
|
|
81
|
+
sentences.append(UnitEnergy("sentence", s, dr, total))
|
|
82
|
+
|
|
83
|
+
paragraphs: List[UnitEnergy] = []
|
|
84
|
+
for p in [pp for pp in text.splitlines() if pp.strip()]:
|
|
85
|
+
total, dr = string_energy(p, principle)
|
|
86
|
+
paragraphs.append(UnitEnergy("paragraph", p, dr, total))
|
|
87
|
+
|
|
88
|
+
doc_total, doc_dr = string_energy(text, principle)
|
|
89
|
+
doc = [UnitEnergy("document", text, doc_dr, doc_total)]
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"chars": chars,
|
|
93
|
+
"words": words,
|
|
94
|
+
"sentences": sentences,
|
|
95
|
+
"paragraphs": paragraphs,
|
|
96
|
+
"document": doc,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def vector_energy(text: str, principle: Principle) -> Dict[str, Dict[str, int]]:
|
|
101
|
+
letters = 0
|
|
102
|
+
digits = 0
|
|
103
|
+
symbols = 0
|
|
104
|
+
for ch in text:
|
|
105
|
+
if ch.isspace():
|
|
106
|
+
continue
|
|
107
|
+
if ch.isalpha():
|
|
108
|
+
letters += char_energy(ch, principle)
|
|
109
|
+
elif ch.isdigit():
|
|
110
|
+
digits += char_energy(ch, principle)
|
|
111
|
+
else:
|
|
112
|
+
symbols += char_energy(ch, principle)
|
|
113
|
+
return {
|
|
114
|
+
"sum": {"letters": letters, "digits": digits, "symbols": symbols},
|
|
115
|
+
"dr": {
|
|
116
|
+
"letters": digital_root(letters, principle.normalize_zero_to_nine),
|
|
117
|
+
"digits": digital_root(digits, principle.normalize_zero_to_nine),
|
|
118
|
+
"symbols": digital_root(symbols, principle.normalize_zero_to_nine),
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def harmonic_triads(text: str, principle: Principle) -> Dict[str, int]:
|
|
124
|
+
# Group by residue classes: 1/4/7=root, 2/5/8=wave, 3/6/9=peak
|
|
125
|
+
triads = {"root": 0, "wave": 0, "peak": 0}
|
|
126
|
+
for ch in text:
|
|
127
|
+
e = char_energy(ch, principle)
|
|
128
|
+
if e == 0:
|
|
129
|
+
continue
|
|
130
|
+
r = e % 9
|
|
131
|
+
if r in (1, 4, 7):
|
|
132
|
+
triads["root"] += 1
|
|
133
|
+
elif r in (2, 5, 8):
|
|
134
|
+
triads["wave"] += 1
|
|
135
|
+
else:
|
|
136
|
+
triads["peak"] += 1
|
|
137
|
+
return triads
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def energy_profile(text: str, principle: Principle) -> Dict[str, int]:
|
|
141
|
+
profile: Dict[str, int] = {str(i): 0 for i in range(1, 10)}
|
|
142
|
+
for ch in text:
|
|
143
|
+
e = char_energy(ch, principle)
|
|
144
|
+
key = str(9 if e == 0 else e)
|
|
145
|
+
profile[key] += 1
|
|
146
|
+
return profile
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def required_delta(current: int, target: int, zero_to_nine: bool = True) -> int:
|
|
150
|
+
if current == target:
|
|
151
|
+
return 0
|
|
152
|
+
# Determine minimal addition to move digital root to target
|
|
153
|
+
# Try deltas 1..100 and pick the first that matches
|
|
154
|
+
for d in range(1, 101):
|
|
155
|
+
if digital_root(current + d, zero_to_nine) == target:
|
|
156
|
+
return d
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def attune_text(
|
|
161
|
+
text: str,
|
|
162
|
+
target: int,
|
|
163
|
+
principle: Principle,
|
|
164
|
+
method: str = "append",
|
|
165
|
+
symbol: str = ".",
|
|
166
|
+
) -> Tuple[str, int, int, int]:
|
|
167
|
+
total, dr = string_energy(text, principle)
|
|
168
|
+
if not (1 <= target <= 9):
|
|
169
|
+
raise ValueError("Target energy must be 1..9")
|
|
170
|
+
if dr == target:
|
|
171
|
+
return text, total, dr, 0
|
|
172
|
+
if method != "append":
|
|
173
|
+
raise ValueError("Only 'append' method supported in default build")
|
|
174
|
+
sym_e = char_energy(symbol, principle)
|
|
175
|
+
delta = required_delta(total, target, principle.normalize_zero_to_nine)
|
|
176
|
+
if sym_e == 0:
|
|
177
|
+
raise ValueError("Chosen symbol has zero energy; pick another.")
|
|
178
|
+
count = max(1, (delta + sym_e - 1) // sym_e)
|
|
179
|
+
new_text = text + (symbol * count)
|
|
180
|
+
new_total, new_dr = string_energy(new_text, principle)
|
|
181
|
+
# Adjust if overshoot; ensure we hit target
|
|
182
|
+
if new_dr != target:
|
|
183
|
+
# Try up to 20 adjustments by adding one symbol until matched
|
|
184
|
+
tries = 0
|
|
185
|
+
while new_dr != target and tries < 20:
|
|
186
|
+
new_text += symbol
|
|
187
|
+
new_total, new_dr = string_energy(new_text, principle)
|
|
188
|
+
tries += 1
|
|
189
|
+
return new_text, new_total, new_dr, count
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def synthesize_sigil(text: str, principle: Principle, style: str = "grid") -> str:
|
|
193
|
+
total, dr = string_energy(text, principle)
|
|
194
|
+
prof = energy_profile(text, principle)
|
|
195
|
+
if style == "grid":
|
|
196
|
+
# Build a 3x3 grid where each cell repeats count of that energy
|
|
197
|
+
order = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
198
|
+
cells = ["" for _ in range(9)]
|
|
199
|
+
for i, k in enumerate(order):
|
|
200
|
+
cells[i] = str(prof[k])
|
|
201
|
+
rows = [" ".join(cells[i:i+3]) for i in range(0, 9, 3)]
|
|
202
|
+
title = f"DR={dr} TOTAL={total}"
|
|
203
|
+
return title + "\n" + "\n".join(rows)
|
|
204
|
+
if style == "bar":
|
|
205
|
+
bars = []
|
|
206
|
+
for i in range(1, 10):
|
|
207
|
+
k = str(i)
|
|
208
|
+
count = prof[k]
|
|
209
|
+
bars.append(f"{i}: " + ("#" * min(count, 40)))
|
|
210
|
+
return "\n".join(bars)
|
|
211
|
+
return f"DR={dr} TOTAL={total}"
|
gdk9/errors.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Gdk9Error(Exception):
|
|
2
|
+
"""Base exception for Gdk9 CLI."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigError(Gdk9Error):
|
|
6
|
+
"""Configuration file or principle load error."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InputError(Gdk9Error):
|
|
10
|
+
"""Invalid input provided by user."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OptimizationError(Gdk9Error):
|
|
14
|
+
"""Unable to find or apply an attunement/optimization plan."""
|
|
15
|
+
|
gdk9/fmt.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""gdk9/fmt.py — Pretty terminal output helpers.
|
|
2
|
+
|
|
3
|
+
Box Bordered Unicode table with ANSI-aware column widths.
|
|
4
|
+
energy_bar Coloured block-bar for profile histograms.
|
|
5
|
+
fmt_class Symmetry class with colour coding.
|
|
6
|
+
fmt_dr Digital-root integer 1–9 with colour.
|
|
7
|
+
fmt_float_e Float energy value with colour.
|
|
8
|
+
fmt_valid ✓ / ✗ validity mark.
|
|
9
|
+
fmt_form DC / AC form label.
|
|
10
|
+
kv dim-key=bright-value inline pair.
|
|
11
|
+
section Dim rule + bold title header line.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import List, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
from .ansi import colorize, strip_ansi
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── ANSI-aware string helpers ─────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
def vlen(s: str) -> int:
|
|
23
|
+
"""Visible length of *s* (ANSI codes have zero width)."""
|
|
24
|
+
return len(strip_ansi(s))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def vpad(s: str, width: int, align: str = 'l') -> str:
|
|
28
|
+
"""Pad *s* to *width* visible characters, preserving embedded ANSI codes."""
|
|
29
|
+
diff = max(0, width - vlen(s))
|
|
30
|
+
if align == 'r':
|
|
31
|
+
return ' ' * diff + s
|
|
32
|
+
if align == 'c':
|
|
33
|
+
lp = diff // 2
|
|
34
|
+
return ' ' * lp + s + ' ' * (diff - lp)
|
|
35
|
+
return s + ' ' * diff
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Domain formatters ─────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
_CLASS_COLOR = {
|
|
41
|
+
'idempotent': 'cyan',
|
|
42
|
+
'biphasic': 'yellow',
|
|
43
|
+
'involutive': 'magenta',
|
|
44
|
+
'asymmetric': 'red',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_DR_COLOR = {
|
|
48
|
+
1: 'blue', 2: 'blue',
|
|
49
|
+
3: 'cyan', 4: 'green',
|
|
50
|
+
5: 'yellow', 6: 'magenta',
|
|
51
|
+
7: 'red', 8: 'bright_red',
|
|
52
|
+
9: 'bright_white',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Maps DR 1–9 to coloured block chars
|
|
56
|
+
_BAR_CHAR = '█'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fmt_class(cls: str, enabled: bool, *, full: bool = True) -> str:
|
|
60
|
+
"""Return symmetry *cls* with class-specific colour."""
|
|
61
|
+
return colorize(cls, _CLASS_COLOR.get(cls), enabled)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fmt_dr(dr: int, enabled: bool) -> str:
|
|
65
|
+
"""Return the digit *dr* (1–9) in its energy colour."""
|
|
66
|
+
return colorize(str(dr), _DR_COLOR.get(dr), enabled)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fmt_float_e(e: float, enabled: bool) -> str:
|
|
70
|
+
"""Format a float SymPhi energy with colour cues."""
|
|
71
|
+
s = f'{e:>9.4f}'
|
|
72
|
+
if not enabled:
|
|
73
|
+
return s
|
|
74
|
+
if e < 0:
|
|
75
|
+
return colorize(s, 'bright_magenta', True)
|
|
76
|
+
if e >= 10:
|
|
77
|
+
return colorize(s, 'bright_white', True)
|
|
78
|
+
return colorize(s, 'cyan', True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def fmt_valid(v: bool, enabled: bool) -> str:
|
|
82
|
+
if v:
|
|
83
|
+
return colorize('✓', 'bright_green', enabled)
|
|
84
|
+
return colorize('✗', 'bright_red', enabled)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def fmt_form(form: str, enabled: bool) -> str:
|
|
88
|
+
if form == 'upper':
|
|
89
|
+
return colorize('DC', 'bold', enabled)
|
|
90
|
+
return colorize('ac', 'dim', enabled)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def energy_bar(dr: int, count: int, max_count: int,
|
|
94
|
+
bar_width: int = 20, enabled: bool = True) -> str:
|
|
95
|
+
"""Return a coloured block-bar proportional to *count* / *max_count*."""
|
|
96
|
+
filled = round(count / max_count * bar_width) if max_count else 0
|
|
97
|
+
bar = _BAR_CHAR * filled
|
|
98
|
+
return colorize(bar, _DR_COLOR.get(dr), enabled)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def kv(key: str, value: str, enabled: bool = True, sep: str = '=') -> str:
|
|
102
|
+
"""Return a dim *key* + *value* inline pair.
|
|
103
|
+
|
|
104
|
+
If *value* already contains ANSI codes it is passed through unchanged;
|
|
105
|
+
otherwise it is wrapped in bright_white.
|
|
106
|
+
"""
|
|
107
|
+
from .ansi import strip_ansi
|
|
108
|
+
v = value if (enabled and strip_ansi(value) != value) else colorize(value, 'bright_white', enabled)
|
|
109
|
+
return colorize(key, 'dim', enabled) + sep + v
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def section(title: str, width: int = 44, enabled: bool = True) -> str:
|
|
113
|
+
"""A light horizontal rule with a bold centred label.
|
|
114
|
+
|
|
115
|
+
Example: ``── classify ─────────────────────``
|
|
116
|
+
"""
|
|
117
|
+
label = f' {title} '
|
|
118
|
+
prefix = '── '
|
|
119
|
+
fill = max(0, width - len(prefix) - len(label))
|
|
120
|
+
return (
|
|
121
|
+
colorize(prefix, 'dim', enabled)
|
|
122
|
+
+ colorize(title, 'bold', enabled)
|
|
123
|
+
+ colorize(' ' + '─' * fill, 'dim', enabled)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ── Box table ─────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
class Box:
|
|
130
|
+
"""Unicode-bordered table with ANSI-aware column widths.
|
|
131
|
+
|
|
132
|
+
Build the table, then call ``render()`` or ``print()``::
|
|
133
|
+
|
|
134
|
+
t = Box(['char', 'class', 'energy'], col_align=['c', 'l', 'r'])
|
|
135
|
+
t.row(['F', fmt_class('asymmetric', True), fmt_float_e(7.0, True)])
|
|
136
|
+
print(t.render())
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
_TL = '┌'; _TR = '┐'; _BL = '└'; _BR = '┘'
|
|
140
|
+
_TJ = '┬'; _BJ = '┴'; _LJ = '├'; _RJ = '┤'; _X = '┼'
|
|
141
|
+
_H = '─'; _V = '│'
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
headers: Sequence[str],
|
|
146
|
+
*,
|
|
147
|
+
col_align: Optional[Sequence[str]] = None,
|
|
148
|
+
enabled: bool = True,
|
|
149
|
+
) -> None:
|
|
150
|
+
self._h = list(headers)
|
|
151
|
+
self._rows: List[List[str]] = []
|
|
152
|
+
self._aligns = list(col_align or ['l'] * len(headers))
|
|
153
|
+
self._divs: set = set() # row indices after which to draw ├─┤
|
|
154
|
+
self.enabled = enabled
|
|
155
|
+
|
|
156
|
+
# ── Builder API ───────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def row(self, cells: Sequence[str]) -> 'Box':
|
|
159
|
+
self._rows.append(list(cells))
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def divider(self) -> 'Box':
|
|
163
|
+
"""Insert a ├───┤ rule after the most recently added row."""
|
|
164
|
+
if self._rows:
|
|
165
|
+
self._divs.add(len(self._rows) - 1)
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
# ── Rendering ─────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def render(self) -> str:
|
|
171
|
+
en = self.enabled
|
|
172
|
+
nc = len(self._h)
|
|
173
|
+
|
|
174
|
+
# Compute per-column widths from visible content
|
|
175
|
+
widths = [vlen(h) for h in self._h]
|
|
176
|
+
for row in self._rows:
|
|
177
|
+
for i in range(min(nc, len(row))):
|
|
178
|
+
widths[i] = max(widths[i], vlen(row[i]))
|
|
179
|
+
|
|
180
|
+
def bd(s: str) -> str:
|
|
181
|
+
return colorize(s, 'dim', en) if en else s
|
|
182
|
+
|
|
183
|
+
def hsep(left: str, mid: str, right: str) -> str:
|
|
184
|
+
segs = (bd(self._H * (w + 2)) for w in widths)
|
|
185
|
+
return bd(left) + bd(mid).join(segs) + bd(right)
|
|
186
|
+
|
|
187
|
+
def render_row(cells: List[str]) -> str:
|
|
188
|
+
parts: List[str] = []
|
|
189
|
+
for i in range(nc):
|
|
190
|
+
content = cells[i] if i < len(cells) else ''
|
|
191
|
+
a = self._aligns[i] if i < len(self._aligns) else 'l'
|
|
192
|
+
parts.append(' ' + vpad(content, widths[i], a) + ' ')
|
|
193
|
+
return bd(self._V) + bd(self._V).join(parts) + bd(self._V)
|
|
194
|
+
|
|
195
|
+
lines: List[str] = []
|
|
196
|
+
lines.append(hsep(self._TL, self._TJ, self._TR))
|
|
197
|
+
lines.append(render_row([colorize(h, 'bold', en) for h in self._h]))
|
|
198
|
+
lines.append(hsep(self._LJ, self._X, self._RJ))
|
|
199
|
+
|
|
200
|
+
for i, row in enumerate(self._rows):
|
|
201
|
+
lines.append(render_row(row))
|
|
202
|
+
if i in self._divs and i < len(self._rows) - 1:
|
|
203
|
+
lines.append(hsep(self._LJ, self._X, self._RJ))
|
|
204
|
+
|
|
205
|
+
lines.append(hsep(self._BL, self._BJ, self._BR))
|
|
206
|
+
return '\n'.join(lines)
|
|
207
|
+
|
|
208
|
+
def __str__(self) -> str:
|
|
209
|
+
return self.render()
|
|
210
|
+
|
|
211
|
+
def print(self) -> None:
|
|
212
|
+
print(self.render())
|
gdk9/imply.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, asdict
|
|
6
|
+
from typing import Any, Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
from .errors import InputError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
NAME_RE = re.compile(r"^[A-Z][A-Za-z0-9_]*$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Rule:
|
|
16
|
+
name: str
|
|
17
|
+
type: str # 'fusion' or 'split'
|
|
18
|
+
arity: int
|
|
19
|
+
params: Dict[str, Any]
|
|
20
|
+
|
|
21
|
+
def to_json(self) -> Dict[str, Any]:
|
|
22
|
+
return asdict(self)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_symbol_name(name: str) -> None:
|
|
26
|
+
if not NAME_RE.match(name):
|
|
27
|
+
raise InputError(f"Invalid symbol name: {name}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_fusion(rule_name: str, out_name: str, arity: int) -> Rule:
|
|
31
|
+
if arity < 2:
|
|
32
|
+
raise InputError("Fusion arity must be >= 2")
|
|
33
|
+
return Rule(name=rule_name, type="fusion", arity=arity, params={"out": out_name})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def make_split(rule_name: str, out_a: str, out_b: str, ratio: float) -> Rule:
|
|
37
|
+
if not (0.0 <= ratio <= 1.0):
|
|
38
|
+
raise InputError("Split ratio must be between 0 and 1")
|
|
39
|
+
return Rule(name=rule_name, type="split", arity=1, params={"out_a": out_a, "out_b": out_b, "ratio": float(ratio)})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply_rule(rule: Rule, symbols: Dict[str, float], inputs: List[str], tol: float = 1e-9) -> Dict[str, Any]:
|
|
43
|
+
if rule.type == "fusion":
|
|
44
|
+
if len(inputs) < rule.arity:
|
|
45
|
+
raise InputError(f"Rule '{rule.name}' requires at least {rule.arity} inputs")
|
|
46
|
+
energies = []
|
|
47
|
+
for name in inputs:
|
|
48
|
+
if name not in symbols:
|
|
49
|
+
raise InputError(f"Unknown symbol: {name}")
|
|
50
|
+
energies.append(float(symbols[name]))
|
|
51
|
+
total = sum(energies)
|
|
52
|
+
out_name = rule.params.get("out", "AUTO")
|
|
53
|
+
if out_name.upper() == "AUTO":
|
|
54
|
+
out_name = "".join(inputs)
|
|
55
|
+
validate_symbol_name(out_name)
|
|
56
|
+
outputs = [{"name": out_name, "energy": total}]
|
|
57
|
+
if abs(sum(energies) - sum(o["energy"] for o in outputs)) > tol:
|
|
58
|
+
raise InputError("Conservation failed in fusion")
|
|
59
|
+
return {"inputs": list(zip(inputs, energies)), "outputs": outputs}
|
|
60
|
+
|
|
61
|
+
if rule.type == "split":
|
|
62
|
+
if len(inputs) != 1:
|
|
63
|
+
raise InputError(f"Rule '{rule.name}' requires exactly 1 input")
|
|
64
|
+
name = inputs[0]
|
|
65
|
+
if name not in symbols:
|
|
66
|
+
raise InputError(f"Unknown symbol: {name}")
|
|
67
|
+
ein = float(symbols[name])
|
|
68
|
+
ratio = float(rule.params.get("ratio", 0.5))
|
|
69
|
+
out_a = str(rule.params.get("out_a", "OUT_A"))
|
|
70
|
+
out_b = str(rule.params.get("out_b", "OUT_B"))
|
|
71
|
+
validate_symbol_name(out_a)
|
|
72
|
+
validate_symbol_name(out_b)
|
|
73
|
+
ea = ein * ratio
|
|
74
|
+
eb = ein * (1.0 - ratio)
|
|
75
|
+
if not math.isfinite(ea) or not math.isfinite(eb):
|
|
76
|
+
raise InputError("Non-finite energies in split")
|
|
77
|
+
if abs(ein - (ea + eb)) > tol:
|
|
78
|
+
raise InputError("Conservation failed in split")
|
|
79
|
+
outputs = [{"name": out_a, "energy": ea}, {"name": out_b, "energy": eb}]
|
|
80
|
+
return {"inputs": [(name, ein)], "outputs": outputs}
|
|
81
|
+
|
|
82
|
+
raise InputError(f"Unsupported rule type: {rule.type}")
|
|
83
|
+
|
gdk9/io_utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .errors import InputError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_input(text: Optional[str], file: Optional[str]) -> str:
|
|
11
|
+
if file and text:
|
|
12
|
+
raise InputError("Provide either text or --file, not both.")
|
|
13
|
+
if file:
|
|
14
|
+
p = Path(file)
|
|
15
|
+
if not p.exists() or not p.is_file():
|
|
16
|
+
raise InputError(f"File not found: {file}")
|
|
17
|
+
return p.read_text(encoding="utf-8", errors="replace")
|
|
18
|
+
if text:
|
|
19
|
+
return text
|
|
20
|
+
data = sys.stdin.read()
|
|
21
|
+
if not data:
|
|
22
|
+
raise InputError("No input provided. Pass text, --file, or pipe stdin.")
|
|
23
|
+
return data
|
|
24
|
+
|
gdk9/kernel/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Minimal symbolic implication kernel.
|
|
2
|
+
|
|
3
|
+
The kernel package is intentionally independent from the CLI and plugin
|
|
4
|
+
surfaces. It exposes small, typed primitives that can be composed and tested
|
|
5
|
+
without file-system state, terminal output, or user configuration side effects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .engine import ImplicationEngine
|
|
9
|
+
from .expression import Expression
|
|
10
|
+
from .principle import KernelPrinciple
|
|
11
|
+
from .proof import Judgment, ProofStep
|
|
12
|
+
from .rule import ImplicationRule, RuleKind
|
|
13
|
+
from .symbol import Symbol
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Expression",
|
|
17
|
+
"ImplicationEngine",
|
|
18
|
+
"ImplicationRule",
|
|
19
|
+
"Judgment",
|
|
20
|
+
"KernelPrinciple",
|
|
21
|
+
"ProofStep",
|
|
22
|
+
"RuleKind",
|
|
23
|
+
"Symbol",
|
|
24
|
+
]
|
gdk9/kernel/engine.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from .errors import ConservationError, ImplicationError
|
|
7
|
+
from .expression import Expression
|
|
8
|
+
from .principle import KernelPrinciple
|
|
9
|
+
from .proof import Judgment, ProofStep
|
|
10
|
+
from .rule import ImplicationRule, RuleKind
|
|
11
|
+
from .symbol import Symbol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ImplicationEngine:
|
|
16
|
+
"""Pure symbolic implication engine."""
|
|
17
|
+
|
|
18
|
+
principle: KernelPrinciple = field(default_factory=KernelPrinciple.default)
|
|
19
|
+
rules: tuple[ImplicationRule, ...] = ()
|
|
20
|
+
|
|
21
|
+
def evaluate(self, expression: Expression) -> tuple[float, int]:
|
|
22
|
+
total = expression.total_energy()
|
|
23
|
+
return total, expression.digital_root(self.principle)
|
|
24
|
+
|
|
25
|
+
def judge(self, rule: ImplicationRule, expression: Expression) -> Judgment:
|
|
26
|
+
before_total = expression.total_energy()
|
|
27
|
+
after = rule.apply(expression)
|
|
28
|
+
after_total = after.total_energy()
|
|
29
|
+
delta = after_total - before_total
|
|
30
|
+
conserved = abs(delta) <= rule.tolerance
|
|
31
|
+
if rule.preserves_energy and not conserved:
|
|
32
|
+
raise ConservationError(
|
|
33
|
+
f"Rule '{rule.name}' changed energy by {delta:.12g}"
|
|
34
|
+
)
|
|
35
|
+
return Judgment(rule.name, expression, after, conserved, delta)
|
|
36
|
+
|
|
37
|
+
def apply(self, rule_name: str, expression: Expression) -> Judgment:
|
|
38
|
+
for rule in self.rules:
|
|
39
|
+
if rule.name == rule_name:
|
|
40
|
+
return self.judge(rule, expression)
|
|
41
|
+
raise ImplicationError(f"Unknown rule: {rule_name}")
|
|
42
|
+
|
|
43
|
+
def normalize(self, expression: Expression, max_steps: int = 64) -> list[ProofStep]:
|
|
44
|
+
"""Apply the first valid rule repeatedly until no rule changes the expression."""
|
|
45
|
+
steps: list[ProofStep] = []
|
|
46
|
+
current = expression
|
|
47
|
+
seen = {current.names()}
|
|
48
|
+
for index in range(max_steps):
|
|
49
|
+
for rule in self.rules:
|
|
50
|
+
try:
|
|
51
|
+
judgment = self.judge(rule, current)
|
|
52
|
+
except ImplicationError:
|
|
53
|
+
continue
|
|
54
|
+
if judgment.after.names() == current.names():
|
|
55
|
+
continue
|
|
56
|
+
if judgment.after.names() in seen:
|
|
57
|
+
return steps
|
|
58
|
+
step = ProofStep(judgment, index)
|
|
59
|
+
steps.append(step)
|
|
60
|
+
current = judgment.after
|
|
61
|
+
seen.add(current.names())
|
|
62
|
+
break
|
|
63
|
+
else:
|
|
64
|
+
return steps
|
|
65
|
+
return steps
|
|
66
|
+
|
|
67
|
+
def infer(
|
|
68
|
+
self,
|
|
69
|
+
source: Expression,
|
|
70
|
+
target: Expression,
|
|
71
|
+
max_depth: int = 4,
|
|
72
|
+
) -> list[ProofStep] | None:
|
|
73
|
+
"""Find a bounded implication path from source to target."""
|
|
74
|
+
if source.names() == target.names():
|
|
75
|
+
return []
|
|
76
|
+
queue: deque[tuple[Expression, list[ProofStep]]] = deque([(source, [])])
|
|
77
|
+
seen = {source.names()}
|
|
78
|
+
while queue:
|
|
79
|
+
current, path = queue.popleft()
|
|
80
|
+
if len(path) >= max_depth:
|
|
81
|
+
continue
|
|
82
|
+
for rule in self.rules:
|
|
83
|
+
try:
|
|
84
|
+
judgment = self.judge(rule, current)
|
|
85
|
+
except (ConservationError, ImplicationError):
|
|
86
|
+
continue
|
|
87
|
+
names = judgment.after.names()
|
|
88
|
+
if names in seen:
|
|
89
|
+
continue
|
|
90
|
+
next_path = [*path, ProofStep(judgment, len(path))]
|
|
91
|
+
if names == target.names():
|
|
92
|
+
return next_path
|
|
93
|
+
seen.add(names)
|
|
94
|
+
queue.append((judgment.after, next_path))
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fusion_rule(name: str = "fuse") -> ImplicationRule:
|
|
99
|
+
def transform(expression: Expression) -> Expression:
|
|
100
|
+
if len(expression) < 2:
|
|
101
|
+
raise ImplicationError("Fusion requires at least two symbols")
|
|
102
|
+
fused_name = "".join(expression.names())
|
|
103
|
+
return Expression((Symbol(fused_name, expression.total_energy()),))
|
|
104
|
+
|
|
105
|
+
return ImplicationRule(name, RuleKind.FUSION, transform, reversible=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def split_rule(
|
|
109
|
+
parts: tuple[str, ...],
|
|
110
|
+
energies: tuple[float, ...],
|
|
111
|
+
name: str = "split",
|
|
112
|
+
) -> ImplicationRule:
|
|
113
|
+
if len(parts) != len(energies):
|
|
114
|
+
raise ValueError("Split parts and energies must have the same length")
|
|
115
|
+
if not parts:
|
|
116
|
+
raise ValueError("Split requires at least one output part")
|
|
117
|
+
|
|
118
|
+
def transform(expression: Expression) -> Expression:
|
|
119
|
+
if len(expression) != 1:
|
|
120
|
+
raise ImplicationError("Split requires exactly one input symbol")
|
|
121
|
+
return Expression(tuple(Symbol(part, energy) for part, energy in zip(parts, energies)))
|
|
122
|
+
|
|
123
|
+
return ImplicationRule(name, RuleKind.SPLIT, transform, reversible=True)
|