extends Node
## GATE — the JOURNAL INDEX (2026-10-05: "lets just do those fixes to how history works").
##
## LevelPersistence keeps the journal parsed in memory, updated one key at a time from
## GameState.state_changed, instead of re-reading and re-parsing the whole room on every
## fold, History refresh, undo and record write. GameState stays the source of truth, so
## the index must ALWAYS answer exactly what a from-scratch rebuild answers:
##
## 1. after every step of seeded random sequences — brush creates (in gestures),
## object create/move/prop/delete, undo, redo, seek, a PEER's record, an erase,
## a level-source change, a map_changing drop — records(), gestures(true),
## undo/redo depth and find_source() equal a fresh rebuild's
## 2. the Lamport max seq never goes DOWN (an erase must not let a seq be re-minted)
## 3. COST: an object move parses ONE record (not the journal), and its fold skips
## the brush half; a brush edit's fold applies it
## 4. a gesture's undo/redo goes through the gesture table: one Ctrl+Z takes back all
## of a 40-record gesture
## 5. THE TAIL FOLD (dc_journal::apply_tail): whenever a step's fold took the tail path
## (only new brush records since the last fold), a forced FULL fold leaves the level
## byte-identical (fingerprint); the tail path was actually taken; an undo is not a
## tail
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_journal_index.tscn
var fails := 0
var checks := 0
var _rng := RandomNumberGenerator.new()
func _ok(what: String, cond: bool, note: String = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", note])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
keet.current_role = "admin" # to write a PEER's record and erase
var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
_run(map)
if checks < 13:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE journal_index: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
## Everything the index answers, as comparable strings.
static func snapshot(persist: Node) -> Dictionary:
var recs := PackedStringArray()
for r: Dictionary in (persist.records() as Array):
recs.append(var_to_str(r))
recs.sort()
var gest := PackedStringArray()
for e: Dictionary in (persist.gestures(true) as Array):
var kinds: Array = (e["kinds"] as Dictionary).keys()
kinds.sort()
gest.append("%s|%s|%d|%s|%d|%s" % [e["g"], e["label"], int(e["count"]), str(e["undone"]), int(e["stamp"]), str(kinds)])
return {"records": recs, "gestures": gest, "undo": persist.undo_depth(), "redo": persist.redo_depth(),
"source": var_to_str(persist.find_source()), "live": persist.newest_live_gesture()}
func _diff(a: Dictionary, b: Dictionary) -> String:
var out := PackedStringArray()
for k: String in a:
if a[k] != b[k]:
out.append(k)
return ",".join(out)
func _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
var gs: Node = get_tree().root.get_node("/root/GameState")
_ok("map_test has a journal with an index", persist != null and persist.has_method("index_stats"))
if persist == null or not persist.has_method("index_stats"):
return
# ---- 1 + 2: random sequences vs a rebuild ----
var mismatches: Array = []
var seq_drops := 0
var objs: Array = []
for run in 3:
_rng.seed = 1000 + run
for step in 70:
var op := _rng.randi_range(0, 10)
match op:
0, 1:
persist.begin_gesture("G")
for i in _rng.randi_range(1, 6):
var x := _rng.randf_range(40, 200)
persist.record("brush.create", {"planes": _box(Vector3(x, 0, 0), Vector3(x + 1, 1, 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.end_gesture()
2:
var d: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(_rng.randf(), 1, 0)))
if d != "":
objs.append(d)
3:
if not objs.is_empty():
map.move_object(objs[_rng.randi() % objs.size()], Transform3D(Basis.IDENTITY, Vector3(_rng.randf() * 5, 1, 0)))
4:
if not objs.is_empty():
map.set_object_props(objs[_rng.randi() % objs.size()], {"tint": [_rng.randf(), 0.5, 0.5]})
5:
persist.undo_last()
6:
persist.redo_last()
7:
var g: Array = persist.gestures()
if not g.is_empty():
persist.seek(String(g[_rng.randi() % g.size()]["g"]))
8:
# A PEER's record arriving (another author's key).
gs.set_state("world:level:peer-b:op:%d" % (1791300000 + run * 1000 + step), {
"v": 2, "kind": "brush.create", "undone": false, "g": "peer-b:%d" % step,
"planes": _box(Vector3(300, 0, step), Vector3(301, 1, step + 1)), "proj": [],
"material_id": 1, "level_id": 0, "sky": false})
9:
# An ERASE (a flatten, or a peer's delete arriving).
var keys: Array[String] = gs.keys_with_prefix("world:level:")
if keys.size() > 5:
gs.erase_state(keys[_rng.randi() % keys.size()])
10:
if _rng.randf() < 0.5:
persist.persist_source("%064d" % _rng.randi_range(1, 3), "gate.map", 1)
else:
gs.map_changing.emit("map_test") # the mirror-drop signal: index stale
persist.refold()
var max_before: int = persist._max_seq_seen()
var inc := snapshot(persist)
persist.set("_ix_ready", false) # force a from-scratch rebuild
var fresh := snapshot(persist)
if inc != fresh:
mismatches.append("run %d step %d op %d: %s" % [run, step, op, _diff(inc, fresh)])
if persist._max_seq_seen() < max_before and op != 10:
seq_drops += 1
_ok("1. incremental index == rebuild after 210 random steps", mismatches.is_empty(),
"%d mismatch(es) %s" % [mismatches.size(), str(mismatches.slice(0, 3))])
var st0: Dictionary = persist.index_stats()
_ok("1. it exercised a real journal", int(st0["records"]) > 100, "%d records" % int(st0["records"]))
_ok("2. the max seq never went down across erases", seq_drops == 0, "%d drop(s)" % seq_drops)
# ---- 3: cost ----
persist.refold()
var od: String = map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(0, 1, 0)))
persist.refold()
var s1: Dictionary = persist.index_stats()
map.move_object(od, Transform3D(Basis.IDENTITY, Vector3(1, 1, 0)))
var f1: Dictionary = persist.last_fold()
var s2: Dictionary = persist.index_stats()
_ok("3. an object move parses ONE record", int(s2["parses"]) - int(s1["parses"]) == 1
and int(s2["rebuilds"]) == int(s1["rebuilds"]), "%d parses, %d rebuilds of %d records" % [
int(s2["parses"]) - int(s1["parses"]), int(s2["rebuilds"]) - int(s1["rebuilds"]), int(s2["records"])])
_ok("3. and its fold skips the brush half", String(f1.get("brush_fold", "")) == "skipped", str(f1.get("brush_fold", "")))
persist.record("brush.create", {"planes": _box(Vector3(500, 0, 0), Vector3(501, 1, 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.refold()
_ok("3. a brush edit's fold applies the brush half", String(persist.last_fold().get("brush_fold", "")) in ["applied", "tail"],
persist.last_fold().get("brush_fold", ""))
# ---- 5: the TAIL FOLD over a seeded sequence (no index rebuilds: one would rightly
# disable it). Creates and deletes append (tail); a move pre-applies its edit
# (fingerprint changed -> full); an undo changes a record (full).
_rng.seed = 4242
var tails := 0
var tail_bad: Array = []
var live_keys: Array = []
persist.refold()
for step in 80:
var op := _rng.randi_range(0, 9)
if op <= 4 or live_keys.is_empty():
var x := _rng.randf_range(100, 160)
live_keys.append(persist.record("brush.create", {"planes": _box(Vector3(x, 0, 0), Vector3(x + 1, 1, 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false}))
persist.refold()
elif op <= 6:
var k: String = live_keys.pop_at(_rng.randi() % live_keys.size())
map.delete_brushes([map.level.brush_by_key(k)])
elif op == 7:
map.select_brushes(PackedInt32Array([map.level.brush_by_key(live_keys[_rng.randi() % live_keys.size()])]))
map.translate_selection(Vector3(0, 0, 0.5))
persist.refold()
else:
persist.undo_last()
persist.refold()
if String(persist.last_fold().get("brush_fold", "")) == "tail":
tails += 1
var fp_tail: int = map.level.fingerprint()
persist.set("_ix_tail_ok", false)
persist.set("_brush_ver_folded", -1)
persist.refold() # forced FULL fold
if int(map.level.fingerprint()) != fp_tail:
tail_bad.append("step %d op %d" % [step, op])
_ok("5. every tail fold == a forced full fold (fingerprint)", tail_bad.is_empty(),
"%d bad %s" % [tail_bad.size(), str(tail_bad.slice(0, 3))])
_ok("5. the tail path was taken", tails >= 20, "%d of 80 steps" % tails)
# ---- 4: a big gesture through the table ----
var live0: int = (map.level.get_live_brush_ids() as PackedInt32Array).size()
persist.begin_gesture("Big")
for i in 40:
persist.record("brush.create", {"planes": _box(Vector3(600 + i * 2, 0, 0), Vector3(601 + i * 2, 1, 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.end_gesture()
persist.refold()
var g0: Dictionary = (persist.gestures() as Array)[0]
_ok("4. a 40-record gesture is one History row", String(g0["label"]) == "Big" and int(g0["count"]) == 40, str(g0))
_ok("4. one Ctrl+Z takes all 40 back", persist.undo_last() != "" and _refolded_live(map, persist) == live0)
_ok("5. an undo is NOT a tail (it changes records)", String(persist.last_fold().get("brush_fold", "")) == "applied",
persist.last_fold().get("brush_fold", ""))
_ok("4. one redo brings all 40 back", persist.redo_last() != "" and _refolded_live(map, persist) == live0 + 40)
func _refolded_live(map: Node3D, persist: Node) -> int:
persist.refold()
return (map.level.get_live_brush_ids() as PackedInt32Array).size()