@r1z4x/tezgah 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,16 +7,16 @@ The session block, the per-turn block and the per-turn digest
7
7
  ledger here, so they agree on what a lesson is and on its key. A module of its
8
8
  own because the gate must not import `tezgah_context` (its import cost, and the
9
9
  rule `hooks/tezgah_task.py::repo_root` states): this one imports only
10
- `tezgah_paths` at load, and `tezgah_gate` lazily, only for a line that names a
11
- gate rule as its enforcer.
10
+ `tezgah_paths` and `tezgah_store` at load, and `tezgah_gate` lazily, only for a
11
+ line that names a gate rule as its enforcer.
12
12
 
13
13
  Stdlib only."""
14
14
  import hashlib
15
- import json
16
15
  import os
17
16
  import re
18
17
 
19
- from tezgah_paths import cache_dir, off
18
+ import tezgah_store
19
+ from tezgah_paths import off
20
20
 
21
21
  # A line carries its metadata after its text, never in it: from the first
22
22
  # `|| check:` or `|| enforced_by:` clause outside a code span to the end of the
@@ -114,73 +114,28 @@ def is_ledger(path):
114
114
  and os.path.basename(os.path.dirname(path)) == ".tezgah")
115
115
 
116
116
 
117
- def taint_path(root):
118
- """The `lesson_tainted` index of the repository at `root`: the gate keeps
119
- each keyed row here as well as in the session's ledger, so the context of a
120
- later session reads one small file for the per-line label instead of every
121
- ledger on the machine (3173 files, 30 MB on the measuring machine,
122
- 2026-10-07)."""
123
- name = hashlib.sha1(os.path.realpath(root).encode("utf-8", "replace"))
124
- return os.path.join(cache_dir(), "lessons", name.hexdigest()[:16] + ".jsonl")
125
-
126
-
127
- def _index_rows(path):
128
- """The parseable keyed rows of an index file, oldest first; [] without one.
129
- A damaged line costs only itself."""
130
- rows = []
131
- try:
132
- with open(path, encoding="utf-8", errors="replace") as fh:
133
- for ln in fh:
134
- try:
135
- row = json.loads(ln)
136
- except ValueError:
137
- continue
138
- if isinstance(row, dict) and row.get("key"):
139
- rows.append(row)
140
- except OSError:
141
- pass
142
- return rows
117
+ def taint_root(root):
118
+ """The key of the repository at `root` in the `lesson_taint` store: the gate
119
+ keeps each keyed row there as well as in the session's ledger, so the
120
+ context of a later session reads one repository's rows for the per-line
121
+ label instead of every ledger on the machine (3173 files, 30 MB on the
122
+ measuring machine, 2026-10-07)."""
123
+ return hashlib.sha1(os.path.realpath(root).encode("utf-8", "replace")).hexdigest()[:16]
143
124
 
144
125
 
145
126
  def write_taint(root, keep, rows):
146
127
  """Rewrite the index of the repository at `root`: the rows it holds whose key
147
- is in `keep` (the ledger's lines plus the write's new ones), then `rows`. So
148
- the file never outgrows the ledger it labels. Best effort, like a ledger row.
149
- ponytail: two sessions rewriting one index in the same instant can lose one
150
- row (one label); the session ledger keeps its own copy of every row."""
151
- path = taint_path(root)
152
- kept = [r for r in _index_rows(path) if r["key"] in keep] + rows
153
- tmp = "%s.%d.tmp" % (path, os.getpid())
154
- try:
155
- os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
156
- fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
157
- with os.fdopen(fd, "w", encoding="utf-8") as fh:
158
- fh.write("".join(json.dumps(r) + "\n" for r in kept))
159
- os.replace(tmp, path)
160
- except OSError:
161
- pass
162
-
163
-
164
- # The index as last read, per (path, mtime, size): the session block, the
165
- # per-turn block and the digest of one prompt each ask for it, and a hook
166
- # process serves one prompt.
167
- _TAINTED = {}
128
+ is in `keep` (the ledger's lines plus the write's new ones), then `rows`, in
129
+ one transaction. So the index never outgrows the ledger it labels. Best
130
+ effort, like a ledger row."""
131
+ tezgah_store.write_taint(taint_root(root), keep, rows)
168
132
 
