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/parser.py ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Energy registry and parsing helpers.
5
+
6
+ This module centralizes access to the active principle's symbol energy
7
+ mapping to discourage arbitrary, ad hoc assignments. Use these helpers when
8
+ you need to resolve or expose symbol energies.
9
+ """
10
+
11
+ from typing import Dict
12
+
13
+ from .principles import Principle, load_principle
14
+ from .energy import char_energy
15
+
16
+
17
+ def loadPrinciple(path: str | None) -> Principle:
18
+ """Load a `Principle` from a JSON/YAML file or return the default when None."""
19
+ return load_principle(path)
20
+
21
+
22
+ def energyRegistry(principle: Principle) -> Dict[str, int]:
23
+ """Return the symbol-energy mapping for the active principle.
24
+
25
+ The registry contains explicit symbol assignments (non-alphanumeric). Letters and digits
26
+ remain computed via principle modes and weights using `char_energy`.
27
+ """
28
+ # Copy to avoid accidental mutation of the original mapping
29
+ return dict(principle.symbol_energy)
30
+
31
+
32
+ def resolveCharEnergy(ch: str, principle: Principle) -> int:
33
+ """Resolve the energy for a single character via the registry and rules."""
34
+ return char_energy(ch, principle)
35
+
@@ -0,0 +1,9 @@
1
+ """Plugin system stubs for Gdk9.
2
+
3
+ Future work will expose a stable interface for loading user-defined symbolic
4
+ grammars and rule packs at runtime. See `registry.py` for the current
5
+ experimental surface.
6
+ """
7
+
8
+ from .registry import PluginRegistry # re-export for convenience
9
+
gdk9/plugins/loader.py ADDED
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
9
+
10
+ from ..errors import ConfigError, InputError
11
+ from ..imply import make_fusion, make_split, Rule, apply_rule
12
+ from ..state import set_rule, set_symbol
13
+ from ..principles import Principle
14
+
15
+
16
+ DEFAULT_PLUGIN_DIRS: List[Path] = [
17
+ Path.cwd() / "plugins",
18
+ Path(os.path.expanduser("~/.gdk9/plugins")),
19
+ ]
20
+ DEFAULT_PLUGIN_CONFIG = Path(os.path.expanduser("~/.gdk9/plugins.json"))
21
+
22
+
23
+ @dataclass
24
+ class LoadedPlugin:
25
+ name: str
26
+ version: str
27
+ description: str
28
+ rules: List[Rule]
29
+ symbol_energy: Dict[str, int]
30
+ symbols: Dict[str, float]
31
+ source: Path
32
+
33
+
34
+ def _read_text(path: Path) -> str:
35
+ try:
36
+ return path.read_text(encoding='utf-8')
37
+ except Exception as exc:
38
+ raise ConfigError(f"Failed to read plugin at {path}: {exc}")
39
+
40
+
41
+ def _parse_yaml_or_json(text: str, path: Path) -> Dict[str, Any]:
42
+ # Try JSON first (valid YAML subset)
43
+ try:
44
+ return json.loads(text)
45
+ except json.JSONDecodeError:
46
+ pass
47
+ # Try YAML if available
48
+ try:
49
+ import yaml # type: ignore
50
+ return yaml.safe_load(text)
51
+ except Exception as exc:
52
+ raise ConfigError(
53
+ f"Unsupported or invalid plugin file {path.name}; install PyYAML or use JSON-compatible YAML."
54
+ ) from exc
55
+
56
+
57
+ def _parse_python_literal_plugin(text: str, path: Path) -> Dict[str, Any]:
58
+ """Read a Python plugin file and extract PLUGIN literal via AST without executing code."""
59
+ try:
60
+ tree = ast.parse(text, filename=str(path))
61
+ except SyntaxError as exc:
62
+ raise ConfigError(f"Invalid Python plugin syntax: {exc}") from exc
63
+ plugin_node: Optional[ast.AST] = None
64
+ for node in tree.body:
65
+ if isinstance(node, ast.Assign):
66
+ for target in node.targets:
67
+ if isinstance(target, ast.Name) and target.id == 'PLUGIN':
68
+ plugin_node = node.value
69
+ break
70
+ if plugin_node is None:
71
+ raise ConfigError("Python plugin must define a top-level PLUGIN = {...} mapping")
72
+ try:
73
+ data = ast.literal_eval(plugin_node) # type: ignore[arg-type]
74
+ except Exception as exc:
75
+ raise ConfigError("PLUGIN must be a literal mapping; no dynamic code allowed") from exc
76
+ if not isinstance(data, dict):
77
+ raise ConfigError("PLUGIN must be a mapping")
78
+ return data
79
+
80
+
81
+ def find_plugin(name_or_path: str, base_dirs: Optional[Iterable[Path]] = None) -> Path:
82
+ p = Path(name_or_path)
83
+ # If direct path provided
84
+ if p.exists():
85
+ if p.is_dir():
86
+ for cand in [p / 'plugin.json', p / 'plugin.yaml', p / 'plugin.yml', p / 'plugin.py']:
87
+ if cand.exists():
88
+ return cand
89
+ return p
90
+ search_dirs = list(base_dirs or DEFAULT_PLUGIN_DIRS)
91
+ suffixes = ['.json', '.yaml', '.yml', '.py']
92
+ for d in search_dirs:
93
+ # file plugin
94
+ for s in suffixes:
95
+ candidate = d / f"{name_or_path}{s}"
96
+ if candidate.exists():
97
+ return candidate
98
+ # directory plugin
99
+ dir_candidate = d / name_or_path
100
+ if dir_candidate.is_dir():
101
+ for cand in [dir_candidate / 'plugin.json', dir_candidate / 'plugin.yaml', dir_candidate / 'plugin.yml', dir_candidate / 'plugin.py']:
102
+ if cand.exists():
103
+ return cand
104
+ raise ConfigError(f"Plugin '{name_or_path}' not found in: " + ", ".join(str(x) for x in search_dirs))
105
+
106
+
107
+ def _validate_symbol_energy(se: Any) -> Dict[str, int]:
108
+ if se is None:
109
+ return {}
110
+ if not isinstance(se, dict):
111
+ raise ConfigError("symbol_energy must be a mapping of symbol->int")
112
+ out: Dict[str, int] = {}
113
+ for k, v in se.items():
114
+ if not isinstance(k, str) or len(k) != 1:
115
+ raise ConfigError("symbol_energy keys must be single-character strings")
116
+ if not isinstance(v, int):
117
+ raise ConfigError("symbol_energy values must be integers")
118
+ out[k] = int(v)
119
+ return out
120
+
121
+
122
+ def _validate_symbols(sym: Any) -> Dict[str, float]:
123
+ if sym is None:
124
+ return {}
125
+ if not isinstance(sym, dict):
126
+ raise ConfigError("symbols must be a mapping of name->float")
127
+ out: Dict[str, float] = {}
128
+ for k, v in sym.items():
129
+ if not isinstance(k, str) or not k:
130
+ raise ConfigError("symbols keys must be names")
131
+ try:
132
+ out[k] = float(v)
133
+ except Exception:
134
+ raise ConfigError("symbols values must be numeric")
135
+ return out
136
+
137
+
138
+ def _build_rules(data: Dict[str, Any]) -> List[Rule]:
139
+ rules: List[Rule] = []
140
+ raw = data.get('rules', [])
141
+ if raw is None:
142
+ raw = []
143
+ if not isinstance(raw, list):
144
+ raise ConfigError("rules must be a list")
145
+ for r in raw:
146
+ if not isinstance(r, dict):
147
+ raise ConfigError("each rule must be a mapping")
148
+ rtype = r.get('type')
149
+ name = r.get('name')
150
+ if rtype not in {'fusion', 'split'}:
151
+ raise ConfigError("rule type must be 'fusion' or 'split'")
152
+ if not isinstance(name, str) or not name:
153
+ raise ConfigError("rule name must be a non-empty string")
154
+ if rtype == 'fusion':
155
+ arity = int(r.get('arity', 2))
156
+ out_name = str(r.get('out', 'AUTO'))
157
+ rules.append(make_fusion(name, out_name, arity))
158
+ else:
159
+ out_a = str(r.get('out_a', 'OUT_A'))
160
+ out_b = str(r.get('out_b', 'OUT_B'))
161
+ ratio = float(r.get('ratio', 0.5))
162
+ rules.append(make_split(name, out_a, out_b, ratio))
163
+ return rules
164
+
165
+
166
+ def _run_checks(plugin: LoadedPlugin, tol: float = 1e-9) -> None:
167
+ checks = _coalesce_checks(plugin.source)
168
+ if not checks:
169
+ return
170
+ # Build ephemeral symbol table from provided checks
171
+ for chk in checks:
172
+ rule_name = chk.get('rule')
173
+ inputs = chk.get('inputs', [])
174
+ if not isinstance(inputs, list) or not isinstance(rule_name, str):
175
+ raise ConfigError("check must include 'rule' and list 'inputs'")
176
+ # Build temporary symbol energies
177
+ symbols: Dict[str, float] = {}
178
+ for s in inputs:
179
+ if not isinstance(s, dict) or 'name' not in s or 'energy' not in s:
180
+ raise ConfigError("each input must be a mapping with name and energy")
181
+ symbols[str(s['name'])] = float(s['energy'])
182
+ # Find corresponding rule
183
+ prule = None
184
+ for r in plugin.rules:
185
+ if r.name == rule_name:
186
+ prule = r
187
+ break
188
+ if prule is None:
189
+ raise ConfigError(f"check references unknown rule: {rule_name}")
190
+ names = [str(s['name']) for s in inputs]
191
+ res = apply_rule(prule, symbols, names, tol=tol)
192
+ # Energy conservation validation is guaranteed by apply_rule; still assert sums
193
+ ein = sum(symbols[n] for n in names)
194
+ eout = sum(o['energy'] for o in res.get('outputs', []))
195
+ if abs(ein - eout) > tol:
196
+ raise ConfigError("energy conservation failed in plugin checks")
197
+
198
+
199
+ def _coalesce_checks(path: Path) -> List[Dict[str, Any]]:
200
+ # Try to read a sibling checks file for directory plugins
201
+ parent = path.parent
202
+ if parent.is_dir():
203
+ for cand in [parent / 'checks.json', parent / 'checks.yaml', parent / 'checks.yml']:
204
+ if cand.exists():
205
+ data = _parse_yaml_or_json(_read_text(cand), cand)
206
+ arr = data.get('checks', data if isinstance(data, list) else [])
207
+ if isinstance(arr, list):
208
+ return arr # type: ignore[return-value]
209
+ # Inline checks may be embedded in same file (handled in load_plugin)
210
+ return []
211
+
212
+
213
+ def load_plugin(path: Path) -> LoadedPlugin:
214
+ text = _read_text(path)
215
+ if path.suffix.lower() in {'.json', '.yaml', '.yml'}:
216
+ data = _parse_yaml_or_json(text, path)
217
+ elif path.suffix.lower() == '.py':
218
+ data = _parse_python_literal_plugin(text, path)
219
+ else:
220
+ raise ConfigError("Unsupported plugin type; use .json, .yml/.yaml, or .py")
221
+ if not isinstance(data, dict):
222
+ raise ConfigError("plugin root must be a mapping")
223
+ name = str(data.get('name') or path.stem)
224
+ version = str(data.get('version') or '0.1')
225
+ description = str(data.get('description') or '')
226
+ symbol_energy = _validate_symbol_energy(data.get('symbol_energy'))
227
+ symbols = _validate_symbols(data.get('symbols'))
228
+ rules = _build_rules(data)
229
+ plugin = LoadedPlugin(name=name, version=version, description=description, rules=rules, symbol_energy=symbol_energy, symbols=symbols, source=path)
230
+ # Optional inline checks
231
+ inline_checks = data.get('checks')
232
+ if isinstance(inline_checks, list):
233
+ # temporarily write inline checks into sibling to reuse runner
234
+ pass # run via direct loop below
235
+ # Run inline checks if provided
236
+ if isinstance(inline_checks, list) and inline_checks:
237
+ for chk in inline_checks:
238
+ if not isinstance(chk, dict):
239
+ raise ConfigError("check entries must be mappings")
240
+ rule_name = chk.get('rule')
241
+ inputs = chk.get('inputs', [])
242
+ if not isinstance(inputs, list) or not isinstance(rule_name, str):
243
+ raise ConfigError("check must include 'rule' and list 'inputs'")
244
+ symbols_map: Dict[str, float] = {}
245
+ for s in inputs:
246
+ if not isinstance(s, dict) or 'name' not in s or 'energy' not in s:
247
+ raise ConfigError("each input must be a mapping with name and energy")
248
+ symbols_map[str(s['name'])] = float(s['energy'])
249
+ rule_obj = next((r for r in plugin.rules if r.name == rule_name), None)
250
+ if rule_obj is None:
251
+ raise ConfigError(f"check references unknown rule: {rule_name}")
252
+ names = [str(s['name']) for s in inputs]
253
+ res = apply_rule(rule_obj, symbols_map, names, tol=1e-9)
254
+ ein = sum(symbols_map[n] for n in names)
255
+ eout = sum(o['energy'] for o in res.get('outputs', []))
256
+ if abs(ein - eout) > 1e-9:
257
+ raise ConfigError("energy conservation failed in plugin checks")
258
+ # Also attempt to run sibling checks files
259
+ _run_checks(plugin)
260
+ return plugin
261
+
262
+
263
+ def list_available(base_dirs: Optional[Iterable[Path]] = None) -> List[str]:
264
+ names: List[str] = []
265
+ for d in (base_dirs or DEFAULT_PLUGIN_DIRS):
266
+ if not d.exists():
267
+ continue
268
+ for p in d.iterdir():
269
+ if p.is_file() and p.suffix.lower() in {'.json', '.yaml', '.yml', '.py'}:
270
+ names.append(p.stem)
271
+ elif p.is_dir() and any((p / f"plugin{ext}").exists() for ext in ('.json', '.yaml', '.yml', '.py')):
272
+ names.append(p.name)
273
+ return sorted(list(set(names)))
274
+
275
+
276
+ def apply_plugin(plugin: LoadedPlugin, principle: Principle, state: Dict[str, Any]) -> Tuple[Principle, Dict[str, Any], Dict[str, Any]]:
277
+ # Merge symbol_energy into principle (override existing)
278
+ if plugin.symbol_energy:
279
+ merged = dict(principle.symbol_energy)
280
+ merged.update(plugin.symbol_energy)
281
+ principle.symbol_energy = merged
282
+ # Seed symbols into state
283
+ actor = f"plugin:{plugin.name}"
284
+ if plugin.symbols:
285
+ for k, v in plugin.symbols.items():
286
+ set_symbol(state, k, float(v), actor=actor)
287
+ # Add rules (avoid overwriting existing rules with same name)
288
+ rules = state.setdefault('rules', {})
289
+ added = 0
290
+ for r in plugin.rules:
291
+ if r.name not in rules:
292
+ set_rule(state, r.name, r.to_json(), actor=actor)
293
+ added += 1
294
+ return principle, state, {"rules_added": added, "symbols_added": len(plugin.symbols), "symbol_energy_updates": len(plugin.symbol_energy)}
295
+
296
+
297
+ def _load_config(path: Path = DEFAULT_PLUGIN_CONFIG) -> Dict[str, Any]:
298
+ if not path.exists():
299
+ return {"enabled": [], "paths": {}}
300
+ try:
301
+ return json.loads(path.read_text(encoding='utf-8'))
302
+ except Exception as exc:
303
+ raise ConfigError(f"Failed to read plugin config {path}: {exc}")
304
+
305
+
306
+ def _save_config(cfg: Dict[str, Any], path: Path = DEFAULT_PLUGIN_CONFIG) -> None:
307
+ path.parent.mkdir(parents=True, exist_ok=True)
308
+ tmp = path.with_suffix(path.suffix + '.tmp')
309
+ tmp.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding='utf-8')
310
+ tmp.replace(path)
311
+
312
+
313
+ def enable_plugin(name: str, resolved_path: Path, config_path: Path = DEFAULT_PLUGIN_CONFIG) -> None:
314
+ cfg = _load_config(config_path)
315
+ enabled = cfg.setdefault('enabled', [])
316
+ if name not in enabled:
317
+ enabled.append(name)
318
+ cfg.setdefault('paths', {})[name] = str(resolved_path)
319
+ _save_config(cfg, config_path)
320
+
321
+
322
+ def disable_plugin(name: str, config_path: Path = DEFAULT_PLUGIN_CONFIG) -> None:
323
+ cfg = _load_config(config_path)
324
+ enabled = cfg.setdefault('enabled', [])
325
+ if name in enabled:
326
+ enabled.remove(name)
327
+ cfg.get('paths', {}).pop(name, None)
328
+ _save_config(cfg, config_path)
329
+
330
+
331
+ def reset_config(config_path: Path = DEFAULT_PLUGIN_CONFIG) -> None:
332
+ """Reset plugin configuration to defaults (no enabled plugins)."""
333
+ _save_config({"enabled": [], "paths": {}}, config_path)
334
+
335
+
336
+ def auto_boot(principle: Principle, state: Dict[str, Any]) -> Tuple[Principle, Dict[str, Any], List[str]]:
337
+ cfg = _load_config(DEFAULT_PLUGIN_CONFIG)
338
+ enabled = cfg.get('enabled', [])
339
+ loaded: List[str] = []
340
+ for name in enabled:
341
+ pstr = cfg.get('paths', {}).get(name)
342
+ path = Path(pstr) if pstr else find_plugin(name)
343
+ plugin = load_plugin(path)
344
+ principle, state, _ = apply_plugin(plugin, principle, state)
345
+ loaded.append(plugin.name)
346
+ return principle, state, loaded
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Lightweight plugin registry (experimental).
5
+
6
+ Allows registering and retrieving logic packs or grammar providers by name.
7
+ This is a minimal in-memory registry intended as a stepping stone toward a
8
+ dynamic plugin loader.
9
+ """
10
+
11
+ from typing import Any, Callable, Dict, Iterable, Optional
12
+
13
+
14
+ class PluginRegistry:
15
+ def __init__(self) -> None:
16
+ self._loaders: Dict[str, Callable[[], Any]] = {}
17
+
18
+ def register(self, name: str, loader: Callable[[], Any]) -> None:
19
+ """Register a plugin by name with a zero-arg loader callable.
20
+
21
+ Loader should return an object that exposes the plugin interface (to be
22
+ formalized). Duplicate registrations overwrite by design.
23
+ """
24
+ if not isinstance(name, str) or not name:
25
+ raise ValueError("Plugin name must be a non-empty string")
26
+ if not callable(loader):
27
+ raise TypeError("loader must be callable")
28
+ self._loaders[name] = loader
29
+
30
+ def get(self, name: str) -> Any:
31
+ """Load and return the plugin by name, raising KeyError if unknown."""
32
+ loader = self._loaders[name]
33
+ return loader()
34
+
35
+ def list(self) -> Iterable[str]:
36
+ """List registered plugin names."""
37
+ return sorted(self._loaders.keys())
38
+
39
+ def unregister(self, name: str) -> None:
40
+ """Remove a plugin by name if present."""
41
+ self._loaders.pop(name, None)
42
+
gdk9/principles.py ADDED
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Dict, Optional
7
+
8
+ from .errors import ConfigError
9
+
10
+
11
+ @dataclass
12
+ class Principle:
13
+ name: str
14
+ description: str
15
+ symbol_energy: Dict[str, int] = field(default_factory=dict)
16
+ letter_mode: str = "a1z26" # a1z26 or codepoint
17
+ number_mode: str = "digital_root" # digital_root
18
+ normalize_zero_to_nine: bool = True
19
+ weights: Dict[str, int] = field(default_factory=lambda: {"letter": 1, "digit": 1, "symbol": 1})
20
+ harmonics: bool = True
21
+
22
+ @staticmethod
23
+ def default() -> "Principle":
24
+ # Prefer official bundled mapping if present
25
+ try:
26
+ here = Path(__file__).parent
27
+ official = here / 'data' / 'official.json'
28
+ if official.exists():
29
+ data = json.loads(official.read_text(encoding='utf-8'))
30
+ return Principle(
31
+ name=data.get("name", "Gdk9 Official"),
32
+ description=data.get("description", "Official Gdk9 mapping."),
33
+ symbol_energy=data.get("symbol_energy", {}),
34
+ letter_mode=data.get("letter_mode", "a1z26"),
35
+ number_mode=data.get("number_mode", "digital_root"),
36
+ normalize_zero_to_nine=bool(data.get("normalize_zero_to_nine", True)),
37
+ weights=data.get("weights", {"letter": 1, "digit": 1, "symbol": 1}),
38
+ harmonics=bool(data.get("harmonics", True)),
39
+ )
40
+ except Exception:
41
+ pass
42
+ return Principle(
43
+ name="Ninefold Grid",
44
+ description=(
45
+ "Default Gdk9 principle: A1Z26 letters, digital-root numbers, "
46
+ "custom symbol energies, zero maps to nine."
47
+ ),
48
+ symbol_energy={
49
+ ".": 1,
50
+ ",": 2,
51
+ "!": 3,
52
+ "?": 4,
53
+ ":": 5,
54
+ ";": 6,
55
+ "-": 7,
56
+ "_": 8,
57
+ "*": 9,
58
+ "#": 5,
59
+ "+": 4,
60
+ "=": 6,
61
+ "@": 8,
62
+ "$": 7,
63
+ "%": 5,
64
+ "&": 9,
65
+ "/": 3,
66
+ "\\": 3,
67
+ "(": 2,
68
+ ")": 2,
69
+ "[": 2,
70
+ "]": 2,
71
+ "{": 2,
72
+ "}": 2,
73
+ "<": 1,
74
+ ">": 1,
75
+ "|": 1,
76
+ "~": 9,
77
+ "^": 8,
78
+ },
79
+ weights={"letter": 1, "digit": 1, "symbol": 1},
80
+ harmonics=True,
81
+ )
82
+
83
+
84
+ def load_principle(path: Optional[str]) -> Principle:
85
+ if not path:
86
+ return Principle.default()
87
+ p = Path(path)
88
+ if not p.exists():
89
+ raise ConfigError(f"Principle file not found: {path}")
90
+ try:
91
+ if p.suffix.lower() in {".json"}:
92
+ data = json.loads(p.read_text(encoding="utf-8"))
93
+ elif p.suffix.lower() in {".yml", ".yaml"}:
94
+ try:
95
+ import yaml # type: ignore
96
+ except Exception as exc: # pragma: no cover - optional dep
97
+ raise ConfigError(
98
+ "YAML support requires PyYAML; install it or use JSON."
99
+ ) from exc
100
+ data = yaml.safe_load(p.read_text(encoding="utf-8"))
101
+ else:
102
+ raise ConfigError("Unsupported principle file type. Use .json or .yml/.yaml")
103
+ except json.JSONDecodeError as exc:
104
+ raise ConfigError(f"Invalid JSON in principle file: {exc}") from exc
105
+
106
+ try:
107
+ pr = Principle(
108
+ name=data.get("name", "Custom Principle"),
109
+ description=data.get("description", ""),
110
+ symbol_energy=data.get("symbol_energy", {}),
111
+ letter_mode=data.get("letter_mode", "a1z26"),
112
+ number_mode=data.get("number_mode", "digital_root"),
113
+ normalize_zero_to_nine=bool(data.get("normalize_zero_to_nine", True)),
114
+ weights=data.get("weights", {"letter": 1, "digit": 1, "symbol": 1}),
115
+ harmonics=bool(data.get("harmonics", True)),
116
+ )
117
+ except Exception as exc:
118
+ raise ConfigError(f"Invalid principle schema: {exc}") from exc
119
+ # basic validation
120
+ if not isinstance(pr.symbol_energy, dict):
121
+ raise ConfigError("symbol_energy must be a mapping of symbol->int")
122
+ for k, v in pr.symbol_energy.items():
123
+ if not isinstance(k, str) or len(k) != 1:
124
+ raise ConfigError("symbol_energy keys must be single-character strings")
125
+ if not isinstance(v, int):
126
+ raise ConfigError("symbol_energy values must be integers")
127
+ if pr.letter_mode not in {"a1z26", "codepoint"}:
128
+ raise ConfigError("letter_mode must be 'a1z26' or 'codepoint'")
129
+ if pr.number_mode not in {"digital_root"}:
130
+ raise ConfigError("number_mode must be 'digital_root'")
131
+ return pr
gdk9/saturate.py ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """CLI: saturate a small GDk9 DR fixture via egglog (fallback: gdk9.energy).
3
+
4
+ Usage:
5
+ python -m gdk9.saturate
6
+ python -m gdk9.saturate --values 26,10,9
7
+ python -m gdk9.saturate --fallback-only
8
+ gdk9-saturate
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+
15
+ DEFAULT_FIXTURE = (26, 10, 9) # DR(26)+DR(10)+9 -> 8+1+9=18 -> 9
16
+
17
+
18
+ def main(argv: list[str] | None = None) -> int:
19
+ p = argparse.ArgumentParser(description="GDk9 egglog DR saturation spike")
20
+ p.add_argument(
21
+ "--values",
22
+ default=",".join(str(v) for v in DEFAULT_FIXTURE),
23
+ help="comma-separated energy totals (default: 26,10,9)",
24
+ )
25
+ p.add_argument(
26
+ "--fallback-only",
27
+ action="store_true",
28
+ help="skip egglog; use gdk9.energy.digital_root only",
29
+ )
30
+ p.add_argument("--json", action="store_true", help="machine-readable output")
31
+ args = p.parse_args(argv)
32
+
33
+ values = [int(x.strip()) for x in args.values.split(",") if x.strip()]
34
+
35
+ from gdk9.egglog_bridge.fallback import saturate_fallback
36
+
37
+ fb = saturate_fallback(values)
38
+
39
+ egg = None
40
+ err = None
41
+ if not args.fallback_only:
42
+ try:
43
+ from gdk9.egglog_bridge.dr_egglog import saturate_egglog
44
+
45
+ egg = saturate_egglog(values)
46
+ except Exception as exc: # noqa: BLE001 — spike must report install/API failure
47
+ err = f"{type(exc).__name__}: {exc}"
48
+
49
+ verdict = "NO"
50
+ if egg and egg.get("ok") and fb.get("ok") and egg.get("best_dr") == fb.get("best_dr"):
51
+ verdict = "YES"
52
+ elif args.fallback_only and fb.get("ok"):
53
+ verdict = "FALLBACK-ONLY"
54
+ elif err:
55
+ verdict = "NO"
56
+
57
+ payload = {
58
+ "verdict": verdict,
59
+ "fixture": values,
60
+ "egglog": egg,
61
+ "fallback": fb,
62
+ "error": err,
63
+ }
64
+
65
+ if args.json:
66
+ print(json.dumps(payload, indent=2))
67
+ else:
68
+ print("=== GDk9 egglog bridge spike: saturate ===")
69
+ print(f"fixture: {values}")
70
+ if egg:
71
+ print(f"[egglog] before: {egg['before']}")
72
+ print(f"[egglog] after_sum: {egg['after_sum']}")
73
+ print(f"[egglog] after_dr: {egg['after_dr']}")
74
+ print(f"[egglog] best_dr: {egg['best_dr']}")
75
+ if err:
76
+ print(f"[egglog] ERROR: {err}")
77
+ print(
78
+ f"[fallback] total={fb['total']} folded={fb['folded_terms']} "
79
+ f"best_dr={fb['best_dr']} congruence_dr={fb['congruence_dr']}"
80
+ )
81
+ print(f"VERDICT: {verdict}")
82
+ if verdict == "YES":
83
+ print("collapsed/best form:", egg["after_dr"] if egg else fb["best_dr"])
84
+
85
+ return 0 if verdict in {"YES", "FALLBACK-ONLY"} else 1
86
+
87
+
88
+ if __name__ == "__main__":
89
+ raise SystemExit(main())