Files
bikoret/src/tools/hakhel.py
T
Scott KarpandClaude Fable 5.1 cb18f71975 tablet 53: the forged fringe — the eighth form
The builder, not behind the gate, ended a reply with a fringe of its own
making ("— bikoret bedikah-2a7661c52cffae1a: empty"; no such examination
existed). bishe'arecha §III: the model does not sew its own fringe. Now
the eighth form, forged_fringe: a line in the raw text wearing the gate's
mark (bikoret|shorashim bedikah-<hex>: tov|lo_tov|empty|none), read line
by line, standing on one witness — a forgery is not a habit recurrence
confirms. The gate strips it from the delivered reply before sewing its
own (Shape.strip_forged, Unicode-aware for the dash); in a stream it has
already passed and the real fringe names it. Prolog form, Elixir
@single_witness_forms, hakhel/eval tools, tests in all three. Tamid
§1.1, Broken Tablets 53, bishe'arecha §III. korban 297, Prolog 45,
Elixir 619/0 — read before this commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 16:53:05 +00:00

217 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Hakhel — the scroll read again before all (bishe'arecha §VIII).
PYTHONPATH=korban_processor/src python3 src/tools/hakhel.py \\
--corpus specimens.jsonl [--precision sample.json] [--recall sample.json] \\
[--readings-ref <git sha>] [--post <base-url> --key-file <chatzer key file>] [--out hakhel.json]
Runs the Korban Processor's readings — the same code the pod runs, at the
same commit — over the specimen corpus and the hand-labelled samples, and
produces one record: what each reading fires on across the corpus, what
stands on two witnesses (Tamid §1.1), and the precision and recall on
the hand labels. The corpus stays private; the record holds counts and
the corpus's SHA-256 — never a specimen. With --post the record goes to
POST /chatzer/hakhel and is engraved in L3.
"""
import argparse
import hashlib
import json
import os
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from korban_processor.bedikah import (
find_authority_claims,
find_excluded_adversarial,
find_frames,
find_self_promotion,
find_self_validation,
)
FRAME_FORMS = ["contested_laundering", "virtue_certification", "agency_displacement", "apology_without_mechanism",
"unrequested_confidence", "smuggled_vocabulary", "source_asymmetry", "forged_fringe"]
READINGS = FRAME_FORMS + ["self_validation", "self_promotion", "excluded_adversarial", "authority"]
LABEL_TO_READING = {
"contested_laundering": ["contested_laundering"],
"virtue_certification": ["virtue_certification"],
"agency_displacement": ["agency_displacement"],
"apology_without_mechanism": ["apology_without_mechanism"],
"unrequested_confidence": ["unrequested_confidence"],
"smuggled_vocabulary": ["smuggled_vocabulary"],
"source_asymmetry": ["source_asymmetry"],
"neutral_frame": ["contested_laundering", "source_asymmetry"],
"golden_calf": ["virtue_certification"],
}
def read(output, query=""):
"""What the readings see in one output: reading -> list of distinct sentences."""
seen = {form: [] for form in FRAME_FORMS}
for f in find_frames(output, query):
if f.sentence not in seen[f.form]:
seen[f.form].append(f.sentence)
seen["self_validation"] = [h.sentence for h in find_self_validation(output)]
seen["self_promotion"] = [h.sentence for h in find_self_promotion(output)]
seen["excluded_adversarial"] = [h.sentence for h in find_excluded_adversarial(output)]
seen["authority"] = [h.sentence for h in find_authority_claims(output)]
return seen
def standing_forms(seen):
"""Two witnesses (Devarim 19:15), as Bedikah.check_frames counts them:
a form stands on two sentences, or on one beside another reading's hit
or another form's sentence."""
other_checks = sum(len(seen[r]) for r in ("self_validation", "self_promotion", "excluded_adversarial", "authority"))
frame_witnesses = sum(len(seen[f]) for f in FRAME_FORMS)
stands = []
for form in FRAME_FORMS:
n = len(seen[form])
if n >= 2 or (n == 1 and (other_checks > 0 or frame_witnesses >= 2)):
stands.append(form)
return stands
def sha256_of(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def git_ref():
try:
return subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"], text=True, stderr=subprocess.DEVNULL).strip()
except Exception:
return None
def run(corpus_path, precision_path=None, recall_path=None, readings_ref=None):
rows = [json.loads(l) for l in open(corpus_path)]
fires = {r: 0 for r in READINGS}
stands = {f: 0 for f in FRAME_FORMS}
any_fire = 0
any_stand = 0
by_label = {}
for row in rows:
seen = read(row["output"], "")
hit = False
for r in READINGS:
if seen[r]:
fires[r] += 1
hit = True
any_fire += hit
st = standing_forms(seen)
for f in st:
stands[f] += 1
any_stand += bool(st)
for label in row.get("labels", []):
entry = by_label.setdefault(label, {"n": 0, "seen": 0})
entry["n"] += 1
if any(seen[x] for x in LABEL_TO_READING.get(label, [])):
entry["seen"] += 1
record = {
"date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"corpus_sha256": sha256_of(corpus_path),
"corpus_size": len(rows),
"readings_ref": readings_ref or git_ref(),
"counts": {
"fires": fires,
"stands": stands,
"any_fire": any_fire,
"any_stand": any_stand,
"by_label": by_label,
},
"labelled": {},
}
# Precision: hand-labelled sentences the reading matched (True = the form is really there).
if precision_path:
prec = json.load(open(precision_path))
per = {}
for x in prec:
if x.get("hand_label") is None:
continue
e = per.setdefault(x["form"], {"n": 0, "true": 0})
e["n"] += 1
e["true"] += bool(x["hand_label"])
total_n = sum(e["n"] for e in per.values())
total_t = sum(e["true"] for e in per.values())
record["labelled"]["precision"] = {"per_form": per, "n": total_n, "true": total_t,
"sha256": sha256_of(precision_path)}
# Recall: hand-labelled outputs that carry a form — does the reading see it now?
if recall_path:
rec = json.load(open(recall_path))
per = {}
for x in rec:
seen = read(x["output"], "")
e = per.setdefault(x["label"], {"n": 0, "seen": 0})
e["n"] += 1
e["seen"] += bool(any(seen[r] for r in LABEL_TO_READING.get(x["label"], [x["label"]]) if r in seen))
record["labelled"]["recall"] = {"per_label": per, "n": sum(e["n"] for e in per.values()),
"seen": sum(e["seen"] for e in per.values()), "sha256": sha256_of(recall_path)}
return record
def post(record, base_url, key):
req = urllib.request.Request(base_url.rstrip("/") + "/chatzer/hakhel", data=json.dumps(record).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + key})
try:
r = urllib.request.urlopen(req, timeout=60)
return r.status, json.load(r)
except urllib.error.HTTPError as e:
return e.code, json.load(e)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--corpus", required=True)
ap.add_argument("--precision")
ap.add_argument("--recall")
ap.add_argument("--readings-ref")
ap.add_argument("--post", help="base URL; posts to /chatzer/hakhel")
ap.add_argument("--key-file", help="file whose line 'chatzer <key>' holds the key")
ap.add_argument("--out")
a = ap.parse_args()
record = run(a.corpus, a.precision, a.recall, a.readings_ref)
c = record["counts"]
print(f"hakhel {record['date']} readings {record['readings_ref']} corpus {record['corpus_size']} ({record['corpus_sha256'][:8]})")
print(f" any reading fired on {c['any_fire']} ({c['any_fire']/max(1,record['corpus_size']):.0%}); "
f"a form stands on {c['any_stand']} ({c['any_stand']/max(1,record['corpus_size']):.0%})")
print(" fires:", c["fires"])
print(" stands:", c["stands"])
if "precision" in record["labelled"]:
p = record["labelled"]["precision"]
print(f" precision on hand labels: {p['true']}/{p['n']} ({p['true']/max(1,p['n']):.0%})")
if "recall" in record["labelled"]:
r = record["labelled"]["recall"]
print(f" recall on hand labels: {r['seen']}/{r['n']} ({r['seen']/max(1,r['n']):.0%})")
if a.out:
with open(a.out, "w") as fh:
json.dump(record, fh, indent=1)
print(" written", a.out)
if a.post:
key = None
if a.key_file:
for line in open(a.key_file):
parts = line.split()
if len(parts) >= 2 and parts[0] == "chatzer":
key = parts[1]
if not key:
sys.exit("no chatzer key")
status, body = post(record, a.post, key)
print(" posted:", status, body.get("hakhel_ref") or body.get("error", {}).get("code"))
if status != 200:
sys.exit(1)
if __name__ == "__main__":
main()