169
133
 
170
134
  def tainted(root):
171
135
  """{key: source} for the lessons of the repository at `root` that a gate
172
- recorded as written in a turn that had read untrusted text (`taint_path`);
173
- {} when there is none. Read once while the file is unchanged."""
174
- path = taint_path(root)
175
- try:
176
- st = os.stat(path)
177
- except OSError:
178
- return {}
179
- key = (path, st.st_mtime_ns, st.st_size)
180
- if key not in _TAINTED:
181
- _TAINTED[key] = {str(r["key"]): str(r.get("source") or "")
182
- for r in _index_rows(path)}
183
- return _TAINTED[key]
136
+ recorded as written in a turn that had read untrusted text (`taint_root`);
137
+ {} when there is none."""
138
+ return tezgah_store.taint(taint_root(root))
184
139
 
185
140
 
186
141
  def lines(root, retired=None):
@@ -612,50 +612,40 @@ def reply_lang():
612
612
  # `authorized` row, the stand-down text) - the control rule refuses the routes
613
613
  # the gate sees, and an interpreter writing them is SECURITY.md's residual.
614
614
  STAND_DOWN = "tezgah-setup --uninstall --full\n"
615
- _LATCH = {"ledger": None, "since": None, "auth": None}
615
+ _LATCH = {"ledger": None, "since": None}
616
616
 
617
617
 
618
618
  def latch(ledger):
619
619
  """Bind this process's `off()` to one session's ledger path (None unbinds)."""
620
- _LATCH.update(ledger=ledger, since=None, auth=None)
620
+ _LATCH.update(ledger=ledger, since=None)
621
621
 
622
622
 
623
623
  def _first_ts(ledger):
624
+ import tezgah_store # deferred: an unbound process never opens the ledger
624
625
  try:
625
- with open(ledger, "rb") as fh:
626
- ts = json.loads(fh.readline()).get("ts")
627
- except (OSError, ValueError, AttributeError):
626
+ ts = json.loads(tezgah_store.evidence_first(ledger) or "null").get("ts")
627
+ except (*tezgah_store.ERRORS, ValueError, AttributeError):
628
628
  return None
629
629
  return ts if isinstance(ts, (int, float)) else None
630
630
 
631
631
 
632
632
  def _authorized(ledger):
633
- """The switch names this session's `authorized` rows carry, memoized on
634
- the ledger's size: read only when a switch is newer than the first row."""
635
- try:
636
- size = os.path.getsize(ledger)
637
- except OSError:
638
- return frozenset()
639
- memo = _LATCH["auth"]
640
- if memo and memo[0] == size:
641
- return memo[1]
633
+ """The switch names this session's `authorized` rows carry: read only when
634
+ a switch is newer than the first row."""
635
+ import tezgah_store
642
636
  names = set()
643
637
  try:
644
- with open(ledger, encoding="utf-8", errors="replace") as fh:
645
- for line in fh:
646
- if '"authorized"' not in line:
647
- continue
648
- try:
649
- row = json.loads(line)
650
- except ValueError:
651
- continue
652
- if row.get("kind") == "authorized" and isinstance(
653
- row.get("authorized"), list):
654
- names.update(n for n in row["authorized"] if isinstance(n, str))
655
- except OSError:
638
+ found = tezgah_store.evidence_rows(ledger, kind="authorized")
639
+ except tezgah_store.ERRORS:
656
640
  return frozenset()
657
- _LATCH["auth"] = (size, frozenset(names))
658
- return _LATCH["auth"][1]
641
+ for text in found:
642
+ try:
643
+ row = json.loads(text)
644
+ except ValueError:
645
+ continue
646
+ if isinstance(row, dict) and isinstance(row.get("authorized"), list):
647
+ names.update(n for n in row["authorized"] if isinstance(n, str))
648
+ return frozenset(names)
659
649
 
660
650
 
661
651
  def _honored(path, name):
@@ -40,8 +40,10 @@ call's own output.
40
40
 
41
41
  **Output.** Everything is written under `~/.cache/tezgah/replay/<run>/`, owner
