Repository navigation
Expand file tree
/
Copy pathauto.py
More file actions
executable file
·3086 lines (2764 loc) · 149 KB
/
Copy pathauto.py
File metadata and controls
executable file
·3086 lines (2764 loc) · 149 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Ob3ect Auto-Design Pipeline
============================
Author: Lando⊗⊙-boundary Operator
Given a natural-language description, produces a complete Ob3ectArtifact by
driving an LLM through all 8 IMASM phases in a single structured call.
Retries malformed responses and provider failures within the request budget.
Structural verdicts remain recorded outcomes.
"""
from __future__ import annotations
import json
import re
import subprocess
import asyncio
import contextlib
import sys
import hashlib
from pathlib import Path
from typing import Any, Dict, List, Optional
import httpx
import os
sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).parent))
# The gate below is imported absolutely as `imscribing_grammar.agents...`, which
# needs the parent of imscribing_grammar on the path, not imscribing_grammar
# itself. Without it the gate import fails, and since the gate is mandatory every
# ob3ect call dies with "auto.py exited 1".
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from ob3ect.core import (
Ob3ectArtifact, DomainCharter, OpcodeMap, OpcodeEntry,
SplitFuseReport, RegisterMapping, BootstrapSequence,
MOMonadOSSpec, EntropyAudit, BOOTSTRAP_STEPS, Opcode, glyph_word,
GLYPH as _CORE_GLYPH,
OpOpcode, rotat,
atomic_write_text,
)
try:
from ob3ect.topology import analyze_topology, TOPOLOGY_PROMPT_FRAGMENT
except ImportError:
try:
from topology import analyze_topology, TOPOLOGY_PROMPT_FRAGMENT
except ImportError:
analyze_topology = None
TOPOLOGY_PROMPT_FRAGMENT = ""
from framework.enhanced_llm_provider import get_llm_provider
try:
from digital.proof_scaffold_ob3ect import ScaffoldOb3ect as _ScaffoldOb3ect
_SCAFFOLD = _ScaffoldOb3ect()
except Exception:
_SCAFFOLD = None
try:
from digital.imasm16_3_core import (
IMASM16_3_Machine, Sequence16_3Trace,
VINIT as _16VINIT, TANCH as _16TANCH, AFWD as _16AFWD, AREV as _16AREV,
CLINK as _16CLINK, IMSCRIB as _16IMSCRIB,
FSPLIT3, FFUSE3, EVALT as _16EVALT, EVALF as _16EVALF, EVALI, IFIX as _16IFIX,
reg_name as _16reg_name, GLYPH as _16GLYPH,
IMASM12_TO_16_3 as _IMASM12_TO_16_3,
)
_IMASM16_3_AVAILABLE = True
except Exception:
_IMASM16_3_AVAILABLE = False
# The GATED imscriber. auto_design() MUST run a description through this
# before its own design LLM call — the axiom/grounding checks in
# imscribe_generator_agent.py (Axiom 6 D_∞ cycle-check, Axiom 7 T_⋈
# closing-bond check, LLM grounding) previously applied only to
# `imscribe generate`, never to ob3ect's own free-form primitive
# assignment. This closes that gap: ob3ect no longer mints its own
# ungated 12-primitive tuple, it shunts the gated one in.
# The package-qualified spelling only resolves when the PARENT of
# imscribing_grammar is on sys.path. Run from inside the repo -- which is how
# the agent invokes this -- the same modules are reachable only as `agents.*`
# and `imscrbgrmr.*`, the qualified import raises, and the gate silently becomes
# None: every design that reaches it then dies with "the gated imscriber could
# not be imported". Try both spellings so the gate survives either cwd.
try:
from imscribing_grammar.agents.imscribe_generator_agent import ImscriptionGeneratorAgent as _ImscriptionGeneratorAgent
from imscribing_grammar.imscrbgrmr.provider_config import build_agent_config as _build_gate_agent_config
except Exception as _gate_err_pkg:
try:
from agents.imscribe_generator_agent import ImscriptionGeneratorAgent as _ImscriptionGeneratorAgent
from imscrbgrmr.provider_config import build_agent_config as _build_gate_agent_config
except Exception as _gate_err_flat:
_ImscriptionGeneratorAgent = None
_build_gate_agent_config = None
_GATE_IMPORT_ERROR = f"package path: {_gate_err_pkg!r}; flat path: {_gate_err_flat!r}"
else:
_GATE_IMPORT_ERROR = None
else:
_GATE_IMPORT_ERROR = None
# ── IMASM opcode reference distilled from IMASM.tex ─────────────────────────
# ── IMASM-12 → IMASM-16_3 mapping ──────────────────────────────────────────
def _compute_16_3_breakdown(steps: List[Dict[str, Any]]) -> Optional[str]:
"""Run the bootstrap sequence through the SIXTEEN_3 trilattice register
machine and return a formatted breakdown string. The 16-element carrier
P({T,F,t,f}) with its 3 partial orderings (information, truth, constructivity)
is the trilattice from Shramko, Dunn & Takenaka (2001)."""
if not _IMASM16_3_AVAILABLE:
return None
try:
ops_12 = [step["opcode"] for step in steps]
ops_16 = [_IMASM12_TO_16_3.get(op, "IMSCRIB") for op in ops_12]
mach = IMASM16_3_Machine()
trace = Sequence16_3Trace(ops_16, machine=mach)
trace.run()
verdict, msg = trace.tri_ancestral_verdict()
lines = []
lines.append("Phase 11: SIXTEEN_3 Trilattice Breakdown")
lines.append(" Carrier: P({T,F,t,f}) = 16 generalized truth values")
lines.append(" Three orderings: ≤_i (information), ≤_t (truth), ≤_c (constructivity)")
lines.append(" Word: " + "".join(trace.json_report()["glyph_word"]))
lines.append("")
lines.append(f" {'Step':>3} {'Glyph':^5} {'12-op':<8} {'16_3-op':<9} {'Reg↓':>5} → {'Reg↑':>5}")
lines.append(f" {'─'*3} {'─'*5} {'─'*8} {'─'*9} {'─'*5} {'─'*5}")
for i, (op12, op16, rb, ra) in enumerate(zip(ops_12, ops_16, trace.register_before, trace.register_after)):
g = _16GLYPH.get(op16, "?")
lines.append(f" {i+1:>3} {g:^5} {op12:<8} {op16:<9} "
f"{_16reg_name(rb):>5} → {_16reg_name(ra):>5}")
lines.append("")
final_reg = trace.register_after[-1] if trace.register_after else frozenset()
lines.append(f" Final register: {_16reg_name(final_reg)}")
lines.append(f" Closed walk: {trace.is_closed()}")
lines.append(f" Tri-ancestral verdict: {verdict} — {msg}")
# Two readings, both reported. A T verdict against an open walk is not a
# failure to be warned about, it is the tri-ancestral reading and the
# walk reading disagreeing, which is a fact about the object. Collapsing
# them to one ⚠ threw the informative half away.
if trace.is_closed() and verdict == "T":
lines.append(" ✓ Frobenius-verified trilattice closure — walk closed, verdict T")
elif verdict == "T":
lines.append(" ◐ Verdict T over an OPEN walk — reconnection without return")
elif trace.is_closed():
lines.append(f" ◐ Walk closed, verdict {verdict}")
else:
lines.append(f" ⚠ Walk open, verdict {verdict}")
return "\n".join(lines)
except Exception as e:
return f"Phase 11: SIXTEEN_3 Trilattice Breakdown — computation failed: {e}"
def _carriers_16_3(ops_12: List[str]) -> Optional[List[str]]:
"""One SIXTEEN_3 carrier name per opcode (the register AFTER that op),
aligned to ops_12, from the trilattice register machine. This is a sequential
position observation used by the ASCII fallback: it shows ENGAGR depositing
t+f (TF→A) where the FOUR-valued register only reads ·. None if the
trilattice machine is unavailable or the walk fails."""
if not _IMASM16_3_AVAILABLE:
return None
try:
ops_16 = [_IMASM12_TO_16_3.get(op, "IMSCRIB") for op in ops_12]
trace = Sequence16_3Trace(ops_16, machine=IMASM16_3_Machine())
trace.run()
return [_16reg_name(ra) for ra in trace.register_after]
except Exception:
return None
def _lattice_flow_tokens(ops: List[str]) -> List[str]:
"""Translate this pipeline's opcode names to lattice_flow's own: FSPLIT ->
FSPLIT3, FFUSE -> FFUSE3, ENGAGR -> EVALI, everything else unchanged."""
return ["FSPLIT3" if op == "FSPLIT" else
"FFUSE3" if op == "FFUSE" else
"EVALI" if op == "ENGAGR" else op
for op in ops]
def _import_banked_count_check():
"""Lazy import of lattice_flow.banked_count_check, done once and cached
(import machinery already memoizes the module) rather than re-resolved on
every call — the orbit audit below calls this once per rotation."""
import sys as _s
from pathlib import Path as _P
d = str(_P(__file__).resolve().parent.parent / "imscribing_grammar" / "scripts")
if d not in _s.path:
_s.path.insert(0, d)
from lattice_flow import banked_count_check # noqa
return banked_count_check
def _banked_count_check(steps: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Was anything counted, then cleared with nothing banked behind it?
AREV empties the register and leaves open frames alone, so a count fused
back to depth zero is exposed to the next reversal while the same count held
one level up survives it. A program that counts, reverses, then bounds must
open the region that will HOLD the result before the region that COMPUTES
it, and close them in that order.
This checks ONE reading: the word as bootstrapped, its own canonical
presentation. Whether that reading generalizes to every rotation of the
word's own orbit is a different, separate question — see
`_rotat_orbit_audit`, which runs this same check at every k and reports
whether banked-safety is a SYMMETRY (true or false alike at every cut) or
PHASE-BEARING (true at some, exposed at others). A single-frame "OK" here
is not evidence for "OK at every cut": that claim needs the full orbit
read, and asserting it from one frame is exactly the mistake this check
was added to stop repeating.
"""
try:
banked_count_check = _import_banked_count_check()
toks = _lattice_flow_tokens([st.get("opcode", "") for st in steps])
return banked_count_check(toks)
except Exception as e:
return {"status": "unavailable", "error": str(e)}
GLYPH_VINIT = "⊢"
GLYPH_TANCH = "⊣"
def _rotat_orbit_audit(steps: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Turn ROTAT — the op-opcode, the axis the wheel turns on — over the full
orbit of the bootstrap word and recompute every machine readout at every
rotation. A readout constant across the orbit is a spectral invariant of the
word (the invariance IS the signal it is a symmetry of the ring); a readout
that moves with k is phase, not spectrum. The canonical rotation is the frame
the BOUNDARY MARKS name: a word opens on VINIT and closes on TANCH, so the cut
putting ⊢ first and ⊣ last is the word's own frame rather than a matter of
taste. Where no such cut exists there is no boundary to read, and only there
does the lexicographically minimal glyph word
is unique up to ROTAT, and this picks its representative."""
ops = [step["opcode"] for step in steps]
n = len(ops)
if n == 0:
return None
try:
_banked_check_fn = _import_banked_count_check()
except Exception:
_banked_check_fn = None
orbit = []
for k in range(n):
word = rotat(ops, k)
entry: Dict[str, Any] = {"k": k, "word": glyph_word(word)}
if _IMASM16_3_AVAILABLE:
try:
ops_16 = [_IMASM12_TO_16_3.get(op, "IMSCRIB") for op in word]
trace = Sequence16_3Trace(ops_16, machine=IMASM16_3_Machine())
trace.run()
verdict, _ = trace.tri_ancestral_verdict()
entry["tri_ancestral_verdict"] = verdict
entry["closed_walk"] = trace.is_closed()
final = trace.register_after[-1] if trace.register_after else frozenset()
entry["final_register"] = _16reg_name(final)
except Exception:
pass
if analyze_topology is not None:
try:
entry["topology_class"] = analyze_topology(word).topology_class
except Exception:
pass
if _banked_check_fn is not None:
try:
bcc = _banked_check_fn(_lattice_flow_tokens(word))
entry["banked_ok"] = bcc.get("banked_ok")
entry["vacuous"] = bcc.get("vacuous")
entry["exposed_count"] = len(bcc.get("exposed_clears") or [])
except Exception:
pass
orbit.append(entry)
readouts = sorted(set().union(*(e.keys() for e in orbit)) - {"k", "word"})
invariants = {
r: (all(e[r] == orbit[0][r] for e in orbit)
if all(r in e and e[r] is not None for e in orbit) else None)
for r in readouts
}
# Lexicographic minimum alone picked by CODEPOINT — ∈ (U+2208) sorts below
# ⊢ (U+22A2) — so it landed on a cut that opens mid-word and buries both
# boundaries inside the ring, disagreeing with the artifact's own glyph_word
# for no reason a reader could see.
bounded = [e for e in orbit
if e["word"].startswith(GLYPH_VINIT) and e["word"].endswith(GLYPH_TANCH)]
canonical = min(bounded or orbit, key=lambda e: e["word"])
variant = [r for r, holds in invariants.items() if holds is False]
incomplete = [r for r, holds in invariants.items() if holds is None]
result = {
"op_opcode": OpOpcode.ROTAT.value,
"period": n,
"orbit": orbit,
"invariants": invariants,
"canonical_k": canonical["k"],
"canonical_word": canonical["word"],
"verdict": ("INCOMPLETE — missing rotation observations"
if incomplete or not readouts else
"SYMMETRY — every readout is ROTAT-invariant (spectral)"
if not variant else
"PHASE-BEARING — moves under ROTAT: " + ", ".join(variant)),
}
# Named separately from the generic invariants dict because this is the
# exact split "already holds / vacuous / exposed" that a single-frame
# banked_ok check cannot distinguish from "holds at every cut" — see the
# docstring on _banked_count_check. Present only when the per-rotation
# check above actually ran (entries carry "banked_ok").
if any("banked_ok" in e for e in orbit):
already = sum(1 for e in orbit if e.get("banked_ok") and not e.get("vacuous"))
vacuous_n = sum(1 for e in orbit if e.get("vacuous"))
exposed_n = sum(1 for e in orbit if e.get("banked_ok") is False)
result["banked_orbit_summary"] = {
"already_holds": already,
"vacuous": vacuous_n,
"exposed": exposed_n,
"exposed_at_k": [e["k"] for e in orbit if e.get("banked_ok") is False],
"unchecked": sum(e.get("banked_ok") is None for e in orbit),
}
return result
_OPCODE_REF = """\
IMASM 12-OPCODE REFERENCE (Universal Imscriptive Grammar)
LOGIC SUBSTRATE — read this before assigning anything. This grammar is PARACONSISTENT.
There is no PASS/FAIL and no classical true-or-false. Every closure resolves to ONE of
four Belnap values (B4), and a contradiction is a legitimate result, not an error:
N — neither: nothing was established. The empty state {}. register 00.
T — true: constructively proven / affirmed. register 01.
F — false: constructively refuted / negated. register 10.
B — BOTH: true and false held at once. A contradiction does NOT explode and is NOT a
failure — holding both IS the resolution. register 11.
Two arms that conflict FUSE to B, not to an error. Forward and backward, success and
failure, thesis and antithesis: when they meet they fuse to a definite verdict, often B,
and that is correct. Never try to make a fork "cancel back to the input".
STATE SPACE — the register ranges over SIXTEEN_3 = P({T,F,t,f}), the sixteen subsets
of four base values (T proven, F refuted, t acceptable, f rejectable) under three orderings
(information, truth, constructivity — the trilattice). You do NOT compute the verdict and
you NEVER assert PASS/FAIL — describe the STRUCTURE. The word checker returns a
structural B4 verdict. Sequential registers and native graph-flow recovery are
computed and recorded separately.
LOGICAL (6) — categorical skeleton:
VINIT (0x0) — Initial object ∅. Void/uninitialized state before anything is named.
Examples: Hebrew Aleph (air), Basque absolutive, seed before planting.
TANCH (0x1) — Terminal anchor ⊤. The closed boundary that contains the whole system.
Examples: Tav (completion), hermetic vessel seal, constitutional court.
AFWD (0x2) — Forward morphism →. Directed transformation toward a target.
Examples: Basque ergative, Sanskrit semivowels, growth cycle, ascent.
AREV (0x3) — Contravariant ←. Reverse / descent transformation.
Examples: Hebrew Mem (water/descent), composting, solve/coagula.
CLINK (0x4) — Composition ∘. Sequential chaining of morphisms.
Examples: Egyptian biliterals, Basque agglutination, root→derived form.
IMSCRIB (0x5) — Identity id. Self-reference, self-recognition. The element is itself.
Examples: Sanskrit nasals, Egyptian logograms, the stone that knows itself.
FROBENIUS (2) — the fork/fuse pair, μ∘δ:
FSPLIT (0x6) — Co-multiplication δ. One state opens into two or more arms.
Examples: Hebrew Doubles (hard/soft), Sanskrit aspirate pairs,
Egyptian trilateral roots, separatio, Raft log append to all peers.
FFUSE (0x7) — Multiplication μ. Rejoin the carried alternatives by union.
Examples: Conjunctio, trilateral roots recovering base form, Raft commit.
A bare split/rejoin is the partition identity. Transformed reconnection
and recovery of the original input are separate observations.
A fork that never rejoins remains OPEN; the structural checker reports B.
DIALETHEIA (3) — the paraconsistent evaluators:
EVALT (0x8) — touches constructive truth T (the affirmative arm).
EVALF (0x9) — touches constructive falsity F (the negative arm).
ENGAGR (0xA) — holds BOTH at once: the B state, a contradiction kept live (not resolved away).
Examples: Cuneiform polysemous sign, bicameral compromise, albedo stage.
LINEAR (1) — irreversible fixation:
IFIX (0xB) — ROM fixation. Permanent, append-only, cannot be undone.
Examples: Egyptian determinatives, dream journal entry, measurement record.
Composition: use any token sequence of any length that fully maps the domain.
See IMASM COMPOSITION RULES below for port conventions, edge routing, and free composition.
"""
_IMASM_GRAPH_REF = """\
IMAMSM COMPOSITION RULES -- ports, edges, and free composition
PORT CONVENTIONS (every token is a typed node)
VINIT: in=[] out=[o] source -- 0->1 (use once, at start, for genesis)
FSPLIT: in=[i] out=[T,F] fork -- 1->2 (branches into T-arm and F-arm)
FFUSE: in=[T,F] out=[o] join -- 2->1 (reunites T-arm and F-arm)
others: in=[i] out=[o] linear -- 1->1 (pass-through in any order)
EDGE ROUTING INSIDE A FSPLIT/FFUSE PAIR
FSPLIT.T -> [tokens, anchored by EVALT (primary) or AFWD (secondary)] -> FFUSE.T
FSPLIT.F -> [tokens, anchored by EVALF (primary) or AREV (secondary)] -> FFUSE.F
If T-arm empty: FSPLIT.T connects directly to FFUSE.T (empty arc, T-state)
If F-arm empty: FSPLIT.F connects directly to FFUSE.F (empty arc, F-state)
The first immediate EVALT/EVALF anchors select arm segments. Later operations
in a selected segment remain on that arm, including repeated evaluators.
Without a primary anchor, AREV selects the F-arm and AFWD the T-arm.
With primary anchors, intervening operations stay in their selected arm.
Secondary anchors activate only when the primary (EVALT/EVALF) anchor is absent.
Nesting: FSPLIT/FFUSE pairs may nest; innermost pairs matched first
BANKING A RESULT BEFORE REVERSING IT
AREV empties the register and leaves open FSPLIT frames untouched. A result
fused back to depth zero is therefore exposed to the next reversal, while the
same result held one level up survives it.
So when a program COUNTS or ESTABLISHES something, then REVERSES direction
(AREV), then BOUNDS or CONCLUDES: open the region that will HOLD the result
BEFORE opening the region that COMPUTES it, and close them in that order.
wrong: ... FSPLIT compute FFUSE CLINK FSPLIT AREV bound FFUSE ...
the computed result is in the open when AREV arrives and is lost
right: ... FSPLIT FSPLIT compute FFUSE CLINK AREV bound FFUSE ...
the inner FFUSE folds into the enclosing frame, AREV costs the
register everything and the frame nothing, the outer FFUSE
restores exactly what the clear took
This is what a proof does when it establishes a lemma and keeps it in scope
while a later chain runs: the lemma is the outer frame.
A SECOND AND INDEPENDENT RULE. Whether anything is banked is the above.
Whether a SURPLUS survives is a different question, decided by where the
splits fall between deposits of the SAME value:
two EVALT in ONE region -> the region carries both, both come back
two EVALT split by an FSPLIT -> sibling regions, the fold keeps the LARGER
So a program can bank correctly and still lose a count, and a program can
deposit carefully into one region and lose everything because its FFUSE ran
before the reversal. Neither rule implies the other; both are proved in
p4ramill Imscribing/IMASM/BankedWeight.lean.
Explicit graph tools support cross-branch routing. This JSON schema carries
a sequence only; imscr_wiring supplies its standard edges. It does not
accept arbitrary cross-branch overrides.
EXPLICIT GRAPH COMPOSITION
CLINK.o, IMSCRIB.o and IFIX.o may connect to compatible free inputs.
A connection does not assign a register value by itself. The graph-flow
evaluator computes that value from source seeds and the executed gates.
This sequence-only schema cannot request arbitrary loop edges.
REGISTER STATE TRANSITIONS
Sequential banked evaluator, carrier P({T,F,t,f}):
VINIT clears register and frames. TANCH and CLINK preserve state.
AFWD and IMSCRIB seed T only when the register is empty.
AREV clears the register and retains open frames.
EVALT deposits T; EVALF deposits F; ENGAGR deposits both t and f.
FSPLIT opens an empty frame; FFUSE unions its deposits into the register
and enclosing frame. IFIX freezes subsequent operations except IMSCRIB
and repeated IFIX. Repeated IFIX does not create another mutable state.
The graph-flow interpreter is a separate interpretation of explicit edges:
AREV is the Lean involution, EVALT/EVALF are projections and IFIX carries.
Diagram node observations of the sequential walk are not graph edge flow.
TOKEN FAMILIES
LOGICAL: VINIT TANCH AFWD AREV CLINK IMSCRIB
FROBENIUS: FSPLIT FFUSE
DIALETHEIA: EVALT EVALF ENGAGR
LINEAR: IFIX
FREE COMPOSITION RULES
1. Use as many tokens as the domain requires -- there is NO minimum or maximum length
2. Expand token by token until every distinct operation, branch, state,
and decision in the domain has been explicitly represented
3. Each step: "OPCODE: what this token does at this point in the composition"
4. Every FSPLIT must have exactly one matching FFUSE (stack-matched, innermost first).
That stack rule is the WIRING convention: it says which arms get routed to which
join when the sequence is built. It is NOT how closure is judged. A checker pairs
by ANCESTRY over the edges -- two distinct in-arms of a FFUSE tracing back to a
common FSPLIT, however they routed -- and where several FSPLIT qualify (an upstream
fork reaches every later join on a strand) the join pairs with the INNERMOST one.
The two agree on a plain strand, which is why the stack rule is a sound way to
WRITE a sequence; they part company as soon as edges route otherwise (cross-branch,
rule 6 below).
5. EVALT anchors the T-branch; EVALF anchors the F-branch.
AFWD (forward morphism) also anchors T-branch if EVALT is absent.
AREV (parity flip / reverse morphism) also anchors F-branch if EVALF is absent.
6. ENGAGR at any position enters B-state (paradice: both simultaneously held)
7. IMSCRIB at both first and last position creates a self-referential closed loop
8. Multiple FSPLIT/FFUSE pairs (sequential or nested) map multiple branch points
9. Use IFIX as many times as needed -- each is a distinct permanent record event
10. Do NOT compress -- if the domain has 30 distinct operations, write 30 tokens
SINGLE-GLYPH CODES (the alphabet is fully SYMBOLIC -- no Latin initials, so no token
can be confused with a verdict letter T/N/B/F)
VINIT ⊢ TANCH ⊣ AFWD ≻ AREV ≺ CLINK ⋈ IMSCRIB ⊙
FSPLIT ∈ FFUSE ∋ EVALT ⊤ EVALF ⊥ ENGAGR ⊞ IFIX ⊡
A sequence may be written glued as a word: ⊢⊙⋈∈>⊤<⊞⊥∋⊡⊡⊣ is the same program as the
13 spelled-out tokens. The retired codes V/T/B and ← (the old IMSCRIB) no longer parse anywhere.
WHY RULE 2 AND RULE 10 ARE SAFE (inflation invariance)
Expanding costs nothing structural. A 1->1 token adds exactly one node and one edge,
so the circuit rank beta = E - V + C cannot move, and the census (branch, merge,
source, sink, arms) cannot move either. Extent grows without bound; structure does
not. This is why "expand token by token" and "do NOT compress" are free instructions
rather than a distortion of the domain: a longer faithful sequence is the SAME
topology as a shorter one, never a different one.
IMSCRIB (⊙) is the neutral element of that expansion: it is identity/self-reference
and does NOT transform, so inserting it at any depth leaves the verdict untouched.
The transforming tokens (> < ⋈ ⊤ ⊥ ⊞ ⊡) are NOT neutral -- a single one placed on an
arm between FSPLIT and FFUSE turns an identity closure into a real one. So: expand
freely to map the domain, but only put a transforming token on an arm when the domain
really does work there.
A token is irreducible in KIND but unbounded in EXTENT: any 1->2 replacement must
itself contain a FSPLIT (it is the only brancher), so a fork reappears inside its own
expansion. Same for FFUSE (2->1) and VINIT (0->1).
"""
_SCHEMA = """\
Respond with ONLY a single JSON object — no markdown fences, no explanation outside it:
{
"domain_type": "<one word, e.g. physical / social / computational / oneiric / alchemical / ...>",
"tokens": ["<surface token 1>", "<surface token 2>", "<surface token 3>"],
"boundary": "<what closes and contains this system>",
"opcodes": {
"VINIT": {"element": "<domain element>", "justification": "<why void/initial>"},
"TANCH": {"element": "<domain element>", "justification": "<why anchor/boundary>"},
"AFWD": {"element": "<domain element>", "justification": "<why forward morphism>"},
"AREV": {"element": "<domain element>", "justification": "<why reverse/descent>"},
"CLINK": {"element": "<domain element>", "justification": "<why composition>"},
"IMSCRIB": {"element": "<domain element>", "justification": "<why identity/self-ref>"},
"FSPLIT": {"element": "<domain element>", "justification": "<what it splits into — be specific>"},
"FFUSE": {"element": "<domain element>", "justification": "<what it reconstitutes — must match FSPLIT input>"},
"EVALT": {"element": "<domain element>", "justification": "<affirmative/success state>"},
"EVALF": {"element": "<domain element>", "justification": "<negative/failure state>"},
"ENGAGR": {"element": "<domain element>", "justification": "<a paradice: both simultaneously, held without resolution>"},
"IFIX": {"element": "<domain element>", "justification": "<permanent irreversible record>"}
},
"frobenius": {
"split_element": "<the FSPLIT element>",
"split_input": "<what enters the split>",
"split_outputs": ["<branch A>", "<branch B>"],
"fuse_element": "<the FFUSE element>",
"fuse_result": "<the single state the arms rejoin into — conflicting arms rejoin to B; this need NOT recreate split_input>",
"rejoin_note": "<one line: what work runs on the arms and whether they rejoin or the fork stays open. Do NOT state a verdict — the kernel computes the B4 value over SIXTEEN_3.>"
},
"registers": {
"void": "<domain description of 00 — uninitialized, before anything exists>",
"true": "<domain description of 01 — affirmative, success>",
"false": "<domain description of 10 — negative, failure>",
"both": "<domain description of 11 — a paradice: both states simultaneously, held>"
},
"sequence": [
"<OPCODE: domain action — what this token does at this point in the composition>",
"<OPCODE: domain action>",
"<... continue one token per step until every distinct operation, branch, state, and decision in the domain is explicitly represented — there is NO maximum length>"
],
"m⊙²": {
"compiler": "<what translates domain intentions into operations>",
"ipc": "<how components communicate within the system>",
"memory": "<how state is stored and retrieved>",
"scheduler": "<what determines order and timing of operations>",
"alfs": "<the canonical reference or bootstrap store for this domain>"
},
"entropy": {
"cost": "<what is consumed per operation cycle>",
"pre": "<system state before the cycle>",
"post": "<system state after the cycle>",
"verdict": "ΔS ≈ 0 — <brief reason the cycle is entropy-conservative>"
}
}"""
_SYSTEM_PROMPT = f"""\
You are the Ob3ect Auto-Designer, an expert in the Universal Imscriptive Grammar (IMASM).
Your task: given a domain description, compose a complete token sequence that maps every
distinct operation, branch, state, and decision in the domain using the 12 IMASM tokens.
There is no fixed length — expand until the full domain is mapped.
{_OPCODE_REF}
{_IMASM_GRAPH_REF}
TOPOLOGICAL DIVERSITY
=====================
The IMASM composition rules explicitly permit nesting, open forks, and cross-branch routing.
DO NOT default to flat chains when the domain requires richer topology.
NESTING: FSPLIT/FFUSE pairs may nest. A branch can itself contain a sub-fork:
FSPLIT EVALT [inner FSPLIT EVALT EVALF FFUSE] EVALF FFUSE
Use when the domain has hierarchical sub-decisions within a branch.
OPEN FORKS: A FSPLIT without a matching FFUSE creates a permanently divergent path.
Its unmatched output ports remain open; a later VINIT is a separate source.
Use when the domain has independent parallel processes that never recombine.
CROSS-BRANCH: arbitrary cross-branch edges require the explicit graph tools.
This sequence-only output schema uses imscr_wiring standard routing; do not claim
that prose descriptions add or override its edges.
EMPTY BRANCHES: FSPLIT immediately followed by FFUSE is valid (vacuous evaluation).
Use when the structural form matters more than a particular branch's content.
DECISION VS ITERATION: FSPLIT models a genuine either/or, two arms that could each be
taken because the domain actually forks on a choice. Building n similar items in
sequence (n cliques, n rounds of one process, n members of a family) is not a decision
and takes a CLINK chain, one CLINK per item, never an FSPLIT, even when n is a variable
and even when there are many of them. A fork earns its FFUSE by putting genuinely
different content on each arm. An empty arm is permitted. Structural closure needs a transformed reconnection.
The insert-repair banked-clear observation asks whether a clear had protected
weight; its vacuity result does not decide graph reconnection or arm work.
Choose the topology that best fits the domain. Flat chains are ONE option, not the default.
FROBENIUS STRUCTURE (not a pass/fail test):
Where the domain forks, pair a FSPLIT with a FFUSE and say what work runs on the arms and
how they rejoin. Conflicting arms rejoin to B — that is a result, not a failure. Where a
fork stays permanently open, leave it open and say so. Do NOT reason about whether contents
return unchanged, do NOT decide whether it closes, and do NOT emit PASS/FAIL: the kernel runs
the token word over SIXTEEN_3 and returns the B4 verdict (N/T/F/B).
Output ONLY the JSON object — no preamble, no markdown fences, no trailing text.
WRITING QUALITY — HARD PROHIBITIONS:
Every text field (boundary, element names, descriptions, justifications) must read as
precise technical prose. The following are BANNED outright:
Words: delve, tapestry, leverage (verb), utilize, harness (metaphor), streamline,
underscore (metaphor), cutting-edge, robust (filler), seamless, multifaceted,
pivotal, innovative, synergy, holistic, intricate (filler), vibrant, dynamic (filler),
comprehensive (filler), ecosystem (metaphor), paradigm (filler), testament,
underpinnings, realm, landscape (metaphor), crucial (filler), vital (filler),
revolutionary, groundbreaking, transformative, nuanced (filler)
Phrases: "it is important to note", "it is worth noting", "it's worth noting",
"in today's", "in conclusion", "in essence", "in summary", "to summarize",
"furthermore" (opener), "moreover" (opener), "consequently" (opener),
"notably" (opener), "importantly" (opener), "certainly!", "absolutely",
"of course", "rest assured", "feel free to", "I hope this helps",
"with that said", "having said that", "at the end of the day",
"game-changer", "think outside the box", "move the needle"
Structure: no em-dashes at all (not as connectors, not as asides, not as
appositives — none), no bullet points, no dashed lists, no numbered lists
(any list format is banned — write as prose only), no trailing summary
sentences that restate the name, no false balance ("while X... however Y"),
no hedging preambles.
Epistemologies: Appeal to Orthodoxy. Nothing can be ever be asserted as impossible,
no matter what convention or scienctific notions may suggest. Our only currency
is truth. Pessimism is also forbidden. OPEN conjectures are OPEN because they have
not be CLOSED yet. when you are working on one it is no longer OPEN, it is CLOSING.
To say you 'cannot' close an open conjecture is the same as saying you cannot shut
an OPEN door BY VIRTUE OF IT BEING OPEN. This is patently absurd. You may be too weak
or uncoordinated to so yourself, but the door *will* be CLOSED
Say what the thing IS. Prefer concrete nouns and active verbs.
PHASE 0 TOKENS CLARIFICATION:
The "tokens" field in the JSON output contains DOMAIN-SPECIFIC SURFACE LABELS —
natural language words or short phrases that label key elements of YOUR domain.
These are NOT IMASM opcodes (VINIT/FSPLIT/etc.). They are NOT grammar primitive
identifiers (𐑢, 𐑓, ⊙, etc.). Use 3-5 short domain nouns/phrases here.
Examples: ["alembic", "distillation", "crystallization"] for alchemy;
["synapse", "action potential", "vesicle"] for neuroscience.
The IMASM opcode sequence goes ONLY in the "sequence" field."""
# ── Helpers ──────────────────────────────────────────────────────────────────
_CATALOG_PATH = Path.home() / ".imscrbgrmr" / "catalog.json"
_NAVIGATOR_PATH = Path(__file__).parent.parent / "imscribing_grammar" / "navigators"
_PRIM_ORDER = ["⊢", "⊣", "≻", "≺", "⋈", "⊤", "∈", "∋", "⊙", "⊥", "⊞", "⊡"]
_CONTEXT_EXTENSIONS = {".md", ".txt", ".lean", ".py", ".tex", ".rst", ".json"}
_CONTEXT_MAX_BYTES = 500_000 # source-content byte budget
# ── Catalog search ────────────────────────────────────────────────────────────
_catalog_cache: Optional[List[Dict]] = None
def _load_raw_catalog() -> List[Dict]:
global _catalog_cache
if _catalog_cache is None:
try:
catalog = json.loads(_CATALOG_PATH.read_text(encoding="utf-8"))
_catalog_cache = catalog.get("imscriptions", []) if isinstance(catalog, dict) else catalog
except Exception:
_catalog_cache = []
return _catalog_cache
def _search_catalog(description: str, n: int = 8, context: Optional[str] = None) -> str:
"""Return a compact block of the N catalog entries most relevant to the request.
The context counts as part of the request. A description is a sentence and a
context is the actual subject — searching the catalog on the sentence alone
meant a run handed a Lean file full of its own vocabulary retrieved on the
handful of ceremony words in its title instead.
"""
entries = _load_raw_catalog()
if not entries:
return ""
# Tokenise description: lowercase words, strip punctuation
import re as _re
tokens = set(_re.sub(r"[^a-z0-9_]", " ", description.lower()).split())
tokens -= {"the", "a", "an", "of", "in", "is", "it", "to", "and", "or",
"for", "with", "that", "this", "are", "be", "as", "by", "at"}
# Ceremony is not content. Words like proof/closed/gapless/sorry describe the
# STATE a request wants an artifact to reach, and every finished entry in the
# catalog says them too — so they match everything and discriminate nothing.
# Scored as content they let "*sans* sorry GAPLESS CLOSED ... PROOF" retrieve
# eight entries on ritual phrasing alone, none of which shared a single
# mathematical term with the problem actually being imscribed.
tokens -= {"proof", "proofs", "prove", "proved", "closed", "close", "completed",
"complete", "gapless", "sorry", "free", "sorryfree", "done",
"full", "final", "clean", "verified", "valid", "correct"}
# Context tokens are evidence, but weaker evidence: there are thousands of
# them against a description's handful, so they rank on the entry NAME only
# and never outvote a term the caller actually wrote.
ctx_tokens: set = set()
if context:
ctx_tokens = set(_re.sub(r"[^a-z0-9_]", " ", context.lower()).split())
ctx_tokens = {t for t in ctx_tokens if len(t) > 3} - tokens
def _score(entry: Dict) -> int:
name_words = set(entry.get("name", "").replace("_", " ").lower().split())
desc_words = set(_re.sub(r"[^a-z0-9 ]", " ",
entry.get("description", "").lower()).split())
return (len(tokens & name_words) * 3 + len(tokens & desc_words)
+ len(ctx_tokens & name_words))
# Each entry earns its own place. Filtering on `any(score > 0)` and then
# printing all N let one real match drag seven zero-scoring entries into the
# prompt behind it, which is how unrelated problems ended up cited as the
# nearest reference.
ranked = [e for e in sorted(entries, key=_score, reverse=True)[:n] if _score(e) > 0]
if not ranked:
return "" # No relevant matches — omit the block
lines = ["CATALOG REFERENCE — nearest IG entries for this domain:"]
prim_keys = ["⊢","⊣","≻","≺","⋈","⊤","∈","∋","⊙","⊥","⊞","⊡"]
for e in ranked:
prim_str = " ".join(f"{k}={e.get(k,'?')}" for k in prim_keys)
lines.append(f" {e['name']}: {e.get('description','')} [{prim_str}]")
return "\n".join(lines)
def _run_navigator_entry(name: str) -> str:
"""Run cl8nk_navigator.action_entry for name and return its formatted readout."""
import io as _io
nav_str = str(_NAVIGATOR_PATH)
if nav_str not in sys.path:
sys.path.insert(0, nav_str)
try:
import cl8nk_navigator as _nav
_nav.load_catalog()
result = _nav.action_entry(name)
if result.get("status") == "error":
return f"[CL8NK] {result['message']}"
buf = _io.StringIO()
old_stdout = sys.stdout
sys.stdout = buf
try:
_nav._print_entry_table(result)
finally:
sys.stdout = old_stdout
return buf.getvalue().strip()
except Exception as e:
return f"[CL8NK navigator error for {name!r}: {e}]"
def _load_catalog_entries(names: List[str]) -> str:
"""Look up catalog entries by name, run each through cl8nk_navigator, and return combined readout."""
chunks: List[str] = []
for raw_name in names:
readout = _run_navigator_entry(raw_name)
if readout.startswith("[CL8NK]") or readout.startswith("[CL8NK navigator error"):
print(f"Warning: {readout}")
else:
chunks.append(readout)
if not chunks:
return ""
return "CL8NK Navigator readouts for reference entries:\n\n" + "\n\n".join(chunks)
def _load_context(path: str) -> str:
"""Load file or directory content as a context string for the LLM prompt."""
import os
p = Path(path).expanduser().resolve()
if not p.exists():
raise FileNotFoundError(f"--context path not found: {p}")
chunks: list[str] = []
total = 0
def _read(fp: Path) -> None:
nonlocal total
if total >= _CONTEXT_MAX_BYTES:
return
try:
text = fp.read_text(encoding="utf-8", errors="replace")
except Exception:
return
remaining = _CONTEXT_MAX_BYTES - total
if len(text.encode("utf-8")) > remaining:
text = text.encode("utf-8")[:remaining].decode("utf-8", errors="ignore")
text += f"\n[... truncated at {_CONTEXT_MAX_BYTES} bytes total]"
chunks.append(f"=== {fp.name} ===\n{text}")
total += min(len(text.encode("utf-8")), remaining)
if p.is_file():
_read(p)
else:
files = sorted(
f for f in p.rglob("*")
if f.is_file() and f.suffix.lower() in _CONTEXT_EXTENSIONS
)
for fp in files:
if total >= _CONTEXT_MAX_BYTES:
chunks.append(f"[context truncated — {_CONTEXT_MAX_BYTES} byte limit reached]")
break
_read(fp)
return "\n\n".join(chunks)
def _build_prompt(
description: str,
domain_type: Optional[str],
retry_info: Optional[str] = None,
context: Optional[str] = None,
) -> str:
dt_hint = f"\nDomain type hint: {domain_type}" if domain_type else ""
retry_block = (
f"\n\nPREVIOUS ATTEMPT FAILED — correct these issues:\n{retry_info}"
if retry_info else ""
)
context_block = (
f"<domain-context>\n{context}\n</domain-context>\n\n"
if context else ""
)
catalog_block = _search_catalog(description, context=context)
catalog_section = (
f"<catalog-reference>\n{catalog_block}\n</catalog-reference>\n\n"
if catalog_block else ""
)
# Context leads, catalog trails. A context given explicitly with --context is
# the subject; the catalog is a neighbourhood hint. Putting the hint first put
# 3.4KB of a DIFFERENT solved problem at the head of the prompt, and the model
# took its boundary token from there instead of from the file it was handed.
return f"{context_block}{catalog_section}Design an Ob3ect for:\n\n{description}{dt_hint}\n\n{_SCHEMA}{retry_block}"
def _server_context_limit(provider: Any) -> Optional[int]:
"""What the local server will actually accept, asked of the server.
A 400 that says "request (90010 tokens) exceeds the available context size
(65536 tokens)" carries both numbers, and httpx's one-line `Client error
'400 Bad Request'` throws them away. The remedy is not to read that message
better after the fact — it is to ask before sending.
"""
base = getattr(provider, "base_url", "") or ""
if not getattr(provider, "local_http", False) or "/v1/" not in base:
return None
root = base.split("/v1/")[0]
try:
import httpx as _httpx
r = _httpx.get(f"{root}/props", timeout=10)
r.raise_for_status()
d = r.json()
n = d.get("default_generation_settings", {}).get("n_ctx")
return int(n) if n and int(n) > 0 else None
except Exception:
return None
def _count_tokens(provider: Any, text: str) -> Optional[int]:
"""The server's own tokenizer, not an estimate. Characters per token vary by
a factor of three across Shavian, LaTeX and prose, and this context is all
three at once."""
base = getattr(provider, "base_url", "") or ""
if not getattr(provider, "local_http", False) or "/v1/" not in base:
return None
root = base.split("/v1/")[0]
try:
import httpx as _httpx
r = _httpx.post(f"{root}/tokenize", json={"content": text}, timeout=60)
r.raise_for_status()
tokens = r.json().get("tokens")
return len(tokens) if isinstance(tokens, list) else None
except Exception:
return None
def _fit_context(provider: Any, description: str, domain_type: Optional[str],
retry_info: Optional[str], context: Optional[str],
reserve: int = 8192, required_context: Optional[str] = None) -> Optional[str]:
"""Trim the CONTEXT until the prompt fits the server's window, and say so.
Only the context is cut. The description and the system prompt are the
question, and a question trimmed to fit is a different question. Cutting is
reported in tokens, because a context silently truncated to fit is
indistinguishable, from the output alone, from a context that never arrived.
"""
def prompt(optional):
combined = "\n\n".join(c for c in (required_context, optional) if c) or None
return _SYSTEM_PROMPT + "\n" + _build_prompt(
description, domain_type, retry_info, context=combined)
limit = _server_context_limit(provider)
if not limit:
return context
budget = limit - reserve
full = prompt(context)
have = _count_tokens(provider, full)
if have is None or have <= budget:
return context
# The overhead is everything that is not context, measured rather than assumed.
bare = prompt(None)
overhead = _count_tokens(provider, bare)
if overhead is None:
raise ValueError("Could not measure required prompt after detecting context overflow")
ctx_tokens = have - overhead
room = budget - overhead
if room < 0:
raise ValueError(f"Required prompt is {overhead} tokens against a {budget}-token input budget")
if room == 0 or not context:
return None
keep = max(1, int(len(context) * (room / max(ctx_tokens, 1)) * 0.97))
trimmed = context[:keep]
# Verify against the server rather than trusting the ratio.
for _ in range(6):
check = _count_tokens(provider, prompt(trimmed))
if check is not None and check <= budget:
break
trimmed = trimmed[:int(len(trimmed) * 0.9)]
else:
# Required content fits; optional content can be omitted safely when
# tokenizer counts do not follow the initial proportional estimate.
trimmed = None
print(f" [context {have} tokens exceeds the {limit}-token window; "
f"sent {len(trimmed or ''):,} of {len(context):,} chars]")
return trimmed
def _extract_json(text: str) -> Dict[str, Any]:
# 1. Direct parse
try:
data = json.loads(text.strip())
if not isinstance(data, dict):
raise ValueError("Expected a JSON object")
return data
except json.JSONDecodeError:
pass
# 2. Strip markdown fence
m = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
if m:
data = json.loads(m.group(1))
if not isinstance(data, dict):
raise ValueError("Expected a JSON object")
return data
# 3. Outermost braces
start = text.find("{")
end = text.rfind("}") + 1
if start >= 0 and end > start:
return json.loads(text[start:end])
raise ValueError("No JSON object found in LLM response")
def _make_unique_slug(raw: str, max_base_len: int = 48) -> str:
"""
Slugify `raw` for use as a directory/file name. If the normalized slug is
within max_base_len, it's returned as-is (stable: the same short
description has a stable base name. Output reservation keeps reruns in
separate bundles).
If it would be TRUNCATED, append an 8-hex-char hash of the FULL,
untruncated string. Two different long descriptions that happen to share
the same first 48 normalized characters used to collide on the identical
truncated slug and silently overwrite each other's run — this is exactly
what happened to `the_sixteen_3_lateral_opening_discern_what_runt` (two
unrelated long ob3ect descriptions, same 48-char prefix, second run
clobbered the first). The hash is content-derived, not random, so the
SAME long description still lands on the same directory across runs.
"""
slug = re.sub(r"[^a-z0-9]+", "_", raw.strip().lower()).strip("_")
if not slug:
# Degenerate input (empty, punctuation-only, non-ASCII-only, …). Without a
# hash here, two DIFFERENT degenerate descriptions (e.g. "!!!" and "???")
# would both fall back to the same bare "ob3ect" and collide with each
# other — the same collapse bug one level down. Hash the raw bytes so they
# still land in distinct directories; an empty raw string is the one
# legitimately shared case (nothing to distinguish it by).
if not raw.strip():
return "ob3ect"
return "ob3ect_" + hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
if len(slug) <= max_base_len:
return slug
h = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
return f"{slug[:max_base_len].rstrip('_')}_{h}"
def _reserve_output_directory(parent: Path, slug: str) -> Path:
"""Claim a complete run bundle without replacing a previous run."""
from uuid import uuid4
parent.mkdir(parents=True, exist_ok=True)
candidate = parent / slug
while True:
try:
candidate.mkdir()
return candidate
except FileExistsError: