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/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
|
+
|
gdk9/plugins/__init__.py
ADDED
|
@@ -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
|
gdk9/plugins/registry.py
ADDED
|
@@ -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())
|