42
42
  only, and never leaves the machine: `corpus.jsonl` (the items, with their
43
- inputs), `results.jsonl`, `summary.json`, and the sheet files. The sandbox HOME
44
- and the row stream are deleted when the child returns.
43
+ inputs), `results.jsonl` and the sheet files. The run's summary is its row in
44
+ the cache database's replay index (`tezgah_store.add_replay`), whose newest
45
+ row is the latest run. The sandbox HOME and the row stream are deleted when
46
+ the child returns.
45
47
  """
46
48
  import argparse
47
49
  import datetime
@@ -64,6 +66,7 @@ if HERE not in sys.path:
64
66
  sys.path.insert(0, HERE)
65
67
  import tezgah_integrity as ti # noqa: E402
66
68
  import tezgah_paths as tp # noqa: E402
69
+ import tezgah_store # noqa: E402
67
70
 
68
71
  # The one rule-label derivation; nothing in this module maps text to a rule.
69
72
  rule_of = ti._deny_rule
@@ -425,9 +428,6 @@ def child(run):
425
428
  import tezgah_gate as tg
426
429
  items = {it["i"]: it for it in _read_jsonl(os.path.join(run, "corpus.jsonl"))}
427
430
  evidence = os.path.join(ti.cache_dir(), "evidence")
428
- nudged = os.path.join(ti.cache_dir(), "nudged")
429
- os.makedirs(evidence, mode=0o700, exist_ok=True)
430
- os.makedirs(nudged, mode=0o700, exist_ok=True)
431
431
  now = [0.0]
432
432
 
433
433
  def frozen():
@@ -454,15 +454,13 @@ def child(run):
454
454
  now[0] = float(it["ts"])
455
455
  results.append(_replay_one(it, rows_of[fname], tg, seen, live_events))
456
456
  rows_of[fname].append(row)
457
- path = os.path.join(evidence, fname)
458
- with open(path, "a", encoding="utf-8") as out:
459
- out.write(json.dumps(row) + "\n")
460
- if isinstance(row.get("ts"), (int, float)):
461
- os.utime(path, (row["ts"], row["ts"]))
457
+ # the row as the live writer stored it; its own `ts` is the time the
458
+ # cross-session read (`writers_elsewhere`) windows on
459
+ ti._append(os.path.join(evidence, fname), json.dumps(row) + "\n")
462
460
  if row.get("kind") == "nudge":
463
461
  # the once-per-session nudge mark the live gate wrote with this
464
- # row (`first_nudge`): disk state the ledger records
465
- open(tg.nudge_mark(sid), "w", encoding="utf-8").close()
462
+ # row (`first_nudge`): cache state the ledger records
463
+ tezgah_store.mark("nudged", tg.nudge_key(sid))
466
464
  _write_jsonl(os.path.join(run, "results.jsonl"), results)
467
465
 
468
466
 
@@ -597,8 +595,7 @@ def run_replay(cutoff=None, since=None):
597
595
  "fidelity": fidelity(items, results),
598
596
  "race_family": family,
599
597
  "deny_runs": folds}
600
- _owner_write(os.path.join(run, "summary.json"), json.dumps(summary, indent=2) + "\n")
601
- _owner_write(os.path.join(replay_root(), "latest"), run + "\n")
598
+ tezgah_store.add_replay(_run_key(run), summary, tp.CACHE)
602
599
  return summary
603
600
 
604
601
 
@@ -671,7 +668,7 @@ def sheet(run, seed=SEED, rules=None):
671
668
  """The blind sample: `sheet.jsonl` ({set, n, text}), `sheet-key.jsonl` (the
672
669
  hidden live verdicts) and `instructions.md`. Returns (paths, counts)."""
673
670
  items = _read_jsonl(os.path.join(run, "corpus.jsonl"))
674
- summary = json.load(open(os.path.join(run, "summary.json"), encoding="utf-8"))
671
+ summary = _summary(run)
675
672
  rng = random.Random(seed)
676
673
  pools = defaultdict(list)
677
674
  # A race deny written before the ledger stored `target` cannot show its
@@ -790,7 +787,7 @@ def _rate(k, n):
790
787
 
791
788
 
792
789
  def report(run, label_files=()):
793
- summary = json.load(open(os.path.join(run, "summary.json"), encoding="utf-8"))
790
+ summary = _summary(run)
794
791
  out = {k: summary[k] for k in ("run", "cutoff", "cutoff_local", "ledgers", "candidates",
795
792
  "excluded", "items", "joined", "join",
796
793
  "stop_text_fidelity", "fidelity", "race_family",
@@ -1010,8 +1007,24 @@ def _pct(k, n):
1010
1007
  def _run_dir(arg):
1011
1008
  if arg:
1012
1009
  return arg
1013
- with open(os.path.join(replay_root(), "latest"), encoding="utf-8") as fh:
1014
- return fh.read().strip()
1010
+ run = tezgah_store.replay_latest(tp.CACHE)
1011
+ if not run:
1012
+ raise SystemExit("replay: no run yet; run `tezgah-gate replay` first")
1013
+ return run
1014
+
1015
+
1016
+ def _run_key(run):
1017
+ """A run's key in the replay index: its directory's real path, so a
1018
+ relative or linked `--run` names the run its row was written under."""
1019
+ return os.path.realpath(run)
1020
+
1021
+
1022
+ def _summary(run):
1023
+ """The summary the run in directory `run` was indexed with (`run_replay`)."""
1024
+ summary = tezgah_store.replay_summary(_run_key(run), tp.CACHE)
1025
+ if summary is None:
1026
+ raise SystemExit("replay: no indexed run at %s" % run)
1027
+ return summary
1015
1028
 
1016
1029
 
1017
1030
  def main(argv):
@@ -212,7 +212,7 @@ def rule_yield(files=None, shipped=None):
212
212
  `rule_ship_times()`. Fixture ledgers are left out by `ti.fixture_ledger`, the
213
213
  exclusion `counters_all` applies, because a probe run measures the harness
214
214
  and not use. A rule's exposure is the real ledgers last written at or after
215
- it shipped (the file's mtime), and its fires are the deny rows under its
215
+ it shipped (the newest row's `ts`), and its fires are the deny rows under its
216
216
  label in those ledgers. `mark` is `retire-candidate` at or below
217
217
  `RETIRE_FIRES` fires over at least `MIN_EXPOSURE` ledgers, `low-exposure` at
218
218
  or below `RETIRE_FIRES` over fewer (or an unknown ship date), else None.
@@ -227,9 +227,9 @@ def rule_yield(files=None, shipped=None):
227
227
  rows = ti._foreign_rows(path)
228
228
  if ti.fixture_ledger(rows):
229
229
  continue
230
- try:
231
- when = os.path.getmtime(path)
232
- except OSError:
230
+ when = max((r["ts"] for r in rows if isinstance(r.get("ts"), (int, float))),
231
+ default=None)
232
+ if when is None:
233
233
  continue
234
234
  real.append((when, Counter(_label(row.get("detail")) for row in rows
235
235
  if row.get("kind") == "deny")))
@@ -58,6 +58,7 @@ import tezgah_integrity as ti
58
58
  import tezgah_judge
59
59
  import tezgah_rank
60
60
  import tezgah_paths as tp
61
+ import tezgah_store
61
62
 
62
63
  ARM = "skill-suggest-on"
63
64
  # Below this, the turn's own gate question says no skill is wanted and nothing is
@@ -216,17 +217,8 @@ def judge(prompt, names, session_id=""):
216
217
  roots=[os.path.join(tp.PLUGIN_ROOT, "skills")]))
217
218
 
218
219
 
219
- def _path(session_id):
220
- return os.path.join(tp.cache_dir(), "skill-pick",
221
- slug(session_id) + ".json")
222
-
223
-
224
220
  def _remembered(session_id):
225
- try:
226
- with open(_path(session_id), encoding="utf-8") as fh:
227
- data = json.load(fh)
228
- except (OSError, ValueError):
229
- return {}
221
+ data = tezgah_store.doc("skill_pick", slug(session_id))
230
222
  return data if isinstance(data, dict) else {}
231
223
 
232
224
 
@@ -237,13 +229,7 @@ def _remember(session_id, digest, line):
237
229
  data[digest] = line
238
230
  for old in list(data)[:max(0, len(data) - KEPT)]:
239
231
  del data[old]
240
- path = _path(session_id)
241
- try:
242
- os.makedirs(os.path.dirname(path), exist_ok=True)
243
- with open(path, "w", encoding="utf-8") as fh:
244
- json.dump(data, fh)
245
- except OSError:
246
- pass
232
+ tezgah_store.put_doc("skill_pick", slug(session_id), data)
247
233
 
248
234
 
249
235
  def suggest(prompt, session_id=""):
@@ -617,31 +603,16 @@ def top_section(prompt, roots=None):
617
603
  return tuple(hit), covered, len(known)
618
604
 
619
605
 
620
- def _hint_path(session_id):
621
- return os.path.join(tp.cache_dir(), "skill-section-hint",
622
- slug(session_id) + ".json")
623
-
624
-
625
606
  def _shown(session_id):
626
607
  """The sections this session was already pointed at, ["path:start"]."""
627
- try:
628
- with open(_hint_path(session_id), encoding="utf-8") as fh:
629
- data = json.load(fh)
630
- except (OSError, ValueError):
631
- return []
608
+ data = tezgah_store.doc("section_hint", slug(session_id))
632
609
  return data if isinstance(data, list) else []
633
610
 
634
611
 
635
612
  def _remember_shown(session_id, shown):
636
- """Bounded and best effort, like the judgement's cache: a dir that cannot
613
+ """Bounded and best effort, like the judgement's cache: a store that cannot
637
614
  be written costs a repeated line, never the turn."""
