feat(time_awareness): port to FULL Rust — dichotomy wave

This commit is contained in:
Nick committed 2026-10-12 02:00:44 +01:00
1 parent 36ac3de022
commit 92107319d2
10 files changed
+3769 -92

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""Live parity gate for the time_awareness dichotomy port (#2479 tranche).
Three legs, every observation compared EXACTLY (the clock is pinned, so
nothing is masked):
1. frozen truth replay on the pinned-python path (truth self-consistency)
2. frozen truth replay on the kernel path (RUNE_TA_CORE unset)
3. seeded fuzz (seed 2479): random scenarios + random temporal-reference
texts, each driven through the live facade twice — pinned python vs
kernel — results, state and file must match bit-for-bit.
Exit 0 only on 100% parity.
"""
import os
import random
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.dirname(_HERE)
sys.path.insert(0, _HERE)
sys.path.insert(0, os.path.join(_ROOT, "src"))
import gen_time_awareness_truth as gen # noqa: E402 — shared clock-pinned harness
from rune import ta_loader # noqa: E402
TRUTH_PATH = os.path.join(_ROOT, "src", "rust", "time_awareness", "src", "frozen_ta_truth.json")
def drive_pin(scenarios, pin, workdir):
os.environ["RUNE_TA_CORE"] = pin
driver = gen.Driver(workdir)
return [driver.run(s["ops"]) for s in scenarios]
def replay_truth(workdir):
import json
with open(TRUTH_PATH) as f:
truth = json.load(f)
scenarios = truth["scenarios"]
total = sum(len(s["obs"]) for s in scenarios)
py = drive_pin(scenarios, "python", os.path.join(workdir, "py"))
rs = drive_pin(scenarios, "auto", os.path.join(workdir, "rs"))
ok = 0
for s, a, b in zip(scenarios, py, rs):
for i, (x, y) in enumerate(zip(a, b)):
if x == y:
ok += 1
else:
print(f"DIVERGE {s['name']} op#{i} ({s['ops'][i].get('op')})")
print(" py:", x)
print(" rs:", y)
return ok, total
def fuzz_texts(rng, n):
words = ["yesterday", "today", "tomorrow", "last", "next", "this", "morning",
"afternoon", "tonight", "week", "month", "yesterdays", "mondays",
"café", "lastweek", "soon", "later", "now", "the", "a"]
seps = [" ", " ", "\t", "\n", ", ", ". "]
out = []
for _ in range(n):
k = rng.randint(1, 6)
out.append(rng.choice(seps).join(rng.choice(words) for _ in range(k)))
return out
def fuzz_iso(rng, valid_only=False):
import datetime as _dt
r = rng.random() * (0.8 if valid_only else 1.0)
if r < 0.55:
dt = gen.EPOCH + _dt.timedelta(microseconds=rng.randint(0, gen.BASE_US))
dt += _dt.timedelta(days=rng.randint(-500, 500))
off = rng.choice([None, 0, 3600, -18000])
if off is not None:
dt = dt.replace(tzinfo=_dt.timezone(_dt.timedelta(seconds=off)))
return dt.isoformat()
if r < 0.7:
dt = gen.EPOCH + _dt.timedelta(microseconds=rng.randint(0, gen.BASE_US))
return dt.replace(tzinfo=None).isoformat() # naive → TypeError branch
if r < 0.8:
return "2026-10-10T10:00:00Z"
return rng.choice(["garbage", "", "2026-13-40T99:00:00+00:00", "[1,2]"])
def fuzz_scenarios(rng, n, texts):
scenarios = []
for i in range(n):
ops = [{"op": "new", "mono": round(rng.uniform(0.0, 400.0), 3)}]
if rng.random() < 0.5:
ops[0]["seed_file"] = gen.json.dumps(
{"last_interaction": fuzz_iso(rng)}, indent=2
)
elif rng.random() < 0.1:
ops[0]["seed_file"] = rng.choice(["corrupt{", "[1,2]"])
for _ in range(rng.randint(1, 8)):
kind = rng.choice(["new", "new", "seed_last", "ctx", "ctx", "status",
"parse", "flush", "mark_dirty", "save_if_dirty"])
op = {"op": kind, "mono": round(rng.uniform(0.0, 400.0), 3)}
if kind == "new":
if rng.random() < 0.5:
op["seed_file"] = gen.json.dumps(
{"last_interaction": fuzz_iso(rng)}, indent=2
)
elif rng.random() < 0.1:
op["seed_file"] = rng.choice(["corrupt{", "[1,2]"])
if kind == "seed_last":
op["last_interaction"] = fuzz_iso(rng, valid_only=True) if rng.random() < 0.8 else None
if kind in ("ctx", "status", "parse"):
op["now_us"] = gen.BASE_US + rng.randint(-3000, 3000) * 3600 * 1_000_000
if kind == "ctx":
op["update"] = rng.random() < 0.6
if kind == "parse":
op["text"] = rng.choice(texts)
op["now_us"] = gen.BASE_US + rng.randint(-3000, 3000) * 3600 * 1_000_000
ops.append(op)
scenarios.append({"name": f"fuzz_{i}", "ops": ops})
return scenarios
def main():
import shutil
work = "/tmp/ta_parity_work"
shutil.rmtree(work, ignore_errors=True)
ok, total = replay_truth(work)
print(f"truth replay parity: {ok}/{total}")
rng = random.Random(2479)
texts = fuzz_texts(rng, 300)
scenarios = fuzz_scenarios(rng, 200, texts)
fz_total = sum(len(s["ops"]) for s in scenarios)
py = drive_pin(scenarios, "python", os.path.join(work, "fpy"))
rs = drive_pin(scenarios, "auto", os.path.join(work, "frs"))
fz_ok = 0
for s, a, b in zip(scenarios, py, rs):
for i, (x, y) in enumerate(zip(a, b)):
if x == y:
fz_ok += 1
else:
print(f"DIVERGE {s['name']} op#{i} ({s['ops'][i].get('op')})")
print(" py:", x)
print(" rs:", y)
print(f"fuzz parity (seed 2479): {fz_ok}/{fz_total}")
if ok == total and fz_ok == fz_total:
print(f"PARITY OK — {ok + fz_ok}/{total + fz_total} observations, 100%")
return 0
print("PARITY FAILED")
return 1
if __name__ == "__main__":
raise SystemExit(main())
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python3
"""Freeze the time_awareness truth corpus (dichotomy wave, #2479 tranche).
Drives the LIVE python oracle (src/rune/time_awareness.py) with the clock
pinned (fake datetime.now + fake monotonic — RUNE_TA_CORE=python is pinned
at the top, poisoned-truth law) and freezes every observation into
src/rust/time_awareness/src/frozen_ta_truth.json.
Coverage: ALL public functions and every branch —
* get_context: 14-point time_of_day × circadian grid (weekend + weekday),
update_interaction True/False, urgency branches (>24h, ==24h, >4h,
==4h, 0.1h..4h, ==0.1h, <0.1h, zero, future), save-debounce fire/no-fire
* parse_temporal_reference: all 10 patterns, priority order, word-boundary
positives/negatives, case, punctuation, unicode, whitespace runs
* get_status (read-only), flush, _mark_dirty, _save_if_dirty
* _load: valid aware/no-fraction/Z/offset/naive ISO, null, missing key,
corrupt JSON, non-dict JSON, invalid ISO (blanket except → None)
* module singleton fns (get_time_awareness/get_context/get_status/
parse_temporal_reference) + main() CLI — facade-level, live parity
Timestamps need NO masking: the clock is pinned, so every field
(current_time, dates, hours_since, last_interaction, last_save_time) is
deterministic and frozen verbatim.
"""
import json
import os
import shutil
import sys
from datetime import datetime, timedelta, timezone
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.dirname(_HERE)
sys.path.insert(0, os.path.join(_ROOT, "src"))
os.environ["RUNE_TA_CORE"] = "python" # poisoned-truth law: pin the fallback
os.environ["RUNE_MEMORY_DIR"] = "/tmp/ta_truth_memory"
import rune.time_awareness as ta # noqa: E402
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
class _FakeNow:
"""Stands in for the module's `datetime` name — .now(tz) is pinned;
everything else delegates to the real class (fromisoformat in _load)."""
fixed = None
@staticmethod
def now(tz=None):
return _FakeNow.fixed
@staticmethod
def fromisoformat(s):
return datetime.fromisoformat(s)
class _FakeTime:
mono = 0.0
@staticmethod
def monotonic():
return _FakeTime.mono
ta.datetime = _FakeNow
ta._time = _FakeTime
def us_of(dt):
return (dt - EPOCH) // timedelta(microseconds=1)
def dt_of(us, micro=None):
dt = EPOCH + timedelta(microseconds=us)
return dt
def set_clock(op):
us = op.get("now_us")
_FakeNow.fixed = dt_of(us) if us is not None else _FakeNow.fixed
if "mono" in op:
_FakeTime.mono = op["mono"]
def iso_of(us):
return dt_of(us).isoformat()
BASE_US = us_of(datetime(2026, 10, 11, 12, 0, 0, 123456, tzinfo=timezone.utc))
HOUR_US = 3600 * 1_000_000
MEM_FILE = os.path.join("/tmp/ta_truth_memory", "time_awareness.json")
def state_obs(instance):
return {
"last_interaction": instance.last_interaction.isoformat()
if instance.last_interaction
else None,
"dirty": instance._dirty,
"last_save_time": instance._last_save_time,
"file_exists": os.path.exists(instance.storage_path),
}
def file_obs(path):
if not os.path.exists(path):
return None
with open(path) as f:
raw = f.read()
try:
return json.loads(raw)
except ValueError: # corrupt content — deterministic sentinel, both pins
return {"__unparseable__": raw}
class Driver:
def __init__(self, workdir):
self.workdir = workdir
shutil.rmtree(workdir, ignore_errors=True)
os.makedirs(workdir, exist_ok=True)
self.instance = None
self.n = 0
def step(self, op):
kind = op["op"]
set_clock(op)
if kind == "new":
self.n += 1
path = os.path.join(self.workdir, f"sto_{self.n}.json")
seed = op.get("seed_file")
if seed is None:
if os.path.exists(path):
os.remove(path)
else:
with open(path, "w") as f:
f.write(seed)
self.instance = ta.TimeAwareness(storage_path=path)
return {"result": None, "state": state_obs(self.instance)}
if kind == "seed_last":
li = op.get("last_interaction")
self.instance.last_interaction = (
datetime.fromisoformat(li) if li is not None else None
)
return {"result": None, "state": state_obs(self.instance)}
if kind == "ctx":
update = op.get("update", True)
prev_ls = self.instance._last_save_time
try:
r = self.instance.get_context(update_interaction=update).to_dict()
except Exception as e: # noqa: BLE001 — the oracle raising IS the truth
return {
"result": {"error": type(e).__name__},
"state": state_obs(self.instance),
}
ob = {"result": r, "state": state_obs(self.instance)}
# exact _save_if_dirty decision: fired → the file obs carries it
if update and (op["mono"] - prev_ls >= ta.TimeAwareness._SAVE_DEBOUNCE_SECS):
ob["file"] = file_obs(self.instance.storage_path)
return ob
if kind == "status":
try:
r = self.instance.get_status()
except Exception as e: # noqa: BLE001 — naive last_interaction raises; that IS the truth
r = {"error": type(e).__name__}
return {
"result": r,
"state": state_obs(self.instance),
}
if kind == "parse":
return {
"result": self.instance.parse_temporal_reference(op["text"]),
"state": state_obs(self.instance),
}
if kind == "flush":
self.instance.flush()
return {
"result": None,
"state": state_obs(self.instance),
"file": file_obs(self.instance.storage_path),
}
if kind == "mark_dirty":
self.instance._mark_dirty()
return {"result": None, "state": state_obs(self.instance)}
if kind == "save_if_dirty":
self.instance._save_if_dirty()
return {
"result": None,
"state": state_obs(self.instance),
"file": file_obs(self.instance.storage_path),
}
if kind == "mod_ctx" or kind == "mod_status" or kind == "mod_parse":
seed = op.get("seed_file")
if seed is None:
if os.path.exists(MEM_FILE):
os.remove(MEM_FILE)
else:
with open(MEM_FILE, "w") as f:
f.write(seed)
ta._time_awareness = None
if kind == "mod_ctx":
r = ta.get_context().to_dict()
elif kind == "mod_status":
r = ta.get_status()
else:
r = ta.parse_temporal_reference(op["text"])
inst = ta._time_awareness
ob = {"result": r, "state": state_obs(inst)}
if kind == "mod_ctx":
ob["file"] = file_obs(MEM_FILE)
return ob
if kind == "cli":
import contextlib
import io
ta._time_awareness = self.instance
buf = io.StringIO()
old_argv = sys.argv
sys.argv = ["time_awareness.py"] + op.get("argv", [])
try:
with contextlib.redirect_stdout(buf):
ta.main()
finally:
sys.argv = old_argv
return {
"result": buf.getvalue(),
"state": state_obs(self.instance),
"file": file_obs(self.instance.storage_path),
}
raise ValueError(f"unknown op {kind}")
def run(self, ops):
return [self.step(op) for op in ops]
def ctx_case(now_us, update, seed=None, hour=None):
ops = [{"op": "new", "mono": 0.0}]
if seed is not None:
ops[0]["seed_file"] = seed
ops.append({"op": "ctx", "update": update, "now_us": now_us, "mono": 0.0})
return ops
def scenarios():
out = []
# S1 — time_of_day × circadian grid, weekend (2026-10-11 = Sunday) + weekday
ops = []
for h, m in [(4, 0), (5, 0), (6, 0), (8, 30), (9, 0), (11, 59), (12, 0),
(16, 59), (17, 0), (18, 0), (20, 59), (21, 0), (22, 0), (23, 59)]:
ops += [
{"op": "new", "mono": 0.0},
{"op": "ctx", "update": False,
"now_us": us_of(datetime(2026, 10, 11, h, m, 0, 0, tzinfo=timezone.utc)),
"mono": 0.0},
]
for h in (10, 13):
ops += [
{"op": "new", "mono": 0.0},
{"op": "ctx", "update": False,
"now_us": us_of(datetime(2026, 10, 13, h, 15, 0, 0, tzinfo=timezone.utc)),
"mono": 0.0},
]
out.append({"name": "tod_circadian_grid", "ops": ops})
# S2 — urgency branches (update=False, seeded last_interaction)
ops = []
offsets_h = [25.0, 24.0, 4.0 + 1.0 / 3600.0, 4.0, 2.0, 0.1, 5.0 / 60.0, 0.0, -1.0]
for i, oh in enumerate(offsets_h):
li_us = BASE_US - int(oh * HOUR_US)
seed = json.dumps(
{"last_interaction": iso_of(li_us)}, indent=2
) if i % 2 == 0 else None
ops += [{"op": "new", "mono": 0.0}]
if seed is None:
ops += [{"op": "seed_last", "last_interaction": iso_of(li_us), "mono": 0.0}]
ops += [{"op": "ctx", "update": False, "now_us": BASE_US, "mono": 0.0}]
out.append({"name": "urgency_branches", "ops": ops})
# S3 — parse_temporal_reference: every pattern + boundaries
texts = [
"yesterday", "today", "tomorrow", "last week", "next week",
"last month", "next month", "this morning", "this afternoon", "tonight",
"no temporal refs here", "YESTERDAY we said TODAY", "yesterdays news today",
"see you tomorrow.", "last week", "last\tweek", "lastweek",
"xlast weeky", "café yesterday", "yesterdayé",
"the last week of tomorrow", "next month and last month",
"Monday morning blues this morning", "tonight, tomorrow",
]
ops = [{"op": "new", "mono": 0.0}] + [
{"op": "parse", "text": t, "now_us": BASE_US, "mono": 0.0} for t in texts
]
out.append({"name": "parse_references", "ops": ops})
# S4 — save/debounce policy across a call sequence
out.append({
"name": "save_policy",
"ops": [
{"op": "new", "mono": 0.0},
{"op": "ctx", "update": True, "now_us": BASE_US, "mono": 0.0},
{"op": "ctx", "update": False, "now_us": BASE_US, "mono": 10.0},
{"op": "mark_dirty", "mono": 10.0},
{"op": "save_if_dirty", "mono": 20.0},
{"op": "save_if_dirty", "mono": 40.0},
{"op": "ctx", "update": True, "now_us": BASE_US + HOUR_US, "mono": 45.0},
{"op": "flush", "mono": 45.0},
{"op": "flush", "mono": 45.0},
{"op": "status", "now_us": BASE_US + HOUR_US, "mono": 45.0},
],
})
# S5 — _load file-seed matrix
seeds = [
'{"last_interaction": "2026-10-10T10:00:00.000000+00:00"}',
'{"last_interaction": "2026-10-10T10:00:00+00:00"}',
'{"last_interaction": null}',
"{}",
"corrupt{",
"[1, 2]",
'{"last_interaction": "garbage"}',
'{"last_interaction": "2026-10-10T10:00:00"}',
'{"last_interaction": "2026-10-10T05:00:00-05:00"}',
'{"last_interaction": "2026-10-10T10:00:00Z"}',
]
ops = []
for i, s in enumerate(seeds):
ops.append({"op": "new", "mono": 0.0, "seed_file": s})
if i in (0, 7, 8):
ops.append({"op": "ctx", "update": False, "now_us": BASE_US, "mono": 0.0})
out.append({"name": "load_matrix", "ops": ops})
# S6 — update=True sequence: debounce fire across calls, status, flush, parse
out.append({
"name": "update_sequence",
"ops": [
{"op": "new", "mono": 0.0},
{"op": "ctx", "update": True, "now_us": BASE_US, "mono": 0.0},
{"op": "ctx", "update": True, "now_us": BASE_US + HOUR_US, "mono": 40.0},
{"op": "status", "now_us": BASE_US + HOUR_US, "mono": 40.0},
{"op": "flush", "mono": 40.0},
{"op": "parse", "text": "yesterday", "now_us": BASE_US + HOUR_US, "mono": 40.0},
{"op": "seed_last", "last_interaction": None, "mono": 40.0},
{"op": "ctx", "update": True, "now_us": BASE_US + HOUR_US, "mono": 90.0},
],
})
# S7 — module singleton + CLI (facade-level; battery replays the kernel ops)
out.append({
"name": "module_and_cli",
"ops": [
{"op": "mod_ctx", "now_us": BASE_US, "mono": 0.0},
{"op": "mod_status", "now_us": BASE_US, "mono": 0.0},
{"op": "mod_parse", "text": "next week", "now_us": BASE_US, "mono": 0.0},
{"op": "mod_ctx", "now_us": BASE_US, "mono": 0.0,
"seed_file": '{"last_interaction": "2026-10-10T10:00:00+00:00"}'},
{"op": "new", "mono": 0.0},
{"op": "cli", "argv": ["status"], "now_us": BASE_US, "mono": 0.0},
{"op": "cli", "argv": ["context"], "now_us": BASE_US, "mono": 0.0},
{"op": "cli", "argv": ["bogus"], "now_us": BASE_US, "mono": 0.0},
{"op": "cli", "argv": [], "now_us": BASE_US, "mono": 0.0},
],
})
return out
def main():
shutil.rmtree("/tmp/ta_truth_memory", ignore_errors=True)
os.makedirs("/tmp/ta_truth_memory", exist_ok=True)
scenarios_doc = scenarios()
driver = Driver("/tmp/ta_truth_work")
for s in scenarios_doc:
s["obs"] = [driver.step(op) for op in s["ops"]]
doc = {
"module": "src/rune/time_awareness.py",
"pinned": "RUNE_TA_CORE=python (poisoned-truth law); clock pinned via fake datetime.now + monotonic",
"scenarios": scenarios_doc,
}
out_path = os.path.join(_ROOT, "src", "rust", "time_awareness", "src", "frozen_ta_truth.json")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w") as f:
json.dump(doc, f, indent=1)
f.write("\n")
n_obs = sum(len(s["obs"]) for s in scenarios_doc)
print(f"frozen {n_obs} observations across {len(scenarios_doc)} scenarios → {out_path}")
if __name__ == "__main__":
main()
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust time_awareness core (dichotomy wave,
#2479 tranche). Mirrors to_loader.py's contract:
FFI contract (doc-envelope law):
* ta_run({op, state, args, now_us, now_mono}) ->
{"ok": true, "value": ..., "state": {...mutated...},
"save": bool, "snapshot": {...}}
The kernel is PURE — no clock, no filesystem. The caller supplies the
state (last_interaction as isoformat text-or-null / dirty /
last_save_time) and the clock (now_us = UTC epoch microseconds,
now_mono = monotonic reading). `save: true` means the oracle would have
written the file mid-call; `snapshot` is the trigger-time file content —
the facade performs the write. All I/O, the singleton and the CLI stay
in Python.
Override the .so with TA_RUST_LIB (a FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "time_awareness"
_LIB_PATH = Path(os.environ.get("TA_RUST_LIB", _CRATE_DIR / "target" / "release" / "libtime_awareness.so"))
_lib = None
class TimeAwarenessCoreUnavailable(FileNotFoundError):
"""The Rust time_awareness core is not built. Callers fall back to Python."""
def available() -> bool:
return _LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not _LIB_PATH.is_file():
raise TimeAwarenessCoreUnavailable(str(_LIB_PATH))
lib = ctypes.CDLL(str(_LIB_PATH))
lib.ta_run.argtypes = [ctypes.c_void_p]
lib.ta_run.restype = ctypes.c_void_p
_lib = lib
return lib
def _call(payload: dict) -> dict:
lib = _load()
data = json.dumps(payload).encode()
buf = ctypes.create_string_buffer(data)
ptr = lib.ta_run(ctypes.cast(buf, ctypes.c_void_p))
if not ptr:
raise RuntimeError("ta_run returned null")
out = ctypes.string_at(ptr).decode()
# free via any sibling free symbol already in-process (capability_scoring)
try:
scoring = ctypes.CDLL(str(
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
))
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
scoring.cs_free_string(ptr)
except OSError:
pass # leak-on-fallback only if capability_scoring absent; bounded
doc = json.loads(out)
if not doc.get("ok"):
raise RuntimeError(doc.get("err", "ta_run failed"))
return doc
def run_op(op: str, state: dict, args: dict | None = None, now_us: int = 0, now_mono: float = 0.0) -> dict:
"""Run one kernel op. Returns the full envelope:
{"value": ..., "state": {...mutated...}, "save": bool, "snapshot": ...}.
"""
payload = {"op": op, "state": state, "args": args or {},
"now_us": now_us, "now_mono": now_mono}
return _call(payload)
+120 -4
View File
@@ -30,6 +30,8 @@ if _SD not in sys.path:
sys.path.insert(0, _SD)
import memory_env
_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
@dataclass
class TemporalContext:
@@ -106,13 +108,80 @@ class TimeAwareness:
if now - self._last_save_time >= self._SAVE_DEBOUNCE_SECS:
self._save()
# ── Rust kernel plumbing (dichotomy wave) ───────────────────────────
# The kernel owns every temporal DECISION (time-of-day/circadian/
# urgency branches, hours-since math, temporal-reference parsing,
# dirty/debounce save policy); this facade keeps the I/O (_load/
# _save), the clock and the CLI. `save: true` in the envelope means
# the oracle wrote the file mid-call with the trigger-time snapshot —
# we perform that write, then adopt the kernel's final state.
def _to_state(self) -> dict:
"""Kernel transfer form; last_interaction as isoformat-or-null."""
return {
"last_interaction": self.last_interaction.isoformat() if self.last_interaction else None,
"dirty": self._dirty,
"last_save_time": self._last_save_time,
}
def _apply_state(self, st: dict):
self.last_interaction = (
datetime.fromisoformat(st["last_interaction"])
if st["last_interaction"] is not None
else None
)
self._dirty = st["dirty"]
self._last_save_time = st["last_save_time"]
def _write_snapshot(self, snap: dict):
os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
data = {"last_interaction": snap["last_interaction"]}
tmp = self.storage_path + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=2)
os.replace(tmp, self.storage_path)
def _run_kernel(self, op: str, args: dict, now_us: int, now_mono: float):
"""Run one op through the Rust kernel; None → fall back to the
Python body (kernel absent, loader failure — never a broken call)."""
core = _ta_core()
if core is None:
return None
try:
doc = core.run_op(op, self._to_state(), args, now_us, now_mono)
except Exception:
return None
if doc.get("save"):
self._write_snapshot(doc["snapshot"])
self._apply_state(doc["state"])
return doc["value"]
def get_context(self, update_interaction: bool = True) -> TemporalContext:
"""Get current temporal context.
Args:
update_interaction: If True, update last_interaction timestamp.
Set to False for read-only status checks.
"""
Delegates the whole temporal decision to the Rust core when built."""
now = datetime.now(timezone.utc)
delegated = self._run_kernel(
"get_context",
{"update_interaction": update_interaction},
(now - _EPOCH) // timedelta(microseconds=1),
_time.monotonic(),
)
if delegated is not None:
return TemporalContext(
current_time=delegated["current_time"],
current_date=delegated["current_date"],
day_of_week=delegated["day_of_week"],
time_of_day=delegated["time_of_day"],
is_weekend=delegated["is_weekend"],
hours_since_last_interaction=delegated["hours_since_last_interaction"],
urgency_modifier=delegated["urgency_modifier"],
circadian_phase=delegated["circadian_phase"],
)
now = datetime.now(timezone.utc)
# Calculate hours since last interaction
@@ -174,7 +243,18 @@ class TimeAwareness:
def parse_temporal_reference(self, text: str) -> Dict[str, Any]:
"""Parse temporal references in text (yesterday, last week, etc.).
Uses word boundary matching to avoid false positives."""
Uses word boundary matching to avoid false positives.
Delegates the matching decision to the Rust core when built."""
now = datetime.now(timezone.utc)
delegated = self._run_kernel(
"parse_temporal_reference",
{"text": text},
(now - _EPOCH) // timedelta(microseconds=1),
_time.monotonic(),
)
if delegated is not None:
return delegated
text_lower = text.lower()
now = datetime.now(timezone.utc)
@@ -206,7 +286,12 @@ class TimeAwareness:
return {"reference": None}
def get_status(self) -> Dict[str, Any]:
"""Get time awareness status (read-only, no side effects)."""
"""Get time awareness status (read-only, no side effects).
Delegates to the Rust core when built."""
delegated = self._run_kernel("get_status", {}, *_clock_args())
if delegated is not None:
return delegated
context = self.get_context(update_interaction=False)
return {
"current_time": context.current_time,
@@ -219,11 +304,42 @@ class TimeAwareness:
}
def flush(self):
"""Force-save any pending changes to disk."""
"""Force-save any pending changes to disk.
The save DECISION delegates to the Rust core when built (the
facade performs the write via the snapshot)."""
now = datetime.now(timezone.utc)
delegated = self._run_kernel(
"flush", {},
(now - _EPOCH) // timedelta(microseconds=1),
_time.monotonic(),
)
if delegated is not None:
return
if self._dirty:
self._save()
def _clock_args():
"""(now_us, now_mono) for kernel ops that need no computed `now`."""
now = datetime.now(timezone.utc)
return (now - _EPOCH) // timedelta(microseconds=1), _time.monotonic()
def _ta_core():
"""The Rust time_awareness core when available (strangler law).
RUNE_TA_CORE=python pins the fallback."""
if os.environ.get("RUNE_TA_CORE", "auto") == "python":
return None
try:
from rune import ta_loader
except Exception: # noqa: BLE001 — any loader failure means fallback, never a broken call
return None
try:
return ta_loader if ta_loader.available() else None
except Exception: # noqa: BLE001 — availability probe must never crash the fallback
return None
# Global time awareness
_time_awareness = None
+98
View File
@@ -2,6 +2,104 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time_awareness"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+4
View File
@@ -2,6 +2,10 @@
name = "time_awareness"
version = "0.1.0"
edition = "2021"
description = "FULL parity port of src/rune/time_awareness.py temporal core (dichotomy wave, #2479 tranche); decision logic in the kernel, file I/O and clock stay Python"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
@@ -0,0 +1,143 @@
//! Branch battery — replays the FROZEN ORACLE truth VERBATIM
//! (frozen_ta_truth.json, generated from the live python oracle with
//! RUNE_TA_CORE=python pinned and the clock faked to fixed instants).
//! Each scenario's op script is driven through the kernel exactly as the
//! facade drives it (state in → state out, snapshot on save), and every
//! observation (result + state + file) must equal the frozen oracle
//! byte-for-byte after JSON parse. Because the clock is PINNED, nothing
//! is masked — current_time, dates, hours_since, last_interaction and
//! last_save_time are all frozen and compared verbatim.
//!
//! Facade-level ops are skipped here (live-parity covered): `cli` (python
//! main()) and `mod_*` (module-singleton management). A kernel envelope
//! error on get_context/get_status is the oracle's TypeError (naive
//! last_interaction) raised through the facade fallback — modelled below.
use serde_json::{json, Value};
use super::{load_from_str, run_op, state_to_value, State};
const TRUTH: &str = include_str!("frozen_ta_truth.json");
enum FileModel {
Absent,
/// Exists on disk but not the oracle's document shape (corrupt JSON,
/// a JSON array…) — the oracle's blanket except treats it as no data.
/// The truth never OBSERVES its content pre-save (a save overwrites).
Corrupt,
Doc(Value),
}
fn state_obs(st: &State, file: &FileModel) -> Value {
let mut v = state_to_value(st);
v["file_exists"] = json!(!matches!(file, FileModel::Absent));
v
}
fn file_obs(file: &FileModel) -> Value {
match file {
FileModel::Absent => Value::Null,
FileModel::Corrupt => panic!("corrupt file observed pre-save — the oracle would crash reading it"),
FileModel::Doc(d) => d.clone(),
}
}
/// Drive one battery op through the kernel the way the facade would.
/// Returns the full observation (result + state [+ file]).
fn step(st: &mut State, op: &Value, file: &mut FileModel) -> Value {
let kind = op["op"].as_str().unwrap_or("").to_string();
let now_us = op.get("now_us").and_then(Value::as_i64).unwrap_or(0);
let now_mono = op.get("mono").and_then(Value::as_f64).unwrap_or(0.0);
let mut snap: Option<Value> = None;
let result = match kind.as_str() {
"new" => {
match op.get("seed_file").and_then(Value::as_str) {
Some(s) => {
*st = load_from_str(s);
*file = match serde_json::from_str::<Value>(s) {
Ok(v) if v.is_object() => FileModel::Doc(v),
Ok(_) => FileModel::Corrupt, // parses, but not a document
Err(_) => FileModel::Corrupt,
};
}
None => {
*st = State::default();
*file = FileModel::Absent;
}
}
// the oracle's __init__ stamps the monotonic clock
st.last_save_time = now_mono;
Value::Null
}
"seed_last" => {
st.last_interaction = op
.get("last_interaction")
.and_then(Value::as_str)
.map(str::to_string);
Value::Null
}
"ctx" => {
let args = json!({"update_interaction": op.get("update").and_then(Value::as_bool).unwrap_or(true)});
match run_op(st, "get_context", &args, now_us, now_mono, &mut snap) {
Ok(v) => v,
// naive last_interaction: the facade falls back to python,
// which raises TypeError — the oracle's observable truth
Err(_) => json!({"error": "TypeError"}),
}
}
"status" => match run_op(st, "get_status", &json!({}), now_us, now_mono, &mut snap) {
Ok(v) => v,
// naive last_interaction: the facade falls back to python,
// which raises TypeError — the oracle's observable truth
Err(_) => json!({"error": "TypeError"}),
},
"parse" => {
let args = json!({"text": op["text"].as_str().unwrap_or("")});
run_op(st, "parse_temporal_reference", &args, now_us, now_mono, &mut snap)
.expect("parse op failed")
}
"flush" => run_op(st, "flush", &json!({}), now_us, now_mono, &mut snap)
.expect("flush op failed"),
"mark_dirty" => run_op(st, "mark_dirty", &json!({}), now_us, now_mono, &mut snap)
.expect("mark_dirty op failed"),
"save_if_dirty" => run_op(st, "save_if_dirty", &json!({}), now_us, now_mono, &mut snap)
.expect("save_if_dirty op failed"),
other => panic!("battery must not see facade-level op {other}"),
};
if let Some(s) = &snap {
*file = FileModel::Doc(s.clone());
}
let mut ob = json!({"result": result, "state": state_obs(st, file)});
// the truth generator attaches the file obs exactly when the save
// fired (ctx mirrors _save_if_dirty's decision) — flush/save_if_dirty
// always carry it, matching the driver
if snap.is_some() || matches!(kind.as_str(), "flush" | "save_if_dirty") {
ob["file"] = file_obs(file);
}
ob
}
#[test]
fn replay_frozen_truth_verbatim() {
let doc: Value = serde_json::from_str(TRUTH).expect("frozen truth must parse");
let scenarios = doc["scenarios"].as_array().expect("scenarios array");
let mut checked = 0usize;
for s in scenarios {
let name = s["name"].as_str().unwrap_or("?");
let ops = s["ops"].as_array().expect("ops");
let obs = s["obs"].as_array().expect("obs");
assert_eq!(ops.len(), obs.len(), "{name}: ops/obs length mismatch");
let mut st = State::default();
let mut file = FileModel::Absent;
for (i, (op, want)) in ops.iter().zip(obs.iter()).enumerate() {
let kind = op["op"].as_str().unwrap_or("").to_string();
if kind == "cli" || kind.starts_with("mod_") {
continue; // facade-level (python main()/singleton); live-parity covered
}
let got = step(&mut st, op, &mut file);
assert_eq!(&got, want, "{name} op#{i} ({kind}) diverged");
checked += 1;
}
}
assert!(checked >= 60, "battery shrank: {checked} observations replayed");
}
File diff suppressed because it is too large. Load diff
+490 -88
View File
@@ -1,99 +1,501 @@
//! time_awareness — Rune's sense of time (W6).
//! PARITY port of src/rune/time_awareness.py.
//! time_awareness — Rune's sense of time.
//!
//! FULL parity port of src/rune/time_awareness.py (dichotomy wave, #2479
//! tranche). The kernel is PURE: it owns every temporal decision (time of
//! day, circadian phase, weekend, hours-since math, urgency modifier,
//! temporal-reference parsing, dirty/debounce save policy) while the
//! Python facade keeps the I/O (`_load`/`_save`), the clock, the singleton
//! and the CLI — split at the pure/IO boundary (T8 law).
//!
//! FFI (doc-envelope law): `ta_run({op, state, args, now_us, now_mono})`
//! → {"ok": true, "value": ..., "state": {...mutated...}, "save": bool,
//! "snapshot": {...file content to write...}}.
//! `now_us` is UTC epoch microseconds (the facade reads the clock and
//! hands it in); `now_mono` is the monotonic reading. `save: true` means
//! the oracle would have written the file mid-call; `snapshot` is the
//! trigger-time content ({"last_interaction": iso|null}) — the facade
//! performs the write.
//!
//! Float parity: Python computes hours_since as
//! `((days*86400 + secs) * 10**6 + micros) / 10**6 / 3600` — the kernel
//! does `(delta_us as f64) / 1e6 / 3600.0`, the identical op sequence on
//! exact-integer deltas, so results are bit-identical.
//!
//! ISO handling: the state carries `last_interaction` exactly as the
//! facade holds it (datetime.isoformat() output: optional fraction,
//! ±HH:MM offset or naive). A NAIVE last_interaction parses fine in
//! Python's `_load` but makes `now - naive` raise TypeError — the kernel
//! surfaces that as an envelope error, and the facade's fallback path
//! raises the very same TypeError. Byte-faithful, including the quirk.
/// Temporal context for an interaction
#[derive(Debug, Clone)]
pub struct TemporalContext {
pub current_time: String,
pub current_date: String,
pub day_of_week: String,
pub time_of_day: String,
pub is_weekend: bool,
pub hours_since_last_interaction: f64,
pub urgency_modifier: f64,
pub circadian_phase: String,
use serde_json::{json, Value};
const SAVE_DEBOUNCE_SECS: f64 = 30.0;
const US_PER_DAY: i64 = 86_400_000_000;
#[derive(Clone, Debug, Default)]
struct State {
/// datetime.isoformat() of the facade's last_interaction (or None)
last_interaction: Option<String>,
dirty: bool,
last_save_time: f64,
}
pub fn get_temporal_context(last_interaction: Option<u64>) -> TemporalContext {
let now = std::time::SystemTime::now();
let secs = now.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
// ── state (de)serialization ─────────────────────────────────────────────
// Day of week: 0=Thursday (epoch), 1=Friday, ...
let days_since_epoch = secs / 86400;
let dow_idx = ((days_since_epoch + 4) % 7) as usize; // +4 because epoch is Thursday
let day_names = [
"Thursday",
"Friday",
"Saturday",
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
];
let day_of_week = day_names[dow_idx].to_string();
let is_weekend = dow_idx == 2 || dow_idx == 3; // Saturday or Sunday
// Hour of day (UTC)
let hour = (secs % 86400) / 3600;
let time_of_day = match hour {
6..=11 => "morning",
12..=17 => "afternoon",
18..=22 => "evening",
_ => "night",
}
.to_string();
let circadian_phase = match hour {
7..=21 => "active",
22..=23 => "winding_down",
0..=4 => "sleeping",
5..=6 => "waking",
_ => "active",
}
.to_string();
let hours_since = match last_interaction {
Some(last) => (secs as f64 - last as f64) / 3600.0,
None => 0.0,
};
let urgency_modifier = if hours_since > 24.0 {
1.2
} else if hours_since > 8.0 {
1.0
} else {
0.8
};
TemporalContext {
current_time: format!("{}:{:02}:{:02}", hour, (secs % 3600) / 60, secs % 60),
current_date: format!("epoch day {}", days_since_epoch),
day_of_week,
time_of_day,
is_weekend,
hours_since_last_interaction: hours_since,
urgency_modifier,
circadian_phase,
}
fn parse_state(v: &Value) -> Result<State, String> {
let o = v.as_object().ok_or("state must be an object")?;
Ok(State {
last_interaction: o
.get("last_interaction")
.and_then(Value::as_str)
.map(str::to_string),
dirty: o.get("dirty").and_then(Value::as_bool).unwrap_or(false),
last_save_time: o
.get("last_save_time")
.and_then(Value::as_f64)
.unwrap_or(0.0),
})
}
fn state_to_value(st: &State) -> Value {
json!({
"last_interaction": st.last_interaction.clone(),
"dirty": st.dirty,
"last_save_time": st.last_save_time,
})
}
/// Battery-only: the oracle's `_load` semantics over the FILE format —
/// corrupt JSON, a non-object document, a missing/null key or an ISO
/// string Python's fromisoformat rejects all discard the timestamp (the
/// blanket `except` recovery). A valid string is canonicalized through
/// parse+emit exactly like `datetime.fromisoformat(...).isoformat()`
/// (fraction dropped when zero, Z → +00:00). The production facade keeps
/// the real `_load` (I/O side).
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_temporal_context() {
let ctx = get_temporal_context(None);
assert!(!ctx.day_of_week.is_empty());
assert!(ctx.urgency_modifier >= 0.8);
assert!(!ctx.circadian_phase.is_empty());
fn load_from_str(s: &str) -> State {
let mut st = State::default();
let v = match serde_json::from_str::<Value>(s) {
Ok(v) => v,
Err(_) => return st,
};
let Some(o) = v.as_object() else {
return st; // .get() on a non-dict raises → blanket except
};
if let Some(Value::String(s)) = o.get("last_interaction") {
if let Some((us, off)) = parse_iso(s) {
st.last_interaction = Some(emit_iso(us, off));
} // parse failure = fromisoformat raising → blanket except
}
st
}
#[test]
fn test_weekend_detection() {
let ctx = get_temporal_context(None);
// Just verify it's a bool — the actual day depends on current time
let _ = ctx.is_weekend;
// ── calendar math (proleptic Gregorian, Howard Hinnant's algorithms) ────
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = if m > 2 { m - 3 } else { m + 9 } as i64;
let doy = (153 * mp + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
(if m <= 2 { y + 1 } else { y }, m as u32, d as u32)
}
/// Python weekday(): Monday=0 … Sunday=6.
fn python_weekday(days: i64) -> u32 {
(days + 3).rem_euclid(7) as u32
}
// ── ISO 8601 (the exact forms datetime.isoformat()/fromisoformat emit) ──
/// Parse "YYYY-MM-DDTHH:MM:SS[.ffffff][Z|±HH:MM]" → (epoch µs, offset
/// seconds: None for naive). Accepts the ' ' separator and 1-6 fraction
/// digits like fromisoformat; anything else → None (the blanket except).
fn parse_iso(s: &str) -> Option<(i64, Option<i32>)> {
let b = s.as_bytes();
let num = |r: std::ops::Range<usize>| -> Option<i64> {
let sl = s.get(r.clone())?;
if sl.bytes().any(|c| !c.is_ascii_digit()) {
return None;
}
sl.parse::<i64>().ok()
};
if b.len() < 19 || b[4] != b'-' || b[7] != b'-' {
return None;
}
let (sep, rest) = match b[10] {
b'T' | b' ' => (10, 11),
_ => return None,
};
let _ = sep;
if b[13] != b':' || b[16] != b':' {
return None;
}
let (y, mo, d) = (num(0..4)?, num(5..7)?, num(8..10)?);
let (h, mi, se) = (num(rest..rest + 2)?, num(rest + 3..rest + 5)?, num(rest + 6..rest + 8)?);
if !(1..=12).contains(&mo) || !(1..=31).contains(&d) || h > 23 || mi > 59 || se > 59 {
return None;
}
let mut i = rest + 8;
let mut frac_us = 0i64;
if i < b.len() && b[i] == b'.' {
i += 1;
let start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
let digits = i - start;
if digits == 0 {
return None;
}
// fromisoformat truncates beyond microseconds
frac_us = num(start..i.min(start + 6))? * 10i64.pow(6 - (digits.min(6)) as u32);
}
let mut offset = None;
if i < b.len() {
match b[i] {
b'Z' | b'z' => {
offset = Some(0);
i += 1;
}
b'+' | b'-' => {
if b.len() != i + 6 || b[i + 3] != b':' {
return None;
}
let (oh, om) = (num(i + 1..i + 3)?, num(i + 4..i + 6)?);
if oh > 23 || om > 59 {
return None;
}
let mag = oh * 3600 + om * 60;
offset = Some(if b[i] == b'-' { -mag } else { mag } as i32);
i += 6;
}
_ => return None,
}
}
if i != b.len() {
return None;
}
let days = days_from_civil(y, mo as u32, d as u32);
let mut us = days * US_PER_DAY + h * 3_600_000_000 + mi * 60_000_000 + se * 1_000_000 + frac_us;
if let Some(off) = offset {
us -= off as i64 * 1_000_000;
}
Some((us, offset))
}
/// Inverse of parse: epoch µs + offset → datetime.isoformat() text
/// (fraction only when non-zero, offset as ±HH:MM, naive has none).
fn emit_iso(us: i64, offset: Option<i32>) -> String {
let wall = us + offset.map_or(0, |o| o as i64) * 1_000_000;
let days = wall.div_euclid(US_PER_DAY);
let sod = wall.rem_euclid(US_PER_DAY);
let secs = sod / 1_000_000;
let frac = sod % 1_000_000;
let (y, m, d) = civil_from_days(days);
let (h, mi, s) = (secs / 3600, (secs % 3600) / 60, secs % 60);
let mut out = format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}");
if frac != 0 {
out.push_str(&format!(".{frac:06}"));
}
if let Some(off) = offset {
let (sign, mag) = if off < 0 { ('-', -off) } else { ('+', off) };
out.push_str(&format!("{sign}{:02}:{:02}", mag / 3600, (mag % 3600) / 60));
}
out
}
// ── temporal core ───────────────────────────────────────────────────────
fn date_of(days: i64) -> String {
let (y, m, d) = civil_from_days(days);
format!("{y:04}-{m:02}-{d:02}")
}
fn hours_since(st: &State, now_us: i64) -> Result<f64, String> {
match &st.last_interaction {
None => Ok(0.0),
Some(s) => match parse_iso(s) {
// naive-aware subtraction: Python raises TypeError here
Some((lu, Some(_))) => Ok(((now_us - lu) as f64) / 1_000_000.0 / 3600.0),
Some((_lu, None)) => Err("naive last_interaction".into()),
None => Err("unparseable last_interaction".into()),
},
}
}
/// The context body (both get_context and get_status read it); the
/// update+save side effect happens only when `update` is set.
fn context_value(st: &mut State, update: bool, now_us: i64, now_mono: f64, snap: &mut Option<Value>) -> Result<Value, String> {
let hours = hours_since(st, now_us)?;
if update {
st.last_interaction = Some(emit_iso(now_us, Some(0)));
st.dirty = true;
if now_mono - st.last_save_time >= SAVE_DEBOUNCE_SECS {
st.dirty = false;
st.last_save_time = now_mono;
*snap = Some(json!({"last_interaction": st.last_interaction.clone()}));
}
}
let days = now_us.div_euclid(US_PER_DAY);
let secs = now_us.rem_euclid(US_PER_DAY) / 1_000_000;
let hour = (secs / 3600) as u32;
let time_of_day = if (5..12).contains(&hour) {
"morning"
} else if (12..17).contains(&hour) {
"afternoon"
} else if (17..21).contains(&hour) {
"evening"
} else {
"night"
};
let circadian_phase = if !(6..22).contains(&hour) {
"sleeping"
} else if (6..9).contains(&hour) {
"waking"
} else if (9..18).contains(&hour) {
"active"
} else {
"winding_down"
};
let urgency_modifier = if hours > 24.0 {
1.5
} else if hours > 4.0 {
1.2
} else if hours < 0.1 {
0.8
} else {
1.0
};
let weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
let wd = python_weekday(days);
Ok(json!({
"current_time": format!("{:02}:{:02}", secs / 3600, (secs % 3600) / 60),
"current_date": date_of(days),
"day_of_week": weekdays[wd as usize],
"time_of_day": time_of_day,
"is_weekend": wd >= 5,
"hours_since_last_interaction": hours,
"urgency_modifier": urgency_modifier,
"circadian_phase": circadian_phase,
}))
}
/// `parse_temporal_reference` — the oracle's pattern list, in priority
/// order (first LIST entry that matches anywhere wins, not the earliest
/// text position). Word boundaries follow Python re semantics: a word
/// char is alphanumeric or '_', and \s is the Unicode whitespace class.
fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn word_at(text: &[char], pos: usize, word: &[char]) -> bool {
if pos + word.len() > text.len() {
return false;
}
let before_ok = pos == 0 || !is_word(text[pos - 1]);
let after_ok = pos + word.len() == text.len() || !is_word(text[pos + word.len()]);
before_ok && after_ok && text[pos..pos + word.len()] == *word
}
/// `\b{w1}\s+{w2}\b` — w1 must start at a boundary, then a run of one or
/// more whitespace chars, then w2 at a trailing boundary. The whitespace
/// run is forced (w2 is not whitespace), so greedy matching is unique.
fn pair_at(text: &[char], pos: usize, w1: &[char], w2: &[char]) -> bool {
if pos + w1.len() > text.len() || text[pos..pos + w1.len()] != *w1 {
return false;
}
if pos != 0 && is_word(text[pos - 1]) {
return false; // no \b before w1
}
let mut i = pos + w1.len();
let ws_start = i;
while i < text.len() && text[i].is_whitespace() {
i += 1;
}
if i == ws_start {
return false; // \s+ needs at least one
}
word_at(text, i, w2)
}
fn scan(text: &[char], pred: impl Fn(usize) -> bool) -> bool {
(0..text.len()).any(pred)
}
fn parse_temporal_reference(text: &str, now_us: i64) -> Value {
let lower: Vec<char> = text.to_lowercase().chars().collect();
let w = |s: &str| s.chars().collect::<Vec<char>>();
let one = |word: &str| scan(&lower, |p| word_at(&lower, p, &w(word)));
let two = |a: &str, b: &str| scan(&lower, |p| pair_at(&lower, p, &w(a), &w(b)));
let days = now_us.div_euclid(US_PER_DAY);
let hit: Option<(&str, i64, Option<&str>)> = if one("yesterday") {
Some(("yesterday", -1, None))
} else if one("today") {
Some(("today", 0, None))
} else if one("tomorrow") {
Some(("tomorrow", 1, None))
} else if two("last", "week") {
Some(("last week", -7, None))
} else if two("next", "week") {
Some(("next week", 7, None))
} else if two("last", "month") {
Some(("last month", -30, None))
} else if two("next", "month") {
Some(("next month", 30, None))
} else if two("this", "morning") {
Some(("this morning", 0, Some("morning")))
} else if two("this", "afternoon") {
Some(("this afternoon", 0, Some("afternoon")))
} else if one("tonight") {
Some(("tonight", 0, Some("night")))
} else {
None
};
match hit {
None => json!({"reference": null}),
Some((reference, delta, time)) => {
let mut result = json!({
"reference": reference,
"date": date_of(days + delta),
});
if let Some(t) = time {
result["time"] = json!(t);
}
result
}
}
}
// ── dispatch ────────────────────────────────────────────────────────────
/// Runs one op against the state. Returns the op VALUE; `snap` receives
/// the file content to persist when the oracle would have written mid-call.
fn run_op(
st: &mut State,
op: &str,
args: &Value,
now_us: i64,
now_mono: f64,
snap: &mut Option<Value>,
) -> Result<Value, String> {
match op {
"get_context" => {
let update = args.get("update_interaction").and_then(Value::as_bool).unwrap_or(true);
context_value(st, update, now_us, now_mono, snap)
}
"get_status" => {
let mut v = context_value(st, false, now_us, now_mono, snap)?;
if let Some(o) = v.as_object_mut() {
o.remove("urgency_modifier");
}
Ok(v)
}
"parse_temporal_reference" => {
let text = args.get("text").and_then(Value::as_str).unwrap_or("");
Ok(parse_temporal_reference(text, now_us))
}
"mark_dirty" => {
st.dirty = true;
Ok(Value::Null)
}
"save_if_dirty" => {
if st.dirty && now_mono - st.last_save_time >= SAVE_DEBOUNCE_SECS {
st.dirty = false;
st.last_save_time = now_mono;
*snap = Some(json!({"last_interaction": st.last_interaction.clone()}));
}
Ok(Value::Null)
}
"flush" => {
if st.dirty {
st.dirty = false;
st.last_save_time = now_mono;
*snap = Some(json!({"last_interaction": st.last_interaction.clone()}));
}
Ok(Value::Null)
}
other => Err(format!("unknown op: {other}")),
}
}
// ── FFI (doc-envelope law) ──────────────────────────────────────────────
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
if ptr.is_null() {
return Err("null argument pointer".into());
}
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
serde_json::from_str(x).map_err(|e| e.to_string())
}
fn envelope(r: Result<(Value, State, Option<Value>), String>) -> *mut c_char {
let v = match r {
Ok((value, st, snap)) => json!({
"ok": true,
"value": value,
"state": state_to_value(&st),
"save": snap.is_some(),
"snapshot": snap.unwrap_or(Value::Null),
}),
Err(e) => json!({"ok": false, "err": e}),
};
CString::new(serde_json::to_string(&v).unwrap_or_default())
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
/// ta_run({"op", "state", "args"?, "now_us"?, "now_mono"?})
/// → {"ok": true, "value": ..., "state": {...mutated...}, "save": bool,
/// "snapshot": {...}}. `save: true` = the oracle wrote the file at this
/// point with `snapshot` as content; the facade owns the write. The kernel
/// is pure — no clock, no filesystem.
#[no_mangle]
pub extern "C" fn ta_run(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let mut st = parse_state(a.get("state").ok_or("missing state")?)?;
let args = a.get("args").cloned().unwrap_or_else(|| json!({}));
let now_us = a.get("now_us").and_then(Value::as_i64).unwrap_or(0);
let now_mono = a.get("now_mono").and_then(Value::as_f64).unwrap_or(0.0);
let op = a.get("op").and_then(Value::as_str).ok_or("missing op")?;
let mut snap = None;
let value = run_op(&mut st, op, &args, now_us, now_mono, &mut snap)?;
Ok((value, st, snap))
}))
}
// NOTE: no ta_free_string export — sibling crates sharing the Python
// process already export identical free symbols; a duplicate breaks the
// cdylib link. Callers free through capability_scoring's cs_free_string.
#[path = "branch_battery.rs"]
#[cfg(test)]
mod branch_battery;
+77
View File
@@ -0,0 +1,77 @@
"""Port evidence for the time_awareness dichotomy port (#2479 tranche).
Replays the frozen oracle truth (src/rust/time_awareness/src/
frozen_ta_truth.json — generated from the live python oracle with
RUNE_TA_CORE=python pinned and the clock faked to fixed instants,
7 scenarios / 119 observations) through the wired facade TWICE:
* python path — RUNE_TA_CORE=python (the pinned fallback)
* rust path — the built kernel via ta_loader (skipped when absent)
The clock is PINNED (fake datetime.now + monotonic via the shared
harness in scripts/gen_time_awareness_truth.py), so every observation —
result, state, file — is compared verbatim, nothing masked. Facade-level
ops (cli / module-singleton) are part of the frozen truth and are driven
here through the real facade paths.
"""
import os
import sys
from pathlib import Path
import pytest
_SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(_SCRIPTS))
import gen_time_awareness_truth as gen # noqa: E402 — shared clock-pinned harness
_TRUTH = Path(__file__).resolve().parents[1] / "src" / "rust" / "time_awareness" / "src" / "frozen_ta_truth.json"
def _replay(truth, tmp_path, pin):
os.environ["RUNE_TA_CORE"] = pin
d = gen.Driver(tmp_path)
for s in truth["scenarios"]:
got = d.run(s["ops"])
assert len(got) == len(s["obs"]), s["name"]
for i, (g, w) in enumerate(zip(got, s["obs"])):
assert g == w, f"{s['name']} op#{i} ({s['ops'][i].get('op')}) diverged"
@pytest.fixture()
def truth():
with open(_TRUTH) as f:
import json
return json.load(f)
def test_python_fallback_path_replays_frozen_truth(truth, tmp_path):
_replay(truth, tmp_path / "py", "python")
def test_rust_kernel_path_replays_frozen_truth(truth, tmp_path):
from rune import ta_loader
if not ta_loader.available():
pytest.skip("time_awareness kernel not built")
os.environ.pop("RUNE_TA_CORE", None)
_replay(truth, tmp_path / "rust", "auto")
def test_naive_last_interaction_raises_typeerror_on_both_pins(tmp_path):
"""The oracle's quirk: _load accepts a naive ISO timestamp but
get_context then raises TypeError (naive-aware subtraction). The
kernel surfaces an envelope error and the facade fallback must raise
the very same TypeError — byte-faithful, including the quirk."""
gen.set_clock({"op": "x", "now_us": gen.BASE_US, "mono": 0.0})
for pin in ("python", "auto"):
os.environ["RUNE_TA_CORE"] = pin
d = gen.Driver(tmp_path / pin)
obs = d.run([
{"op": "new", "mono": 0.0,
"seed_file": '{"last_interaction": "2026-10-10T10:00:00"}'},
{"op": "ctx", "update": False, "now_us": gen.BASE_US, "mono": 0.0},
])
assert obs[1]["result"] == {"error": "TypeError"}