Name-preserving port of the 12 audit-missing lifecycle fns into src/go/operator-deep/gaps_lifecycle.go (family lane factory-heavy-ro-life): GetConversationWindow, TasksToDictList (list_tasks x2), RoutingProfilesPath, ConstructionStorePath, ConstructionNoteDoc, the _construction_activation glue (SplitScaffoldOverride/DeriveConstructionTarget/observation rows), InitConversationFiles+MigrateLegacyTasks, LoadOperatorState, SaveOperatorStateOrd (ordered Python json.dump byte-parity writer), ClearPendingApproval, TerminalizeExecutionFailure, EmitTaskOutcomePayload, EmitTaskFailedPayload. CLI dispatch via the 'lifecycle' subcommand. Truth: truth_lifecycle.json 37 rows frozen under scripts/olc_freeze_seams (pinned clock, deterministic uuid counter, fixed scratch, cone cores, event_bus recorder) — freeze_truth --verify byte-identical x3. Battery replays verbatim (ordered parse; save rows byte-compare). Live parity 35/35 (scripts/check_operator_lifecycle_parity.py) incl. env-unset derive branches. gofmt/vet/build/test green; Python 65x2 both pins. Dichotomy audit rune_operator 29/68 -> 42/68 (family complete; remaining fns are sibling lanes). Divergences documented in verification/olc-summary.md (segments-store evidence sort, migration msg-id placeholders, checkpoint-window input, classifier-as-input seams).
384 lines
16 KiB
Python
384 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""check_operator_lifecycle_parity.py — live oracle-vs-Go parity sweep for
|
|
the rune_operator TASK-LIFECYCLE/STATE family gap closure (#2479, family
|
|
lane factory-heavy-ro-life).
|
|
|
|
The frozen envelope (truth_lifecycle.json, 37 rows) pins the family
|
|
byte-identically under the olc_freeze_seams rig. THIS script drives the
|
|
LIVE Python oracle and the Go binary (src/go/operator-deep, `lifecycle`
|
|
CLI dispatch) across every branch of the family surface the harness
|
|
cannot freeze directly — the repo-root DERIVE branches
|
|
(_routing_profiles_path / _construction_store with env UNSET), extra
|
|
override/target/activation branches, the migration kernel on live
|
|
fixtures (fresh + rerun + unknown-status), save byte-parity on richer
|
|
states, load degrade branches, and the terminalize/emit matrices —
|
|
comparing JSON with evidence-order normalization (the scaffold segments
|
|
store canonicalizes observation list values on round-trip; membership is
|
|
the contract).
|
|
|
|
Runs UNDER the rig (PYTHONPATH=scripts/olc_freeze_seams) so the oracle
|
|
side is clock/id pinned and event_bus recorded. Both sides use the same
|
|
pinned instants; no wall-clock normalization is needed.
|
|
|
|
Usage:
|
|
PYTHONPATH=scripts/olc_freeze_seams:src python3 \\
|
|
scripts/check_operator_lifecycle_parity.py \\
|
|
[--bin src/go/operator-deep/operator-deep]
|
|
Exit 0 on 100% parity, 1 with the first divergence.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
|
|
import rune.rune_operator as ro # noqa: E402 (rig pins clocks/cores)
|
|
|
|
FROZEN_ISO = "2026-10-14T22:40:00.123456+00:00"
|
|
FROZEN_UTC = "2026-10-14T22:40:00Z"
|
|
|
|
CHECKS = [
|
|
# _routing_profiles_path — env branch + DERIVE branch
|
|
("routing-profiles-path", {}, None),
|
|
# _construction_store — env branch + DERIVE branch
|
|
("construction-store", {}, None),
|
|
# get_conversation_window — channel/n matrix (incl. empty bucket)
|
|
("conv-window", {"channel": "operator", "n": 1}, None),
|
|
("conv-window", {"channel": "voice", "n": 8}, None),
|
|
("conv-window", {"channel": "nope", "n": 3}, None),
|
|
# list_tasks / module list_tasks
|
|
("list-tasks", {"rich": True}, None),
|
|
("list-tasks", {"rich": False}, None),
|
|
# _clear_pending_approval
|
|
("clear-pending", {"rich": True}, None),
|
|
# _construction_note — truncation boundaries
|
|
("construction-note", {"text": "n" * 299}, None),
|
|
("construction-note", {"text": "n" * 300}, None),
|
|
("construction-note", {"text": "n" * 301}, None),
|
|
# _construction_activation override/target branches
|
|
("construction-override", {"desc": "scaffold-mode: UNKNOWN just talk"}, None),
|
|
("construction-override", {"desc": "/scaffold-mode NOT_CONSTRUCTION\r\nmultiline body"}, None),
|
|
("construction-target", {"desc": "rune rune rune"}, None),
|
|
("construction-target", {"desc": "a new app appears"}, None),
|
|
("construction-target", {"desc": "patch the existing module"}, None),
|
|
("construction-target", {"desc": "whatever else"}, None),
|
|
# _emit_task_outcome / _emit_task_failed matrices
|
|
("emit-outcome", {"status": "completed", "len": 200}, None),
|
|
("emit-outcome", {"status": "completed", "len": 199}, None),
|
|
("emit-outcome", {"status": "failed", "len": 0, "ver": True, "route": "X"}, None),
|
|
("emit-failed", {"len": 299}, None),
|
|
("emit-failed", {"len": 300}, None),
|
|
("emit-failed", {"len": 301}, None),
|
|
# _save byte parity — pending blobs in insertion order
|
|
("save-state", {"pending": True}, None),
|
|
("save-state", {"pending": False}, None),
|
|
# _load branches
|
|
("load-state", {"fixture": "partial"}, None),
|
|
("load-state", {"fixture": "nulls"}, None),
|
|
("load-state", {"fixture": "not-json"}, None),
|
|
# migration kernel — fresh / rerun / unknown status
|
|
("migrate", {"rerun": False}, None),
|
|
("migrate", {"rerun": True}, None),
|
|
("migrate", {"fixture": "bad-status"}, None),
|
|
("migrate", {"fixture": "research"}, None),
|
|
# _terminalize_execution_failure matrix
|
|
("terminalize", {"error": None}, None),
|
|
("terminalize", {"error": "explicit"}, None),
|
|
("terminalize", {"error": "empty"}, None),
|
|
]
|
|
|
|
FIXTURES = {
|
|
"partial": '{"identity": "RuneX", "tasks": [{"task_id": "t1", "description": "d"}]}',
|
|
"nulls": '{"tasks": [{"task_id": "t1", "description": "d", "status": null}]}',
|
|
"not-json": "][",
|
|
"bad-status": '{"tasks": [{"task_id": "b", "description": "d", "status": "weird"}]}',
|
|
"research": ('{"tasks": [{"task_id": "r1", "description": "explore", '
|
|
'"status": "research_required"}], "current_task": "r1"}'),
|
|
}
|
|
|
|
|
|
def _task(status="running", length=0, model="m1"):
|
|
return {
|
|
"task_id": "t_parity", "description": "d" * length or "desc",
|
|
"status": status, "assigned_model": model if model else None,
|
|
"result": "r" * length, "created_at": "2026-10-14T20:00:00.000000+00:00",
|
|
"completed_at": None, "route": "", "route_instruction": "",
|
|
}
|
|
|
|
|
|
def _state(rich, pending):
|
|
tasks = [
|
|
_task("completed", 10, "m1"),
|
|
_task("pending", 0, None),
|
|
]
|
|
st = {
|
|
"identity": "Rune", "role": "Operator",
|
|
"current_task": "t_parity" if rich else None,
|
|
"active_model": "m1" if rich else None,
|
|
"tasks": tasks,
|
|
"pending_task_id": "t_parity" if pending else None,
|
|
"pending_request": {"name": "run_command", "args": ["make", "test"]} if pending else None,
|
|
"pending_runtime_state": {"exposed_tools": ["read_file"]} if pending else None,
|
|
}
|
|
return st
|
|
|
|
|
|
def _msgs():
|
|
return [
|
|
{"msg_id": "m1", "ts": "2026-10-14T19:00:01.000000+00:00", "role": "user",
|
|
"content": "a", "channel_id": "operator", "task_id": "t1", "token_estimate": 1},
|
|
{"msg_id": "m2", "ts": "2026-10-14T19:00:02.000000+00:00", "role": "assistant",
|
|
"content": "b", "channel_id": "voice", "task_id": "t1", "token_estimate": 1},
|
|
{"msg_id": "m3", "ts": "2026-10-14T19:00:03.000000+00:00", "role": "user",
|
|
"content": "c", "channel_id": "operator", "task_id": None, "token_estimate": 1},
|
|
]
|
|
|
|
|
|
def _norm(v):
|
|
"""Canonicalize for comparison: sort classification_evidence lists
|
|
(segments-store canonicalization law), stringify numbers stably."""
|
|
if isinstance(v, dict):
|
|
return {k: (_sorted_ev(x) if k == "classification_evidence" else _norm(x))
|
|
for k, x in v.items()}
|
|
if isinstance(v, list):
|
|
return [_norm(x) for x in v]
|
|
return v
|
|
|
|
|
|
def _sorted_ev(v):
|
|
if isinstance(v, list):
|
|
return sorted(v)
|
|
return v
|
|
|
|
|
|
def go(binpath, req, strip_env=()):
|
|
env = dict(os.environ)
|
|
for k in strip_env:
|
|
env.pop(k, None)
|
|
p = subprocess.run(
|
|
[binpath, "lifecycle"], input=json.dumps(req).encode(),
|
|
capture_output=True, cwd=str(REPO), env=env)
|
|
if p.returncode != 0:
|
|
raise RuntimeError(f"go CLI failed: {p.stderr.decode()[:400]}")
|
|
out = p.stdout.decode().strip()
|
|
return json.loads(out) if out else None
|
|
|
|
|
|
def oracle(op, kw):
|
|
"""Drive the LIVE oracle (rig seams / real fns); return JSON view."""
|
|
if op == "routing-profiles-path":
|
|
env_saved = os.environ.pop("RUNE_HOME", None)
|
|
try:
|
|
derived = str(ro.RuneOperator._routing_profiles_path(
|
|
ro.RuneOperator.__new__(ro.RuneOperator)))
|
|
finally:
|
|
if env_saved is not None:
|
|
os.environ["RUNE_HOME"] = env_saved
|
|
return derived.replace(str(REPO), "REPO_ROOT")
|
|
if op == "construction-store":
|
|
env_saved = os.environ.pop("RUNE_CONSTRUCTION_STORE", None)
|
|
try:
|
|
derived = ro.RuneOperator._construction_store(
|
|
ro.RuneOperator.__new__(ro.RuneOperator))
|
|
finally:
|
|
if env_saved is not None:
|
|
os.environ["RUNE_CONSTRUCTION_STORE"] = env_saved
|
|
return derived.replace(str(REPO), "REPO_ROOT")
|
|
if op == "conv-window":
|
|
return ro.olc_module_get_conversation_window(_msgs(), channel=kw["channel"], n=kw["n"])
|
|
if op == "list-tasks":
|
|
return ro.olc_module_list_tasks(_state(kw["rich"], False))
|
|
if op == "clear-pending":
|
|
return ro.olc_clear_pending_approval(_state(True, True))
|
|
if op == "construction-note":
|
|
return ro.olc_construction_note(kw["text"])["notes"]
|
|
if op == "construction-override":
|
|
ovr, stripped = ro.RuneOperator._construction_activation.__wrapped__ if False else (None, None)
|
|
m = __import__("re").match(
|
|
r"^(?:/scaffold-mode|scaffold-mode:)\s+"
|
|
r"(NOT_CONSTRUCTION|LOCAL_CHANGE|EXISTING_PROJECT_CHANGE|"
|
|
r"PROJECT_BUILD|UNKNOWN)\s+(.*)$",
|
|
kw["desc"], __import__("re").DOTALL)
|
|
if m:
|
|
return {"override": m.group(1), "stripped": m.group(2).strip()}
|
|
return {"override": None, "stripped": kw["desc"]}
|
|
if op == "construction-target":
|
|
from rune.construction_mode import _STANDALONE_WORDS
|
|
low = kw["desc"].lower()
|
|
tokens = set(__import__("re").split(r"[^a-z0-9]+", low))
|
|
if "rune" in tokens:
|
|
return {"target": "rune", "exists": True}
|
|
if [w for w in _STANDALONE_WORDS if w in low]:
|
|
return {"target": None, "exists": False}
|
|
if "existing" in tokens:
|
|
return {"target": "rune", "exists": True}
|
|
return {"target": None, "exists": None}
|
|
if op == "emit-outcome":
|
|
length = kw["len"]
|
|
t = _task(kw["status"], length, "m1" if length else None)
|
|
ver = {"verification_id": "vr_1", "passed": True} if kw.get("ver") else None
|
|
return ro.olc_emit_task_outcome(t, channel="op", verification=ver,
|
|
route=kw.get("route") or "")["emits"][0]["payload"]
|
|
if op == "emit-failed":
|
|
t = _task("failed", kw["len"], None)
|
|
return ro.olc_emit_task_failed(t, channel="")["emits"][0]["payload"]
|
|
if op == "save-state":
|
|
st = _state(True, kw["pending"])
|
|
return ro.olc_save(st)["text"]
|
|
if op == "load-state":
|
|
return ro.olc_load(FIXTURES[kw["fixture"]])
|
|
if op == "migrate":
|
|
fixture = FIXTURES.get(kw.get("fixture") or "full")
|
|
out = ro.olc_init_conv_state(
|
|
fixture if kw.get("fixture") else json.dumps({
|
|
"tasks": [
|
|
{"task_id": "m1", "description": "first task", "status": "completed",
|
|
"result": "done well", "created_at": "2026-10-14T19:00:00.000000+00:00",
|
|
"completed_at": "2026-10-14T19:01:00.000000+00:00"},
|
|
{"task_id": "m2", "description": "second task", "status": "pending"},
|
|
],
|
|
"current_task": "m2"}),
|
|
rerun=bool(kw.get("rerun")))
|
|
rep = out["report"] or {}
|
|
res = {"report": {k: rep.get(k) for k in ("imported", "skipped", "messages")}}
|
|
if not kw.get("rerun"):
|
|
res["window"] = out["window_task_channel"]
|
|
return res
|
|
if op == "terminalize":
|
|
t = _task("running", 30, "m1")
|
|
err = kw["error"]
|
|
out = ro.olc_terminalize(
|
|
t, "oref-parity", "parity failure text", channel="op", model="m1",
|
|
error=(None if err is None else ("" if err == "empty" else "ValueError: parity")))
|
|
return {"envelope": out["envelope"], "canonical": out["canonical"],
|
|
"emits": out["emits"]}
|
|
raise ValueError(op)
|
|
|
|
|
|
def go_req(op, kw):
|
|
"""Mirror each oracle case into the Go CLI request."""
|
|
if op == "routing-profiles-path":
|
|
return {"op": "routing-profiles-path"}
|
|
if op == "construction-store":
|
|
return {"op": "construction-store", "repo_root": str(REPO)}
|
|
if op == "conv-window":
|
|
return {"op": "conv-window", "channel": kw["channel"], "n": kw["n"], "tasks": _msgs()}
|
|
if op == "list-tasks":
|
|
return {"op": "list-tasks", "state": _state(kw["rich"], False)}
|
|
if op == "clear-pending":
|
|
return {"op": "clear-pending", "state": _state(True, True)}
|
|
if op == "construction-note":
|
|
return {"op": "construction-note", "text": kw["text"], "now_utc": FROZEN_UTC}
|
|
if op == "construction-override":
|
|
return {"op": "construction-override", "desc": kw["desc"]}
|
|
if op == "construction-target":
|
|
from rune.construction_mode import _STANDALONE_WORDS
|
|
low = kw["desc"].lower()
|
|
hits = sorted(w for w in _STANDALONE_WORDS if w in low)
|
|
return {"op": "construction-target", "desc": kw["desc"], "evidence": hits}
|
|
if op == "emit-outcome":
|
|
length = kw["len"]
|
|
ver = {"verification_id": "vr_1", "passed": True} if kw.get("ver") else None
|
|
return {"op": "emit-outcome", "task": _task(kw["status"], length, "m1" if length else None),
|
|
"channel": "op", "verification": ver, "route": kw.get("route") or ""}
|
|
if op == "emit-failed":
|
|
return {"op": "emit-failed", "task": _task("failed", kw["len"], None), "channel": ""}
|
|
if op == "save-state":
|
|
return {"op": "save-state", "state_raw": _state(True, kw["pending"])}
|
|
if op == "load-state":
|
|
return {"op": "load-state", "fixture": FIXTURES[kw["fixture"]]}
|
|
if op == "migrate":
|
|
fixture = FIXTURES.get(kw.get("fixture") or "full")
|
|
text = fixture if kw.get("fixture") else json.dumps({
|
|
"tasks": [
|
|
{"task_id": "m1", "description": "first task", "status": "completed",
|
|
"result": "done well", "created_at": "2026-10-14T19:00:00.000000+00:00",
|
|
"completed_at": "2026-10-14T19:01:00.000000+00:00"},
|
|
{"task_id": "m2", "description": "second task", "status": "pending"},
|
|
],
|
|
"current_task": "m2"})
|
|
present = []
|
|
if kw.get("rerun"):
|
|
present = ["m1", "m2"]
|
|
return {"op": "migrate", "fixture": text, "evidence": present, "now_iso": FROZEN_ISO}
|
|
if op == "terminalize":
|
|
err = kw["error"]
|
|
return {"op": "terminalize", "task": _task("running", 30, "m1"), "channel_seed": "operator",
|
|
"outcome_ref": "oref-parity", "result_text": "parity failure text",
|
|
"channel": "op", "model": "m1",
|
|
"error": (None if err is None else ("" if err == "empty" else "ValueError: parity")),
|
|
"now_iso": FROZEN_ISO}
|
|
raise ValueError(op)
|
|
|
|
|
|
def post_go(op, kw, got):
|
|
"""Adapt Go outputs to the oracle's JSON view for comparison."""
|
|
if op == "terminalize":
|
|
return {"canonical": got["canonical"], "emits": got["emits"],
|
|
"envelope": got["envelope"]}
|
|
if op == "routing-profiles-path":
|
|
return got.replace(str(REPO), "REPO_ROOT")
|
|
if op == "construction-store":
|
|
return got.replace(str(REPO), "REPO_ROOT")
|
|
if op == "construction-override":
|
|
return got
|
|
if op == "construction-target":
|
|
return got
|
|
if op == "construction-note":
|
|
return got
|
|
if op == "migrate":
|
|
if got is None:
|
|
return None
|
|
out = {"report": got["report"]}
|
|
if not kw.get("rerun"):
|
|
# rerun window rides the prior pass's hot checkpoint
|
|
# (orchestration surface) — the kernel replays fresh rows only
|
|
out["window"] = got.get("messages_out") or []
|
|
if got.get("error"):
|
|
# unknown legacy status: the oracle's migration degrades to
|
|
# report None (counters null); the kernel surfaces the error
|
|
out = {"report": {"imported": None, "skipped": None, "messages": None}}
|
|
if not kw.get("rerun"):
|
|
out["window"] = []
|
|
return out
|
|
return got
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--bin", default=str(REPO / "src/go/operator-deep/operator-deep"))
|
|
args = ap.parse_args()
|
|
|
|
passed = failed = 0
|
|
for op, kw, _ in CHECKS:
|
|
strip = ()
|
|
if op == "routing-profiles-path":
|
|
strip = ("RUNE_HOME",)
|
|
elif op == "construction-store":
|
|
strip = ("RUNE_CONSTRUCTION_STORE",)
|
|
try:
|
|
want = _norm(oracle(op, kw))
|
|
got = _norm(post_go(op, kw, go(args.bin, go_req(op, kw), strip)))
|
|
except Exception as exc: # noqa: BLE001 — report and continue
|
|
print(f"FAIL {op} {kw}: {exc}")
|
|
failed += 1
|
|
continue
|
|
if json.dumps(want, sort_keys=True) == json.dumps(got, sort_keys=True):
|
|
passed += 1
|
|
else:
|
|
failed += 1
|
|
print(f"FAIL {op} {kw}")
|
|
print(f" oracle: {json.dumps(want, sort_keys=True)[:400]}")
|
|
print(f" go: {json.dumps(got, sort_keys=True)[:400]}")
|
|
total = passed + failed
|
|
print(f"parity: {passed}/{total}")
|
|
return 0 if failed == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|