638
- path = _hint_path(session_id)
639
- try:
640
- os.makedirs(os.path.dirname(path), exist_ok=True)
641
- with open(path, "w", encoding="utf-8") as fh:
642
- json.dump(shown[-KEPT:], fh)
643
- except OSError:
644
- pass
615
+ tezgah_store.put_doc("section_hint", slug(session_id), shown[-KEPT:])
645
616
 
646
617
 
647
618
  def section_hint(prompt, session_id="", host=None, roots=None):
@@ -24,11 +24,12 @@ import time
24
24
  import uuid
25
25
 
26
26
  import tezgah_integrity as ti
27
+ import tezgah_store
27
28
  from tezgah_paths import cache_dir, root_for
28
29
 
29
30
  # A snapshot id is exactly what `capture` generates: 12 hex chars. Validated
30
31
  # before it is used as a path segment, so an id typed at a shell cannot walk out
31
- # of the store with `..` or an absolute path and read some other file's manifest.
32
+ # of the store with `..` or an absolute path and read some other file as a blob.
32
33
  SNAPSHOT_ID = re.compile(r"\A[0-9a-f]{12}\Z")
33
34
 
34
35
  # The store is capped, because copying every file the agent touches with no
@@ -41,7 +42,9 @@ SNAPSHOT_ID = re.compile(r"\A[0-9a-f]{12}\Z")
41
42
  # mistake is most likely and most expensive, while the snapshot anyone actually
