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/kernel/errors.py ADDED
@@ -0,0 +1,17 @@
1
+ """Kernel-specific exceptions."""
2
+
3
+
4
+ class KernelError(Exception):
5
+ """Base exception for symbolic implication kernel failures."""
6
+
7
+
8
+ class ConservationError(KernelError):
9
+ """Raised when a transformation violates an invariant."""
10
+
11
+
12
+ class ImplicationError(KernelError):
13
+ """Raised when a rule cannot be applied to an expression."""
14
+
15
+
16
+ class UnknownSymbolError(KernelError):
17
+ """Raised when an expression references a symbol absent from a principle."""
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Iterable, Iterator, Sequence
5
+
6
+ from .principle import KernelPrinciple
7
+ from .symbol import Symbol
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Expression:
12
+ """An ordered symbolic expression."""
13
+
14
+ symbols: tuple[Symbol, ...]
15
+
16
+ @classmethod
17
+ def from_names(cls, names: Iterable[str], principle: KernelPrinciple) -> "Expression":
18
+ return cls(tuple(Symbol(name, principle.energy_of(name)) for name in names))
19
+
20
+ @classmethod
21
+ def from_text(cls, text: str, principle: KernelPrinciple) -> "Expression":
22
+ return cls.from_names(text, principle)
23
+
24
+ def __iter__(self) -> Iterator[Symbol]:
25
+ return iter(self.symbols)
26
+
27
+ def __len__(self) -> int:
28
+ return len(self.symbols)
29
+
30
+ def names(self) -> tuple[str, ...]:
31
+ return tuple(symbol.name for symbol in self.symbols)
32
+
33
+ def total_energy(self) -> float:
34
+ return sum(symbol.energy for symbol in self.symbols)
35
+
36
+ def digital_root(self, principle: KernelPrinciple) -> int:
37
+ return principle.total_root(self.total_energy())
38
+
39
+ def text(self) -> str:
40
+ return "".join(self.names())
41
+
42
+ def replace(self, symbols: Sequence[Symbol]) -> "Expression":
43
+ return Expression(tuple(symbols))
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Mapping
5
+
6
+ from gdk9.energy import char_energy, digital_root
7
+ from gdk9.principles import Principle
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class KernelPrinciple:
12
+ """Immutable valuation context for kernel expressions."""
13
+
14
+ name: str
15
+ symbol_energy: Mapping[str, float] = field(default_factory=dict)
16
+ source: Principle | None = None
17
+ normalize_zero_to_nine: bool = True
18
+
19
+ @classmethod
20
+ def default(cls) -> "KernelPrinciple":
21
+ principle = Principle.default()
22
+ return cls(
23
+ name=principle.name,
24
+ symbol_energy=dict(principle.symbol_energy),
25
+ source=principle,
26
+ normalize_zero_to_nine=principle.normalize_zero_to_nine,
27
+ )
28
+
29
+ def energy_of(self, name: str) -> float:
30
+ if len(name) == 1:
31
+ if self.source is not None:
32
+ return float(char_energy(name, self.source))
33
+ explicit = self.symbol_energy.get(name)
34
+ if explicit is not None:
35
+ return float(explicit)
36
+ return float(digital_root(ord(name), self.normalize_zero_to_nine))
37
+ explicit = self.symbol_energy.get(name)
38
+ if explicit is not None:
39
+ return float(explicit)
40
+ return sum(self.energy_of(ch) for ch in name)
41
+
42
+ def total_root(self, total: float) -> int:
43
+ return digital_root(int(round(total)), self.normalize_zero_to_nine)
gdk9/kernel/proof.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .expression import Expression
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Judgment:
10
+ """The result of applying one implication rule."""
11
+
12
+ rule: str
13
+ before: Expression
14
+ after: Expression
15
+ conserved: bool
16
+ delta: float
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ProofStep:
21
+ """A traceable implication step suitable for proof paths."""
22
+
23
+ judgment: Judgment
24
+ index: int
gdk9/kernel/rule.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Callable
6
+
7
+ from .expression import Expression
8
+
9
+ Transform = Callable[[Expression], Expression]
10
+
11
+
12
+ class RuleKind(str, Enum):
13
+ FUSION = "fusion"
14
+ SPLIT = "split"
15
+ REWRITE = "rewrite"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ImplicationRule:
20
+ """A pure implication transform with invariant metadata."""
21
+
22
+ name: str
23
+ kind: RuleKind
24
+ transform: Transform
25
+ reversible: bool = False
26
+ preserves_energy: bool = True
27
+ tolerance: float = 1e-9
28
+
29
+ def apply(self, expression: Expression) -> Expression:
30
+ return self.transform(expression)
gdk9/kernel/symbol.py ADDED
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Mapping
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Symbol:
9
+ """A named symbolic unit with an evaluated energy."""
10
+
11
+ name: str
12
+ energy: float
13
+ traits: Mapping[str, str] = field(default_factory=dict)
14
+
15
+ def __post_init__(self) -> None:
16
+ if not self.name:
17
+ raise ValueError("Symbol name must be non-empty")
18
+ if not isinstance(self.energy, (int, float)):
19
+ raise TypeError("Symbol energy must be numeric")
gdk9/kernel_cli.py ADDED
@@ -0,0 +1,195 @@
1
+ """Thin CLI/API adapter for the pure implication kernel.
2
+
3
+ Lives outside ``gdk9.kernel`` so the kernel package stays free of CLI, state,
4
+ plugin, crypto, TUI, and terminal-formatting imports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Sequence
11
+
12
+ from gdk9.errors import InputError
13
+ from gdk9.kernel import Expression, ImplicationEngine, KernelPrinciple
14
+ from gdk9.kernel.engine import fusion_rule, split_rule
15
+ from gdk9.kernel.errors import ConservationError, ImplicationError, KernelError
16
+ from gdk9.kernel.proof import Judgment, ProofStep
17
+ from gdk9.kernel.rule import ImplicationRule
18
+ from gdk9.principles import Principle
19
+
20
+
21
+ def principle_to_kernel(principle: Principle) -> KernelPrinciple:
22
+ """Adapt a GDk9 ``Principle`` into an immutable ``KernelPrinciple``."""
23
+ return KernelPrinciple(
24
+ name=principle.name,
25
+ symbol_energy=dict(principle.symbol_energy),
26
+ source=principle,
27
+ normalize_zero_to_nine=principle.normalize_zero_to_nine,
28
+ )
29
+
30
+
31
+ def expression_payload(expression: Expression) -> dict[str, Any]:
32
+ return {
33
+ "names": list(expression.names()),
34
+ "text": expression.text(),
35
+ "total_energy": expression.total_energy(),
36
+ "symbols": [
37
+ {"name": symbol.name, "energy": symbol.energy} for symbol in expression.symbols
38
+ ],
39
+ }
40
+
41
+
42
+ def judgment_payload(judgment: Judgment) -> dict[str, Any]:
43
+ return {
44
+ "rule": judgment.rule,
45
+ "before": expression_payload(judgment.before),
46
+ "after": expression_payload(judgment.after),
47
+ "conserved": judgment.conserved,
48
+ "delta": judgment.delta,
49
+ }
50
+
51
+
52
+ def proof_steps_payload(steps: Sequence[ProofStep]) -> list[dict[str, Any]]:
53
+ return [
54
+ {"index": step.index, "judgment": judgment_payload(step.judgment)} for step in steps
55
+ ]
56
+
57
+
58
+ def _parse_float_list(raw: str, label: str) -> tuple[float, ...]:
59
+ parts = [p.strip() for p in raw.split(",") if p.strip()]
60
+ if not parts:
61
+ raise InputError(f"{label} must list at least one value")
62
+ try:
63
+ return tuple(float(p) for p in parts)
64
+ except ValueError as exc:
65
+ raise InputError(f"Invalid {label}: {raw}") from exc
66
+
67
+
68
+ def _parse_name_list(raw: str, label: str) -> tuple[str, ...]:
69
+ parts = [p.strip() for p in raw.split(",") if p.strip()]
70
+ if not parts:
71
+ raise InputError(f"{label} must list at least one symbol name")
72
+ return tuple(parts)
73
+
74
+
75
+ def builtin_rules(
76
+ names: Sequence[str],
77
+ *,
78
+ split_parts: tuple[str, ...] | None = None,
79
+ split_energies: tuple[float, ...] | None = None,
80
+ ) -> tuple[ImplicationRule, ...]:
81
+ """Build the small set of demo rules used by the kernel smoke path."""
82
+ rules: list[ImplicationRule] = []
83
+ for name in names:
84
+ key = name.strip().lower()
85
+ if key in {"fuse", "fusion"}:
86
+ rules.append(fusion_rule(name="fuse"))
87
+ elif key == "split":
88
+ if not split_parts or not split_energies:
89
+ raise InputError("split requires --parts and --energies")
90
+ rules.append(split_rule(split_parts, split_energies, name="split"))
91
+ else:
92
+ raise InputError(f"Unknown builtin kernel rule: {name}")
93
+ return tuple(rules)
94
+
95
+
96
+ def _print_json(payload: dict[str, Any]) -> int:
97
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
98
+ return 0
99
+
100
+
101
+ def cmd_kernel_eval(args: Any, principle: Principle) -> int:
102
+ kp = principle_to_kernel(principle)
103
+ expr = Expression.from_text(args.text, kp)
104
+ total, root = ImplicationEngine(kp).evaluate(expr)
105
+ return _print_json(
106
+ {
107
+ "ok": True,
108
+ "cmd": "eval",
109
+ "expression": expression_payload(expr),
110
+ "total": total,
111
+ "digital_root": root,
112
+ "principle": kp.name,
113
+ }
114
+ )
115
+
116
+
117
+ def cmd_kernel_apply(args: Any, principle: Principle) -> int:
118
+ kp = principle_to_kernel(principle)
119
+ names = tuple(args.symbols)
120
+ if not names:
121
+ raise InputError("apply requires at least one symbol name")
122
+ expr = Expression.from_names(names, kp)
123
+ split_parts = _parse_name_list(args.parts, "parts") if getattr(args, "parts", None) else None
124
+ split_energies = (
125
+ _parse_float_list(args.energies, "energies") if getattr(args, "energies", None) else None
126
+ )
127
+ rules = builtin_rules(
128
+ [args.rule],
129
+ split_parts=split_parts,
130
+ split_energies=split_energies,
131
+ )
132
+ engine = ImplicationEngine(kp, rules)
133
+ try:
134
+ judgment = engine.apply(rules[0].name, expr)
135
+ except KernelError as exc:
136
+ return _print_json({"ok": False, "cmd": "apply", "error": str(exc)})
137
+ return _print_json(
138
+ {
139
+ "ok": True,
140
+ "cmd": "apply",
141
+ "judgment": judgment_payload(judgment),
142
+ "principle": kp.name,
143
+ }
144
+ )
145
+
146
+
147
+ def cmd_kernel_search(args: Any, principle: Principle) -> int:
148
+ kp = principle_to_kernel(principle)
149
+ source_names = tuple(args.source)
150
+ target_names = tuple(args.target)
151
+ if not source_names or not target_names:
152
+ raise InputError("search requires --source and --target symbol names")
153
+ source = Expression.from_names(source_names, kp)
154
+ target = Expression.from_names(target_names, kp)
155
+ rule_names = list(args.rules) if getattr(args, "rules", None) else ["fuse"]
156
+ split_parts = _parse_name_list(args.parts, "parts") if getattr(args, "parts", None) else None
157
+ split_energies = (
158
+ _parse_float_list(args.energies, "energies") if getattr(args, "energies", None) else None
159
+ )
160
+ rules = builtin_rules(
161
+ rule_names,
162
+ split_parts=split_parts,
163
+ split_energies=split_energies,
164
+ )
165
+ engine = ImplicationEngine(kp, rules)
166
+ max_depth = int(getattr(args, "max_depth", 4))
167
+ path = engine.infer(source, target, max_depth=max_depth)
168
+ return _print_json(
169
+ {
170
+ "ok": path is not None,
171
+ "cmd": "search",
172
+ "found": path is not None,
173
+ "max_depth": max_depth,
174
+ "source": expression_payload(source),
175
+ "target": expression_payload(target),
176
+ "rules": [rule.name for rule in rules],
177
+ "steps": proof_steps_payload(path or ()),
178
+ "principle": kp.name,
179
+ }
180
+ )
181
+
182
+
183
+ def run_kernel(args: Any, principle: Principle) -> int:
184
+ """Dispatch ``gdk9 kernel …`` subcommands."""
185
+ cmd = getattr(args, "kernel_cmd", None)
186
+ try:
187
+ if cmd == "eval":
188
+ return cmd_kernel_eval(args, principle)
189
+ if cmd == "apply":
190
+ return cmd_kernel_apply(args, principle)
191
+ if cmd == "search":
192
+ return cmd_kernel_search(args, principle)
193
+ except (ConservationError, ImplicationError) as exc:
194
+ return _print_json({"ok": False, "cmd": cmd, "error": str(exc)})
195
+ raise InputError(f"Unknown kernel subcommand: {cmd}")
gdk9/log.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Any
6
+
7
+
8
+ class Logger:
9
+ def __init__(self, enabled: bool = False) -> None:
10
+ self.enabled = enabled or (os.getenv("GDK9_DEBUG") == "1")
11
+
12
+ def set_enabled(self, enabled: bool) -> None:
13
+ self.enabled = bool(enabled)
14
+
15
+ def debug(self, *args: Any) -> None:
16
+ if self.enabled:
17
+ msg = " ".join(str(a) for a in args)
18
+ print(f"[gdk9:debug] {msg}", file=sys.stderr)
19
+
20
+ def info(self, *args: Any) -> None:
21
+ msg = " ".join(str(a) for a in args)
22
+ print(msg)
23
+
24
+ def error(self, *args: Any) -> None:
25
+ msg = " ".join(str(a) for a in args)
26
+ print(msg, file=sys.stderr)
27
+
28
+
29
+ logger = Logger()
30
+
gdk9/optimize.py ADDED
@@ -0,0 +1,288 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+ from typing import Dict, List, Optional, Tuple
6
+
7
+ from .energy import digital_root, string_energy, char_energy
8
+ from .principles import Principle
9
+
10
+
11
+ @dataclass
12
+ class Plan:
13
+ method: str
14
+ target: int
15
+ steps: List[Tuple[str, int]] # (symbol, count)
16
+ total_before: int
17
+ dr_before: int
18
+ total_after: int
19
+ dr_after: int
20
+
21
+
22
+ def _residue(n: int) -> int:
23
+ return n % 9
24
+
25
+
26
+ def _target_residue(total: int, target: int) -> int:
27
+ cur_r = _residue(total)
28
+ tgt_r = target % 9 # 9 -> 0
29
+ return (tgt_r - cur_r) % 9
30
+
31
+
32
+ def _allowed_residues(allowed: List[Tuple[str, int]]) -> Dict[int, List[str]]:
33
+ by_res: Dict[int, List[str]] = {i: [] for i in range(9)}
34
+ for s, e in allowed:
35
+ r = e % 9
36
+ by_res[r].append(s)
37
+ return by_res
38
+
39
+
40
+ def minimal_residue_combo(delta_res: int, allowed: List[Tuple[str, int]], max_steps: int = 64) -> Optional[List[int]]:
41
+ # BFS on residues 0..8; each step adds one symbol residue
42
+ residues = [e % 9 for _, e in allowed]
43
+ if delta_res == 0:
44
+ return []
45
+ # If all residues are 0 we cannot change the residue
46
+ if all(r == 0 for r in residues):
47
+ return None
48
+ parent = {0: (-1, -1)} # residue -> (prev_residue, symbol_index)
49
+ q = deque([0])
50
+ depth = {0: 0}
51
+ while q:
52
+ cur = q.popleft()
53
+ d = depth[cur]
54
+ if d >= max_steps:
55
+ continue
56
+ for idx, r in enumerate(residues):
57
+ nxt = (cur + r) % 9
58
+ if nxt not in parent:
59
+ parent[nxt] = (cur, idx)
60
+ depth[nxt] = d + 1
61
+ if nxt == delta_res:
62
+ # reconstruct path of indices
63
+ seq: List[int] = []
64
+ at = nxt
65
+ while at != 0:
66
+ prev, i = parent[at]
67
+ seq.append(i)
68
+ at = prev
69
+ seq.reverse()
70
+ return seq
71
+ q.append(nxt)
72
+ return None
73
+
74
+
75
+ def build_steps_from_seq(seq: List[int], allowed: List[Tuple[str, int]]) -> List[Tuple[str, int]]:
76
+ counts: Dict[int, int] = {}
77
+ for i in seq:
78
+ counts[i] = counts.get(i, 0) + 1
79
+ steps: List[Tuple[str, int]] = []
80
+ for i, (sym, _e) in enumerate(allowed):
81
+ if i in counts:
82
+ steps.append((sym, counts[i]))
83
+ return steps
84
+
85
+
86
+ def optimize_attunement(
87
+ text: str,
88
+ target: int,
89
+ principle: Principle,
90
+ allowed_symbols: Optional[str] = None,
91
+ method: str = "append",
92
+ max_steps: int = 64,
93
+ ) -> Plan:
94
+ total, dr = string_energy(text, principle)
95
+ if not (1 <= target <= 9):
96
+ raise ValueError("Target energy must be 1..9")
97
+ if dr == target:
98
+ return Plan(method, target, [], total, dr, total, dr)
99
+ # Determine allowed symbol energies
100
+ if not allowed_symbols:
101
+ allowed_symbols = ".!?,*+" # sensible defaults with varied residues
102
+ allowed: List[Tuple[str, int]] = []
103
+ for s in allowed_symbols:
104
+ e = char_energy(s, principle)
105
+ if e == 0:
106
+ continue
107
+ allowed.append((s, e))
108
+ if not allowed:
109
+ raise ValueError("No valid allowed symbols; provide a non-empty set.")
110
+ delta_res = _target_residue(total, target)
111
+ seq = minimal_residue_combo(delta_res, allowed, max_steps=max_steps)
112
+ if seq is None:
113
+ raise ValueError("No feasible combination to reach target with given symbols.")
114
+ steps = build_steps_from_seq(seq, allowed)
115
+ added = sum(count * char_energy(sym, principle) for sym, count in steps)
116
+ new_total = total + added
117
+ new_dr = digital_root(new_total, principle.normalize_zero_to_nine)
118
+ return Plan(method, target, steps, total, dr, new_total, new_dr)
119
+
120
+
121
+ def apply_plan(text: str, plan: Plan, spread: Optional[int] = None) -> str:
122
+ if not plan.steps:
123
+ return text
124
+ if plan.method == "append":
125
+ suffix = "".join(sym * count for sym, count in plan.steps)
126
+ return text + suffix
127
+ if plan.method == "prepend":
128
+ prefix = "".join(sym * count for sym, count in plan.steps)
129
+ return prefix + text
130
+ if plan.method == "intersperse":
131
+ # Spread symbols approximately evenly through text
132
+ chars = list(text)
133
+ inserts: List[str] = []
134
+ for sym, count in plan.steps:
135
+ inserts.extend([sym] * count)
136
+ if not chars:
137
+ return "".join(inserts)
138
+ n = len(inserts)
139
+ if n == 0:
140
+ return text
141
+ interval = spread or max(1, len(chars) // (n + 1))
142
+ out: List[str] = []
143
+ i_ins = 0
144
+ for i, ch in enumerate(chars):
145
+ out.append(ch)
146
+ if (i + 1) % interval == 0 and i_ins < n:
147
+ out.append(inserts[i_ins])
148
+ i_ins += 1
149
+ while i_ins < n:
150
+ out.append(inserts[i_ins])
151
+ i_ins += 1
152
+ return "".join(out)
153
+ raise ValueError(f"Unsupported method: {plan.method}")
154
+
155
+
156
+ @dataclass
157
+ class EditOp:
158
+ kind: str # 'sub', 'del', 'ins'
159
+ pos: int
160
+ ch: Optional[str] = None
161
+ count: int = 1
162
+
163
+
164
+ @dataclass
165
+ class EditPlan:
166
+ method: str # 'substitute' or 'edit'
167
+ target: int
168
+ ops: List[EditOp]
169
+ total_before: int
170
+ dr_before: int
171
+ total_after: int
172
+ dr_after: int
173
+
174
+
175
+ def apply_edit_plan(text: str, plan: EditPlan) -> str:
176
+ if not plan.ops:
177
+ return text
178
+ chars = list(text)
179
+ # Apply deletions first from highest pos to lowest
180
+ dels = [op for op in plan.ops if op.kind == 'del']
181
+ for op in sorted(dels, key=lambda o: o.pos, reverse=True):
182
+ if 0 <= op.pos < len(chars):
183
+ del chars[op.pos]
184
+ # Apply substitutions
185
+ subs = [op for op in plan.ops if op.kind == 'sub']
186
+ for op in subs:
187
+ if 0 <= op.pos < len(chars) and op.ch is not None:
188
+ chars[op.pos] = op.ch
189
+ # Apply insertions from lowest pos to highest
190
+ ins = [op for op in plan.ops if op.kind == 'ins']
191
+ for op in sorted(ins, key=lambda o: o.pos):
192
+ insert_str = (op.ch or '') * max(1, op.count)
193
+ if 0 <= op.pos <= len(chars):
194
+ left = ''.join(chars[:op.pos])
195
+ right = ''.join(chars[op.pos:])
196
+ chars = list(left + insert_str + right)
197
+ return ''.join(chars)
198
+
199
+
200
+ def optimize_substitution(
201
+ text: str,
202
+ target: int,
203
+ principle: Principle,
204
+ subs: Optional[Dict[str, List[str]]] = None,
205
+ allow_delete: bool = False,
206
+ allowed_inserts: Optional[str] = None,
207
+ max_edits: int = 8,
208
+ ) -> EditPlan:
209
+ total, dr = string_energy(text, principle)
210
+ if not (1 <= target <= 9):
211
+ raise ValueError("Target energy must be 1..9")
212
+ if dr == target:
213
+ return EditPlan('substitute', target, [], total, dr, total, dr)
214
+
215
+ # Build per-position residue deltas for substitutions (and deletions)
216
+ n = len(text)
217
+ pos_deltas: List[List[Tuple[int, Optional[str]]]] = [[] for _ in range(n)]
218
+ for i, ch in enumerate(text):
219
+ cur_e = char_energy(ch, principle)
220
+ seen: Dict[int, str] = {}
221
+ # substitution options
222
+ if subs and ch in subs:
223
+ for repl in subs[ch]:
224
+ new_e = char_energy(repl, principle)
225
+ r = (new_e - cur_e) % 9
226
+ if r not in seen:
227
+ seen[r] = repl
228
+ # trivial case-change option for letters
229
+ if ch.isalpha():
230
+ alt = ch.swapcase()
231
+ if alt != ch:
232
+ new_e = char_energy(alt, principle)
233
+ r = (new_e - cur_e) % 9
234
+ if r not in seen:
235
+ seen[r] = alt
236
+ for r, repl in seen.items():
237
+ if r != 0: # ignore no-ops
238
+ pos_deltas[i].append((r, repl))
239
+ # deletion option
240
+ if allow_delete:
241
+ rdel = (-cur_e) % 9
242
+ if rdel != 0:
243
+ pos_deltas[i].append((rdel, None))
244
+
245
+ delta_res = _target_residue(total, target)
246
+ # DP over residues with minimal edits, tracking choices
247
+ # state: residue -> (count, prev_residue, (pos, r, repl))
248
+ best: Dict[int, Tuple[int, int, Tuple[int, int, Optional[str]]]] = {0: (0, -1, (-1, 0, None))}
249
+ frontier = [0]
250
+ for i in range(n):
251
+ new_best = best.copy()
252
+ for res, (cnt, prev, info) in best.items():
253
+ for r, repl in pos_deltas[i]:
254
+ nr = (res + r) % 9
255
+ nc = cnt + 1
256
+ if nc > max_edits:
257
+ continue
258
+ if nr not in new_best or nc < new_best[nr][0]:
259
+ new_best[nr] = (nc, res, (i, r, repl))
260
+ best = new_best
261
+
262
+ if delta_res not in best:
263
+ # Try insertion-only fallback using symbol residues
264
+ if allowed_inserts:
265
+ append_plan = optimize_attunement(text, target, principle, allowed_symbols=allowed_inserts, method='append')
266
+ ops: List[EditOp] = []
267
+ for sym, cnt in append_plan.steps:
268
+ ops.append(EditOp('ins', len(text), sym, cnt))
269
+ out = apply_edit_plan(text, EditPlan('edit', target, ops, total, dr, total, dr))
270
+ new_total, new_dr = string_energy(out, principle)
271
+ return EditPlan('edit', target, ops, total, dr, new_total, new_dr)
272
+ raise ValueError("No feasible substitution/deletion plan to reach target.")
273
+
274
+ # Reconstruct
275
+ ops: List[EditOp] = []
276
+ cur = delta_res
277
+ while cur != 0:
278
+ cnt, prev, (pos, r, repl) = best[cur]
279
+ if pos >= 0:
280
+ if repl is None:
281
+ ops.append(EditOp('del', pos))
282
+ else:
283
+ ops.append(EditOp('sub', pos, repl))
284
+ cur = prev
285
+ # Apply and compute totals
286
+ out = apply_edit_plan(text, EditPlan('edit', target, list(reversed(ops)), total, dr, total, dr))
287
+ new_total, new_dr = string_energy(out, principle)
288
+ return EditPlan('edit', target, list(reversed(ops)), total, dr, new_total, new_dr)