gdk9-cli 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. examples/01_analyze.py +28 -0
  2. examples/02_profile.py +24 -0
  3. examples/03_assign.py +19 -0
  4. examples/04_dcg.py +41 -0
  5. examples/05_tokenize.py +22 -0
  6. examples/06_synthesize.py +20 -0
  7. examples/07_api.py +57 -0
  8. examples/08_kernel.py +35 -0
  9. examples/09_kernel_search_walkthrough.py +102 -0
  10. examples/10_conserve_vs_naive.py +100 -0
  11. gdk9/__init__.py +5 -0
  12. gdk9/ansi.py +57 -0
  13. gdk9/cli.py +1186 -0
  14. gdk9/crdt.py +88 -0
  15. gdk9/crypto.py +132 -0
  16. gdk9/data/ninefold.json +40 -0
  17. gdk9/data/official.example.json +16 -0
  18. gdk9/data/official.json +132 -0
  19. gdk9/data/subs.json +436 -0
  20. gdk9/dcg.py +356 -0
  21. gdk9/egglog_bridge/__init__.py +6 -0
  22. gdk9/egglog_bridge/dr_egglog.py +107 -0
  23. gdk9/egglog_bridge/fallback.py +34 -0
  24. gdk9/energy.py +211 -0
  25. gdk9/errors.py +15 -0
  26. gdk9/fmt.py +212 -0
  27. gdk9/imply.py +83 -0
  28. gdk9/io_utils.py +24 -0
  29. gdk9/kernel/__init__.py +24 -0
  30. gdk9/kernel/engine.py +123 -0
  31. gdk9/kernel/errors.py +17 -0
  32. gdk9/kernel/expression.py +43 -0
  33. gdk9/kernel/principle.py +43 -0
  34. gdk9/kernel/proof.py +24 -0
  35. gdk9/kernel/rule.py +30 -0
  36. gdk9/kernel/symbol.py +19 -0
  37. gdk9/kernel_cli.py +195 -0
  38. gdk9/log.py +30 -0
  39. gdk9/optimize.py +288 -0
  40. gdk9/parser.py +35 -0
  41. gdk9/plugins/__init__.py +9 -0
  42. gdk9/plugins/loader.py +346 -0
  43. gdk9/plugins/registry.py +42 -0
  44. gdk9/principles.py +131 -0
  45. gdk9/saturate.py +89 -0
  46. gdk9/state.py +138 -0
  47. gdk9/subs.py +108 -0
  48. gdk9/tokenize.py +386 -0
  49. gdk9/tui.py +115 -0
  50. gdk9/utilization.py +15 -0
  51. gdk9_cli-0.3.0.dist-info/METADATA +295 -0
  52. gdk9_cli-0.3.0.dist-info/RECORD +82 -0
  53. gdk9_cli-0.3.0.dist-info/WHEEL +5 -0
  54. gdk9_cli-0.3.0.dist-info/entry_points.txt +3 -0
  55. gdk9_cli-0.3.0.dist-info/licenses/LICENSE +661 -0
  56. gdk9_cli-0.3.0.dist-info/top_level.txt +4 -0
  57. scripts/batch_analyze.py +109 -0
  58. scripts/export_json.py +125 -0
  59. scripts/make_zip.py +57 -0
  60. scripts/profile_compare.py +165 -0
  61. scripts/watch_file.py +124 -0
  62. tests/conftest.py +26 -0
  63. tests/experiment/__init__.py +1 -0
  64. tests/experiment/test_conserve_vs_naive.py +287 -0
  65. tests/kernel/test_kernel_energy.py +18 -0
  66. tests/kernel/test_kernel_rules.py +58 -0
  67. tests/kernel/test_kernel_search_bound.py +34 -0
  68. tests/test_crypto.py +36 -0
  69. tests/test_crypto_secure.py +26 -0
  70. tests/test_dcg.py +318 -0
  71. tests/test_dcg_golden.py +67 -0
  72. tests/test_egglog_bridge.py +41 -0
  73. tests/test_energy.py +34 -0
  74. tests/test_kernel_cli_handbook.py +101 -0
  75. tests/test_optimize.py +21 -0
  76. tests/test_plugins.py +64 -0
  77. tests/test_rules.py +43 -0
  78. tests/test_rules_commit.py +40 -0
  79. tests/test_rules_reversibility.py +50 -0
  80. tests/test_state_crdt.py +87 -0
  81. tests/test_tokenize.py +38 -0
  82. tests/test_tokenize_metrics.py +33 -0
gdk9/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
+
@@ -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)