42
43
  # asks for is about a recent mistake - the oldest one is what nobody is coming
43
44
  # back for. Worst case on disk is CAP * MAX_BYTES = 400 MB; the realistic case
44
- # is a few MB, because source files are kilobytes.
45
+ # is a few MB, because source files are kilobytes. Each blob is a file in its
46
+ # own directory; the manifests are the `snapshots` rows of the cache database,
47
+ # in capture order, and that order is what eviction reads.
45
48
  #
46
49
  # MAX_BYTES per file, above which nothing is captured at all. `capture` runs
47
50
  # synchronously in the gate a host calls before the write, so copying a
@@ -61,10 +64,10 @@ HASH_MAX_BYTES = 8 * MAX_BYTES
61
64
 
62
65
 
63
66
  def _store():
64
- """Where the pre-write bytes go: beside the evidence ledger, in whichever
65
- cache dir tezgah settled on for this environment (a sandboxed host has no
66
- writable ~/.cache and falls back to temp), so all tezgah state sits under
67
- one root."""
67
+ """Where the pre-write bytes go: beside the cache database that indexes
68
+ them, in whichever cache dir tezgah settled on for this environment (a
69
+ sandboxed host has no writable ~/.cache and falls back to temp), so all
70
+ tezgah state sits under one root."""
68
71
  return os.path.join(cache_dir(), "snapshots")
69
72
 
70
73
 
@@ -76,34 +79,13 @@ def _blob(snapshot_id):
76
79
  return os.path.join(_dir(snapshot_id), "file")
77
80
 
78
81
 
79
- def _manifest(snapshot_id):
80
- return os.path.join(_dir(snapshot_id), "meta.json")
81
-
82
-
83
- def _dirs():
84
- try:
85
- return [os.path.join(_store(), n) for n in os.listdir(_store())]
86
- except OSError:
87
- return []
88
-
89
-
90
- def _evict(keep=None):
91
- """Drop snapshot directories, oldest first, until `keep` (CAP) remain.
92
-
93
- The order is each directory's own mtime, which is when its capture created
94
- it - nothing writes inside a snapshot directory afterwards. Best effort: an
95
- entry that cannot be read or removed costs the cap, never the capture the
96
- gate is waiting on."""
97
- keep = CAP if keep is None else keep
98
- try:
99
- # ns rather than the float mtime: two captures inside one tick must not
100
- # evict an arbitrary one of the pair
101
- entries = sorted((os.stat(p).st_mtime_ns, p) for p in _dirs())
102
- except OSError:
103
- return
104
- for _, path in entries[:-keep]:
105
- import shutil # deferred: ~3.7 ms of import for a path only a write takes
106
- shutil.rmtree(path, ignore_errors=True)
82
+ def _drop(snapshot_ids):
83
+ """Remove the blob directories of `snapshot_ids`. Best effort: an entry
84
+ that cannot be removed costs disk, never the capture the gate is waiting
85
+ on."""
86
+ import shutil # deferred: ~3.7 ms of import for a path only a write takes
87
+ for sid in snapshot_ids:
88
+ shutil.rmtree(_dir(sid), ignore_errors=True)
107
89
 
108
90
 
109
91
  def _hash_file(path):
@@ -222,21 +204,19 @@ def _capture_one(path, cwd, session_id):
222
204
 
223
205
 
224
206
  def _save(data, meta):
225
- """Write one blob and its manifest under a fresh id, evict past CAP, and
226
- return the id - or None, with nothing left behind, when the write failed."""
207
+ """Write one blob, index it with its manifest, evict past CAP, and return
208
+ the id - or None, with nothing left behind, when the write failed."""
227
209
  sid = uuid.uuid4().hex[:12]
228
210
  try:
229
211
  os.makedirs(_store(), mode=0o700, exist_ok=True)
230
212
  os.makedirs(_dir(sid), mode=0o700, exist_ok=True)
231
213
  with _private_open(_blob(sid)) as fh:
232
214
  fh.write(data)
233
- with _private_open(_manifest(sid)) as fh:
234
- fh.write(json.dumps(dict(meta, id=sid)).encode("utf-8"))
235
- except OSError:
236
- import shutil # deferred: see _evict
237
- shutil.rmtree(_dir(sid), ignore_errors=True) # no half a snapshot
215
+ evicted = tezgah_store.add_snapshot(sid, dict(meta, id=sid), CAP)
216
+ except tezgah_store.ERRORS:
217
+ _drop([sid]) # no half a snapshot
238
218
  return None
239
- _evict()
219
+ _drop(evicted)
240
220
  return sid
241
221
 
242
222
 
@@ -321,14 +301,13 @@ def restore(snapshot_id, force=False, expect=None):
321
301
  sid = str(snapshot_id or "")
322
302
  meta, data = None, b""
323
303
  if SNAPSHOT_ID.match(sid):
304
+ meta = next(iter(tezgah_store.snapshots(sid)), None)
324
305
  try:
325
- with open(_manifest(sid), encoding="utf-8") as fh:
326
- meta = json.load(fh)
327
306
  with open(_blob(sid), "rb") as fh:
328
307
  data = fh.read()
329
- except (OSError, ValueError):
308
+ except OSError:
330
309
  meta = None
331
- if not meta:
310
+ if not isinstance(meta, dict):
332
311
  raise SnapshotError(
333
312
  "unknown snapshot id %r: the store holds no snapshot for it (CAP "
334
313
  "keeps the newest %d, and an evicted one is gone for good)"