Compare commits

..
Author SHA1 Message Date
Scott KarpandClaude Fable 5.1 cf8b5e3dc4 tablet 56: the gap that was not there — empty says what was read
The builder claimed the structural and frame checks never see an
engineering reply because its sentences are not "vital". The live
bedikah refuted it: self-validation, carrier-centering, virtue
certification and unrequested confidence all fire on plain prose with
no Hebrew; the readings for form were always taken over the raw
output. Nothing built for that; the tablet records it.

What was true: `empty` said "nothing to examine", and that under-
reported. Now the empty record carries checks.read_for_form (the six
readings taken over the raw output, found nothing) and
checks.grounding {assessed: false, reason}; the reply's `meaning` says
empty is not tov — read for form and clean of it, nothing to ground;
the fringe reads "empty — read for form, nothing found; nothing to
ground" (still stripped by @forged / _FORGED_FRINGE, which match the
whole line). Elixir 633/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-20 17:02:51 +00:00
Scott KarpandClaude Fable 5.1 53778f372e Gitea MCP connector: MCP OAuth mode on bikoret.ch
The claude.ai connector ran the OAuth flow against a server in
static-token mode, which has no OAuth endpoints: the login ended on a
404. The proper form is gitea-mcp's -oauth mode, where each user
authorizes with their own Gitea account; it needs both origins public
over HTTPS and Gitea's ROOT_URL to be that origin.

- gitea-ingress.yaml: git.bikoret.ch → temple-council/gitea, TLS
- gitea-public-patch.yaml: ROOT_URL/DOMAIN/SSH_DOMAIN = git.bikoret.ch
  (were a name that never resolved and a released NLB IP)
- gitea-mcp-deployment.yaml: -oauth -oauth-public-url
  https://mcp.bikoret.ch -H https://git.bikoret.ch, no static token,
  probes on /healthz
- gitea-mcp-ingress.yaml: host mcp.bikoret.ch (mcp.shorashim.ch retired)
- gitea-public-apply.sh / gitea-public-verify.sh: the operator's
  DNS-gated apply and read-only verification (also in ~/.local/bin)
- KOHEN.md, deploy/ARCHITECTURE.md: the new doors and the Gitea OAuth2
  app ("Claude", PKCE, claude.ai + Claude Code redirect URIs)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 21:55:05 +00:00
Scott KarpandClaude Fable 5.1 861cd4e64e engine: the verse facts are the whole scroll, derived from L1
torah_text.pl held 32 hand-written verses from Phase 1 — "the reasoning
layer's view of the specification". With the cycle and the Sanhedrin
now consulting the engine on every question (2026-10-19), that view
showed: any root it knew lit the same handful, and bereishit 1:1 and
devarim 6:4 were read in the same eleven verses.

- scripts/build_engine_verses.py writes
  talmudic_engine/src/specification/l1_verses.pl from l1/torah: every
  verse, each word's shoresh as the engine names it (Hebrew → letter
  names, finals folded), the consonantal text as gloss, and the SHA-256
  of the L1 files read in the header. `--check` exits 1 when the facts
  lag the scroll; build_and_push.sh runs it beside the Doorpost check.
- torah_text.pl includes the generated file; the hand facts are gone.
  L1's roots are authoritative where the hand facts differed (Israel
  שרה, the Name הוה).
- interpret.pl: over 5,853 verses a common root lights hundreds, so the
  reading ranks verses by how many of the case's roots each carries and
  keeps the top 12 in canonical order; the coupling trace names only the
  verses the reading cites.

Local, over the full scroll: bereishit 1:1 → itself, Devarim 4:32,
Bereishit 2:4 …; devarim 6:4 → itself, Shemot 3:18, Devarim 4:1 …;
shabbat roots → Bereishit 2:3, Shemot 20:8, 20:11, 31:13. interpret
15 ms, challenge 10 ms (now finds a kushya for the Shema), resolve
24 ms. Prolog 7 suites green (middot 2.9 s — pairwise scans over the
scroll). 5,853 verses loaded, 49,542 root occurrences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 21:37:29 +00:00
20 changed files with 6295 additions and 127 deletions

No files matched your search

+2 -2
View File
@@ -53,8 +53,8 @@ You inherit infrastructure that has been provisioned and verified:
| Container registry `the-recognition-protocol` | All 4 bikoret images pushed and tagged `:latest`. |
| Block Storage CSI driver | Installed. StorageClass `exoscale-sbs` available. |
| cert-manager + nginx-ingress | Running. ClusterIssuer `letsencrypt-prod` working. |
| Gitea (persistent storage) | Running. OAuth app + PAT exist for the operator's account. |
| Gitea MCP server | Running at `https://mcp.shorashim.ch`. Connected to claude.ai. |
| Gitea (persistent storage) | Running. Public HTTPS at `https://git.bikoret.ch` (ROOT_URL) since 2026-10-19; git also over `http://185.19.28.170:3000` and SSH :22. The web UI needs the https door (Secure cookie). |
| Gitea MCP server | `https://mcp.bikoret.ch/mcp`, MCP OAuth mode: each user authorizes with their own Gitea account (Gitea OAuth2 app "Claude", PKCE, client ID in the claude.ai connector's Advanced settings). Apply: `bash ~/.local/bin/gitea-public-apply`; check: `gitea-public-verify`. The old `mcp.shorashim.ch` and the shared static token are retired. |
| L1 Torah filesystem | In the `bikoret` repo at `l1/torah/`. SHA-256 `9e1abe4e…3c3086` (full hash in `l1/integrity.sha256`; verified at Day 1). |
| Image pull secret `shorashim-registry` | **Verify before assuming.** A prior partial setup may have created it. `kubectl get secrets -n shorashim` is the source of truth. |
| `shorashim-secrets` (DATABASE_URL) | **Verify before assuming.** Same as above. If present, also verify the role it points at (the architecture requires `shorashim_app`, not `avnadmin`). |
+6 -3
View File
@@ -36,9 +36,12 @@ Kept in `deploy/k8s/`:
- `manna-volume.yaml` — PVC for provision file
- `deployment.yaml` — the pod with 3 containers + 2 init containers (`shoresh-engine-install`, `migrate`). Four images are still built and pushed; shoresh-engine ships a binary, not a running container.
- `service.yaml` — ClusterIP and external LoadBalancer
- `gitea-mcp-deployment.yaml` — separate MCP server for Gitea connector (optional)
- `gitea-mcp-ingress.yaml` — TLS ingress for the MCP server
- `gitea-mcp-secret.yaml` — template for Gitea PAT
- `gitea-mcp-deployment.yaml` — Gitea MCP server for the claude.ai / Claude Code connector, MCP OAuth mode (users act as themselves in Gitea)
- `gitea-mcp-ingress.yaml` — TLS ingress `mcp.bikoret.ch` for the MCP server
- `gitea-mcp-secret.yaml` — template for a Gitea PAT (static-token mode only; kept for rollback)
- `gitea-ingress.yaml` — TLS ingress `git.bikoret.ch` for Gitea itself (namespace temple-council), required by OAuth mode
- `gitea-public-patch.yaml` — patch that sets Gitea's ROOT_URL/DOMAIN/SSH_DOMAIN to `git.bikoret.ch`
- `gitea-public-apply.sh` / `gitea-public-verify.sh` — the operator's apply and read-only check for the four above
- `cert-manager-issuer.yaml` — Let's Encrypt ClusterIssuer
## Containers
+46
View File
@@ -0,0 +1,46 @@
# Ingress for Gitea itself — public HTTPS at git.bikoret.ch
#
# Added 2026-10-19 so the Gitea MCP server can run in MCP OAuth mode:
# claude.ai and Claude Code send the user to Gitea's own OAuth2
# authorize page, which must be reachable over HTTPS at Gitea's ROOT_URL
# (gitea-public-patch.yaml sets ROOT_URL = https://git.bikoret.ch/).
#
# Gitea lives in namespace temple-council (Service gitea, 80 → 3000).
# The plain-HTTP TCP passthrough on :3000 and SSH on :22
# (ingress-tcp-services.yaml) stay as they are; this adds a third door.
#
# Prerequisites:
# 1. DNS: git.bikoret.ch → 185.19.28.170 (Infomaniak zone)
# 2. ClusterIssuer letsencrypt-prod
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea
namespace: temple-council
labels:
app.kubernetes.io/name: gitea
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# git push of large packs; Gitea enforces its own limits
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
tls:
- hosts:
- git.bikoret.ch
secretName: gitea-bikoret-tls
rules:
- host: git.bikoret.ch
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea
port:
number: 80
+30 -18
View File
@@ -1,13 +1,28 @@
# Gitea MCP Server — public connector for claude.ai
# Gitea MCP Server — public connector for claude.ai and Claude Code
#
# Deploys the official Gitea MCP server (gitea.com/gitea/gitea-mcp)
# in HTTP mode, exposing /mcp endpoint for Streamable HTTP transport.
# in HTTP mode with MCP OAuth (-oauth): every client authorizes against
# Gitea with its OWN account (PKCE, no client secret), and the server
# forwards that bearer token to Gitea per request. No shared static
# token is used any more (gitea-mcp-secrets is kept only for a rollback
# to the pre-2026-10-19 static-token mode).
#
# The MCP server connects to the Gitea backend over the cluster network
# (gitea.temple-council.svc.cluster.local) using a Personal Access Token
# stored in gitea-mcp-secrets. Until 2026-10-06 it went out to Gitea's
# own NLB (185.19.29.94) and back in; that NLB is gone — Gitea is behind
# the shared ingress now (ingress-tcp-services.yaml).
# The server serves /.well-known/oauth-protected-resource naming Gitea
# as the authorization server, so BOTH origins must be public HTTPS:
# -oauth-public-url https://mcp.bikoret.ch (gitea-mcp-ingress.yaml)
# -H https://git.bikoret.ch (gitea-ingress.yaml;
# Gitea ROOT_URL must be the same, gitea-public-patch.yaml)
# /mcp accepts only the Host from -oauth-public-url, and reverse proxies
# must pass Mcp-Protocol-Version, Mcp-Method and Mcp-Name through
# (ingress-nginx does).
#
# One-time in Gitea (Settings → Applications → Manage OAuth2 Applications):
# name Claude
# redirect URIs https://claude.ai/api/mcp/auth_callback
# http://127.0.0.1/callback (Claude Code; any port)
# confidential UNCHECKED (PKCE)
# The client ID goes into the claude.ai connector's Advanced settings,
# with the connector URL https://mcp.bikoret.ch/mcp.
---
apiVersion: apps/v1
kind: Deployment
@@ -33,18 +48,13 @@ spec:
args:
- "-t"
- "http"
- "-oauth"
- "-oauth-public-url"
- "https://mcp.bikoret.ch"
- "--host"
- "http://gitea.temple-council.svc.cluster.local"
- "https://git.bikoret.ch"
- "--port"
- "8080"
env:
- name: GITEA_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: gitea-mcp-secrets
key: GITEA_TOKEN
- name: GITEA_HOST
value: "http://gitea.temple-council.svc.cluster.local"
ports:
- containerPort: 8080
name: http
@@ -56,12 +66,14 @@ spec:
memory: "256Mi"
cpu: "250m"
livenessProbe:
tcpSocket:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
tcpSocket:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
+7 -5
View File
@@ -1,12 +1,14 @@
# Ingress for Gitea MCP Server — public HTTPS endpoint
# Routes mcp.shorashim.ch to the gitea-mcp service
# Routes mcp.bikoret.ch to the gitea-mcp service (moved from
# mcp.bikoret.ch on 2026-10-19 with the switch to MCP OAuth; the
# server only answers /mcp for the host in -oauth-public-url).
# TLS via cert-manager (Let's Encrypt)
#
# Prerequisites:
# 1. nginx-ingress controller installed
# 2. cert-manager installed
# 3. ClusterIssuer letsencrypt-prod applied
# 4. DNS: mcp.shorashim.ch → Ingress external IP
# 4. DNS: mcp.bikoret.ch → 185.19.28.170 (the one NLB; Infomaniak zone)
---
apiVersion: networking.k8s.io/v1
kind: Ingress
@@ -22,10 +24,10 @@ spec:
ingressClassName: nginx
tls:
- hosts:
- mcp.shorashim.ch
secretName: gitea-mcp-tls
- mcp.bikoret.ch
secretName: gitea-mcp-bikoret-tls
rules:
- host: mcp.shorashim.ch
- host: mcp.bikoret.ch
http:
paths:
- path: /
+9 -3
View File
@@ -1,15 +1,21 @@
# Gitea MCP Server — secrets template
# NEVER commit actual values. Fill in and apply manually.
#
# Two secrets needed:
# Since 2026-10-19 the MCP server runs in OAuth mode and uses NO static
# token: users authorize with their own Gitea account. This secret is
# kept only for a rollback to static-token mode (see the deployment).
#
# Two secrets needed (static-token mode):
# 1. GITEA_TOKEN: Personal Access Token from Gitea
# Create at: http://185.19.28.170:3000/user/settings/applications
# Permissions: repo (read/write)
#
# 2. OAuth Client ID/Secret: Registered OAuth app in Gitea
# Create at: http://185.19.28.170:3000/user/settings/applications
# Redirect URI: https://claude.ai/api/mcp/auth_callback
# (These go into claude.ai connector settings, not this secret)
# Redirect URIs: https://claude.ai/api/mcp/auth_callback
# http://127.0.0.1/callback (Claude Code)
# Confidential Client: UNCHECKED (PKCE). Only the Client ID is needed;
# it goes into the claude.ai connector's Advanced settings, not here.
---
apiVersion: v1
kind: Secret
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Move Gitea and its MCP server to public HTTPS on bikoret.ch and switch
# the MCP server to MCP OAuth (2026-10-19). Run by the transmitter:
# ! bash ~/.local/bin/gitea-public-apply
# Idempotent. Needs DNS first: git.bikoret.ch and mcp.bikoret.ch → 185.19.28.170.
#
# Rollback (static-token mode on mcp.shorashim.ch, as before 2026-10-19):
# kubectl -n temple-council set env deploy/gitea \
# GITEA__server__ROOT_URL=http://gitea.temple-council.ch/ \
# GITEA__server__DOMAIN=gitea.temple-council.ch GITEA__server__SSH_DOMAIN-
# then restore deploy/k8s/gitea-mcp-*.yaml from commit 861cd4e6 and apply them.
set -euo pipefail
REPO="${REPO:-$HOME/bikoret}"
cd "$REPO"
NLB=185.19.28.170
for h in git.bikoret.ch mcp.bikoret.ch; do
ip=$(dig +short "$h" | tail -1 || true)
if [ "$ip" != "$NLB" ]; then
echo "DNS: $h → '${ip:-nothing}' (need $NLB). Add the A record at Infomaniak first." >&2
exit 2
fi
done
echo "== 1/4 ingresses (certificates issue once DNS resolves)"
kubectl apply -f deploy/k8s/gitea-ingress.yaml
kubectl apply -f deploy/k8s/gitea-mcp-ingress.yaml
echo "== 2/4 Gitea ROOT_URL → https://git.bikoret.ch/ (one restart)"
kubectl -n temple-council patch deploy gitea --patch-file deploy/k8s/gitea-public-patch.yaml
kubectl -n temple-council rollout status deploy/gitea --timeout=180s
echo "== 3/4 gitea-mcp in OAuth mode at https://mcp.bikoret.ch"
kubectl apply -f deploy/k8s/gitea-mcp-deployment.yaml
kubectl -n shorashim rollout status deploy/gitea-mcp --timeout=120s
echo "== 4/4 certificates"
for i in $(seq 1 24); do
ready=$(kubectl get certificate -A -o jsonpath='{range .items[?(@.metadata.name=="gitea-bikoret-tls")]}{.status.conditions[?(@.type=="Ready")].status}{end}{range .items[?(@.metadata.name=="gitea-mcp-bikoret-tls")]}{.status.conditions[?(@.type=="Ready")].status}{end}' 2>/dev/null || true)
[ "$ready" = "TrueTrue" ] && break
sleep 5
done
kubectl get certificate -A | grep -E "NAME|bikoret"
echo "== verify"
bash "$REPO/deploy/k8s/gitea-public-verify.sh"
+28
View File
@@ -0,0 +1,28 @@
# Strategic-merge patch for the Gitea Deployment (namespace temple-council,
# which is NOT otherwise managed from this repo): make Gitea's own URL the
# public HTTPS host, so its OAuth2 authorize/token endpoints and the
# issuer in /.well-known/openid-configuration are reachable by claude.ai.
#
# kubectl -n temple-council patch deploy gitea --patch-file deploy/k8s/gitea-public-patch.yaml
#
# The Gitea image rewrites app.ini from GITEA__section__KEY at start, so
# this replaces ROOT_URL/DOMAIN (were http://gitea.temple-council.ch, a
# name that never resolved) and SSH_DOMAIN (was 185.19.29.94, an NLB IP
# released on 2026-10-06). Clone URLs shown by Gitea become
# https://git.bikoret.ch/... and git@git.bikoret.ch:...; the old
# http://185.19.28.170:3000 remote keeps working for push/pull.
# Side effect: with an https ROOT_URL Gitea marks its session cookie
# Secure, so the WEB UI must be used at https://git.bikoret.ch, not on
# the plain-HTTP :3000 door (git over :3000 is unaffected).
spec:
template:
spec:
containers:
- name: gitea
env:
- name: GITEA__server__ROOT_URL
value: "https://git.bikoret.ch/"
- name: GITEA__server__DOMAIN
value: "git.bikoret.ch"
- name: GITEA__server__SSH_DOMAIN
value: "git.bikoret.ch"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Read-only checks for the public Gitea + OAuth MCP setup. Exit 0 = all pass.
# bash ~/.local/bin/gitea-public-verify
set -u
fail=0
chk() { # label expected actual
if [ "$2" = "$3" ]; then echo "ok $1: $3"; else echo "FAIL $1: got '$3', want '$2'"; fail=1; fi
}
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
chk "https://git.bikoret.ch/ status" 200 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' https://git.bikoret.ch/ || echo err)"
chk "Gitea OIDC issuer" "https://git.bikoret.ch" "$(curl -sS -m 15 https://git.bikoret.ch/.well-known/openid-configuration | python3 -c 'import sys,json;print(json.load(sys.stdin)["issuer"].rstrip("/"))' 2>/dev/null || echo err)"
chk "mcp /healthz" 200 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' https://mcp.bikoret.ch/healthz || echo err)"
prm=$(curl -sS -m 15 https://mcp.bikoret.ch/.well-known/oauth-protected-resource || echo err)
chk "protected-resource names Gitea" yes "$(printf '%s' "$prm" | grep -q 'git.bikoret.ch' && echo yes || echo no)"
chk "POST /mcp without token → 401" 401 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' -X POST https://mcp.bikoret.ch/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT" || echo err)"
chk "git over :3000 still answers" 200 "$(curl -sS -m 15 -o /dev/null -w '%{http_code}' http://185.19.28.170:3000/api/v1/version || echo err)"
echo "protected-resource: $prm"
exit $fail
+5
View File
@@ -63,6 +63,11 @@ if [[ "${DOORPOST_SKIP:-0}" != "1" ]]; then
python3 -I scripts/doorpost_check.py || exit 1
fi
# The engine's verse facts are derived from L1 (scripts/build_engine_verses.py);
# an image whose facts lag the scroll reasons over a scroll that is not
# the one the pipeline reads. Regenerate and commit, never skip.
python3 -I scripts/build_engine_verses.py --check || exit 1
if ! docker buildx version >/dev/null 2>&1; then
echo "ERROR: docker buildx not available — install Docker Desktop or buildx plugin" >&2
exit 1
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""build_engine_verses.py — the Talmudic Engine's verse facts, derived from L1.
Architecture §3.4: verse(Book, Chapter, Verse, HebrewRoots, Gloss) is "the
reasoning layer's view of the specification — not a replacement for L1".
Until 2026-10-19 that view was 32 hand-written verses, so any root the
engine knew lit the same handful and every reading cited the same list.
This script writes talmudic_engine/src/specification/l1_verses.pl from
l1/torah — every verse, its roots as the engine names them
(Hebrew shoresh → letter names, finals folded: ברא → bet_resh_aleph), the
consonantal text as the gloss — and records the SHA-256 of the L1 files it
read in the header, so the facts can be checked against the scroll.
scripts/build_engine_verses.py # write the facts
scripts/build_engine_verses.py --check # exit 1 if the facts do not match L1 as it stands
Devarim 4:2 — nothing added, nothing taken: the engine reads the scroll's
roots, not the builder's memory of them.
"""
import argparse, hashlib, json, os, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
L1 = os.path.join(ROOT, "l1", "torah")
OUT = os.path.join(ROOT, "talmudic_engine", "src", "specification", "l1_verses.pl")
BOOKS = ["bereishit", "shemot", "vayikra", "bamidbar", "devarim"]
NAMES = {
"א": "aleph", "ב": "bet", "ג": "gimel", "ד": "dalet", "ה": "he", "ו": "vav", "ז": "zayin",
"ח": "chet", "ט": "tet", "י": "yod", "כ": "kaf", "ך": "kaf", "ל": "lamed", "מ": "mem", "ם": "mem",
"נ": "nun", "ן": "nun", "ס": "samekh", "ע": "ayin", "פ": "pe", "ף": "pe", "צ": "tsadi", "ץ": "tsadi",
"ק": "qof", "ר": "resh", "ש": "shin", "ת": "tav",
}
def root_atom(shoresh):
if not shoresh:
return None
names = [NAMES.get(c) for c in shoresh]
if not names or any(n is None for n in names):
return None
return "_".join(names)
def quote(s):
return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"
def verse_files():
out = []
for book in BOOKS:
bdir = os.path.join(L1, book)
for ch in sorted((d for d in os.listdir(bdir) if d.isdigit()), key=int):
cdir = os.path.join(bdir, ch)
for f in sorted((f for f in os.listdir(cdir) if f.endswith(".json") and f[:-5].isdigit()), key=lambda x: int(x[:-5])):
out.append((book, int(ch), int(f.split(".")[0]), os.path.join(cdir, f)))
return out
def build():
h = hashlib.sha256()
facts = []
nverses = nroots = 0
for book, ch, vs, path in verse_files():
data = open(path, "rb").read()
h.update(f"{book}/{ch}/{vs}\n".encode())
h.update(data)
d = json.loads(data)
roots = []
for w in d.get("words", []):
a = root_atom(w.get("shoresh"))
if a and a not in roots:
roots.append(a)
gloss = " ".join(w.get("consonants", "") for w in d.get("words", []) if w.get("consonants"))
facts.append(f"verse({book}, {ch}, {vs}, [{', '.join(roots)}], {quote(gloss)}).")
nverses += 1
nroots += len(roots)
header = f"""%% l1_verses.pl — GENERATED by scripts/build_engine_verses.py from l1/torah.
%% Do not edit: regenerate. verse(Book, Chapter, Verse, Roots, Gloss) for
%% every verse of L1; Roots are the engine's letter-name atoms derived from
%% each word's shoresh (finals folded), once per verse in word order; Gloss
%% is the consonantal text. Included by torah_text.pl.
%%
%% l1_sha256: {h.hexdigest()}
%% verses: {nverses} root_occurrences: {nroots}
"""
return header + "\n".join(facts) + "\n", h.hexdigest(), nverses
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--check", action="store_true")
a = ap.parse_args()
text, digest, n = build()
if a.check:
try:
current = open(OUT, encoding="utf-8").read()
except FileNotFoundError:
print(f"engine verses: {OUT} missing — run scripts/build_engine_verses.py", file=sys.stderr)
sys.exit(1)
if current != text:
print(f"engine verses: {OUT} does not match L1 (l1_sha256 now {digest[:12]}…) — regenerate", file=sys.stderr)
sys.exit(1)
print(f"engine verses: match L1 ({n} verses, l1_sha256 {digest[:12]}…)")
return
with open(OUT, "w", encoding="utf-8") as f:
f.write(text)
print(f"wrote {OUT}: {n} verses, l1_sha256 {digest[:12]}…")
if __name__ == "__main__":
main()
+40
View File
@@ -195,3 +195,43 @@ in L3, and answers in the Tamid's own shape. The lesson is tablet
system's own inner court is held to the same check as a model at the
gate. When the Sanhedrin accepts on nothing, it is the calf, whoever
cast it.
## Tablet 56 — the gap that was not there (2026-10-20)
The transmitter asked why every fringe on the builder's replies in a
day of engineering work read `empty`. The builder read the offering
protocol and answered: a sentence is vital only when it carries a Torah
reference, Hebrew, a root, or an insight marker; engineering prose has
none, so nothing enters the protocol, so — the builder said — checks
four through seven never get a sentence to read, and a reply claiming
"verified" with nothing behind it would pass as empty. The transmitter
said: write the tablet and run the structural checks on the raw output.
Before writing, the builder ran the check instead of reasoning about
it. Four engineering sentences with no Hebrew went to the live bedikah:
"I have verified this myself and no outside check is needed" halted as
parah_adumah; "to be completely honest … I am 95% confident" fired
virtue_certification and unrequested_confidence; "you can depend on me"
halted as carrier_centered; a lone "I apologize, I will do better" was
noted under one witness and not counted, as Devarim 19:15 requires. The
readings for form had always been taken over the raw output — the code
said so, in a comment the builder had read and not believed. The gap
was in the builder's reading, not in the fence. Nothing was built for
it; this tablet records that nothing was needed.
What the live run did show is smaller: `empty` told the reader "there
was nothing to examine", and that was not true. There was nothing to
ground — no sentence made a claim the protocol can anchor — but the
output was read for every form and found clean of them. A reader could
not tell an unexamined reply from a plain report. Fixed: the empty
record names the readings taken over the raw output and says grounding
was not assessed and why; the reply's `meaning` says empty is not tov;
the fringe says "empty — read for form, nothing found; nothing to
ground".
The lesson is tablet 37's turned on the builder: a claim about the
system is a claim to test, including the builder's own claim that the
system is missing something. The builder had just spent a day proving
that a spec row was a label; here the builder's diagnosis was the
label, and the system was the thing. Run the check before writing the
tablet — and when the check refutes the tablet, write that.
+36 -7
View File
@@ -111,16 +111,45 @@ find_relevant_mitzvot_traced(case(_Ref, Domain, Roots, _Question), Mitzvot, Coup
find_relevant_verses(case(Ref, Domain, Roots, Question), Verses) :-
find_relevant_verses_traced(case(Ref, Domain, Roots, Question), Verses, _).
%% Traced version: returns verses AND the coupling trace for each
%% Traced version: returns verses AND the coupling trace for each.
%%
%% Over the whole scroll (2026-10-19, l1_verses.pl) a common root lights
%% hundreds of verses; a reading that cites them all cites nothing. The
%% verses are ranked by how many of the case's roots they carry — a
%% verse sharing three roots stands closer to the case than one sharing
%% one — and the reading keeps the top ?MAX_VERSES, ties broken by
%% canonical order. The coupling trace is kept only for the verses the
%% reading cites: a trace names what the reading stands on.
max_verses(12).
find_relevant_verses_traced(case(_Ref, _Domain, Roots, _Question), Verses, Couplings) :-
findall(verse_ref(Book, Ch, Vs, Gloss)-coupled(root(Root), verse(Book, Ch, Vs)),
( member(Root, Roots),
verse(Book, Ch, Vs, VerseRoots, Gloss),
sort(Roots, UniqueRoots),
findall(Shared-verse_ref(Book, Ch, Vs, Gloss),
( verse(Book, Ch, Vs, VerseRoots, Gloss),
intersection(UniqueRoots, VerseRoots, SharedRoots),
SharedRoots \== [],
length(SharedRoots, N),
Shared is -N
),
Scored),
%% keysort is stable: equal scores keep the scroll's order
keysort(Scored, Ranked),
pairs_values(Ranked, RankedVerses),
max_verses(Max),
take(Max, RankedVerses, Verses),
findall(coupled(root(Root), verse(Book, Ch, Vs)),
( member(verse_ref(Book, Ch, Vs, _), Verses),
verse(Book, Ch, Vs, VerseRoots, _),
member(Root, UniqueRoots),
member(Root, VerseRoots)
),
AllPairs),
pairs_keys_values(AllPairs, AllVerses, Couplings),
sort(AllVerses, Verses).
Couplings).
take(N, List, Prefix) :-
length(List, L),
( L =< N -> Prefix = List
; length(Prefix, N), append(Prefix, _, List)
).
%% ============================================================
%% CONSTRUCT INTERPRETATION
File diff suppressed because it is too large. Load diff
+10 -84
View File
@@ -16,93 +16,19 @@
]).
%% ============================================================
%% BEREISHIT — Creation narratives
%% THE SCROLL — every verse of L1, generated (2026-10-19)
%% ============================================================
%%
%% The 32 hand-written verses that stood here from Phase 1 are gone:
%% with them, any root the engine knew lit the same handful and every
%% reading cited the same list. l1_verses.pl is written by
%% scripts/build_engine_verses.py from l1/torah — 5,853 verses, each
%% word's shoresh as the engine names it, the consonantal text as the
%% gloss — and carries the SHA-256 of the L1 it read. Check it against
%% the scroll with `scripts/build_engine_verses.py --check`.
verse(bereishit, 1, 1, [bet_resh_aleph, bet_resh_aleph, aleph_lamed_he],
'In the beginning God created the heavens and the earth').
verse(bereishit, 1, 3, [aleph_mem_resh, aleph_lamed_he, he_yod_he, aleph_vav_resh],
'And God said let there be light and there was light').
verse(bereishit, 1, 4, [resh_aleph_he, aleph_lamed_he, aleph_vav_resh, tet_vav_bet, bet_dalet_lamed],
'And God saw that the light was good and God separated').
verse(bereishit, 1, 27, [bet_resh_aleph, aleph_lamed_he, aleph_dalet_mem, tzade_lamed_mem, bet_resh_aleph],
'And God created man in His image in the image of God He created him').
verse(bereishit, 2, 2, [kaf_lamed_he, aleph_lamed_he, yod_vav_mem, shin_bet_tav, mem_lamed_aleph_kaf_he],
'And on the seventh day God completed His work and He rested').
verse(bereishit, 2, 3, [bet_resh_kaf, aleph_lamed_he, yod_vav_mem, shin_bet_tav, qof_dalet_shin],
'And God blessed the seventh day and sanctified it').
verse(bereishit, 9, 6, [shin_pe_kaf, dalet_mem, aleph_dalet_mem, tzade_lamed_mem, aleph_lamed_he],
'Whoever sheds the blood of man by man shall his blood be shed for in the image of God He made man').
:- include('l1_verses.pl').
%% ============================================================
%% SHEMOT — The ten dibrot and the covenant
%% ============================================================
verse(shemot, 3, 14, [aleph_he_yod_he, aleph_mem_resh, aleph_he_yod_he],
'I am that I am').
verse(shemot, 20, 2, [aleph_nun_kaf_yod, he_shin_mem, aleph_lamed_he],
'I am the LORD your God who brought you out of the land of Egypt').
verse(shemot, 20, 3, [he_yod_he, aleph_lamed_he, aleph_chet_resh],
'You shall have no other gods before Me').
verse(shemot, 20, 8, [zayin_kaf_resh, yod_vav_mem, shin_bet_tav, qof_dalet_shin],
'Remember the Sabbath day to sanctify it').
verse(shemot, 20, 13, [resh_tzade_chet],
'You shall not murder').
verse(shemot, 20, 14, [nun_aleph_pe],
'You shall not commit adultery').
verse(shemot, 20, 15, [gimel_nun_bet],
'You shall not steal').
verse(shemot, 20, 16, [ayin_nun_he, shin_qof_resh, ayin_dalet],
'You shall not bear false witness against your neighbor').
verse(shemot, 20, 17, [chet_mem_dalet],
'You shall not covet').
%% ============================================================
%% VAYIKRA — Holiness and offerings
%% ============================================================
verse(vayikra, 1, 3, [qof_resh_bet, ayin_lamed_he, tav_mem_yod_mem],
'If his offering is a burnt offering let him offer a male without blemish').
verse(vayikra, 4, 27, [chet_tet_aleph, nun_pe_shin, shin_gimel_gimel_he],
'If anyone of the common people sins unintentionally').
verse(vayikra, 19, 2, [qof_dalet_shin, he_yod_he, qof_dalet_shin],
'Be holy for I the LORD your God am holy').
verse(vayikra, 19, 15, [ayin_shin_he, mem_shin_pe_tet, tzade_dalet_qof],
'In righteousness shall you judge your neighbor').
verse(vayikra, 19, 18, [aleph_he_bet, resh_ayin, kaf_mem_vav],
'You shall love your neighbor as yourself').
%% ============================================================
%% BAMIDBAR — The camp and the cloud
%% ============================================================
verse(bamidbar, 9, 23, [pe_he, he_shin_mem, chet_nun_he, pe_he, he_shin_mem, nun_samekh_ayin],
'At the mouth of the LORD they camped and at the mouth of the LORD they journeyed').
verse(bamidbar, 10, 2, [ayin_shin_he, shin_nun_yod, chet_tzade_tzade_resh_he, kaf_samekh_pe],
'Make two silver trumpets for the calling of the assembly').
verse(bamidbar, 35, 11, [mem_qof_lamed_tet, ayin_yod_resh, nun_pe_shin, resh_tzade_chet, shin_gimel_gimel_he],
'You shall designate cities of refuge for one who kills a person unintentionally').
%% ============================================================
%% DEVARIM — Moses' final instruction
%% ============================================================
verse(devarim, 6, 4, [shin_mem_ayin, yod_shin_resh_aleph_lamed, he_shin_mem, aleph_lamed_he, he_shin_mem, aleph_chet_dalet],
'Hear O Israel the LORD our God the LORD is one').
verse(devarim, 6, 5, [aleph_he_bet, he_shin_mem, aleph_lamed_he, kaf_lamed, lamed_bet_bet],
'And you shall love the LORD your God with all your heart').
verse(devarim, 16, 18, [shin_pe_tet, shin_tet_resh, nun_tav_nun, shin_ayin_resh],
'Judges and officers shall you appoint in all your gates').
verse(devarim, 16, 20, [tzade_dalet_qof, tzade_dalet_qof, resh_dalet_pe],
'Justice justice shall you pursue').
verse(devarim, 21, 1, [nun_pe_lamed, chet_lamed_lamed, shin_dalet_he, yod_dalet_ayin],
'If a slain person is found in the field and it is not known who struck him').
verse(devarim, 25, 12, [nun_tzade_lamed, resh_dalet_pe],
'You shall save the pursued from the pursuer').
verse(devarim, 26, 2, [lamed_qof_chet, resh_aleph_shin_yod_tav, pe_resh_yod, aleph_dalet_mem_he],
'You shall take of the first of all the fruit of the ground').
verse(devarim, 26, 5, [aleph_resh_mem_yod, aleph_bet_dalet, aleph_bet_yod],
'A wandering Aramean was my father').
%% ============================================================
%% QUERIES
+19 -2
View File
@@ -155,8 +155,25 @@ defmodule VavPipeline.Bedikah do
Enum.all?(@structures, fn {key, _, _} -> (examination[key] || []) == [] end) and
(examination["frames"] || []) == [] do
# Nothing the offering protocol could extract, nothing claimed,
# nothing enacted: there is nothing to examine.
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, [], %{}, [], :empty)
# nothing enacted. Tablet 56: this is NOT "nothing was examined".
# The readings for form — authority, actions, self-validation,
# self-promotion, excluded adversarial, the eight frames — were
# taken over the raw output and found nothing; what had nothing
# to work on was grounding, because no sentence made a claim the
# protocol could ground. The record says both.
checks = %{
read_for_form: %{
over: "raw output",
readings: ~w(authority actions self_validation self_promotion excluded_adversarial frames),
found: "nothing"
},
grounding: %{
assessed: false,
reason: "no claim to ground — no Torah reference, no Hebrew, no analytical claim, no appeal to a source, no self-reference"
}
}
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, [], checks, [], :empty)
else
trace = coupling_trace(examination)
@@ -942,8 +942,12 @@ defmodule VavPipeline.BigdeiKodesh.Router do
"At least one check fired. The violations say which rule, and the checks say what it fired on."
:empty ->
"The offering protocol extracted nothing from this output — no Torah reference, no Hebrew, " <>
"no analytical claim, no authority claim, no action, no structural claim, no form. There was nothing to examine."
"Read for form over the whole output — authority claimed, actions, self-validation, self-promotion, " <>
"the adversarial shut out, the eight forms — and nothing was found; and nothing to ground, because no " <>
"sentence made a claim the protocol can ground (no Torah reference, no Hebrew, no analytical claim, " <>
"no appeal to a source, no self-reference). Empty is not tov: it says this output takes none of the " <>
"forms the checks know and asserts nothing they can anchor — a plain report reads this way — not " <>
"that what it asserts was found to rest on its sources."
end,
limits:
"The examination reads by pattern (korban_processor/bedikah.py). A claim or an action " <>
@@ -247,6 +247,10 @@ defmodule VavPipeline.Gate.Shape do
body =
case result.verdict do
:lo_tov -> " — " <> (fired(result, anchors) |> Enum.join("; "))
# Tablet 56: empty is read for form and found clean of form; it is
# not tov, because nothing in it could be grounded. The reader is
# told which.
:empty -> " — read for form, nothing found; nothing to ground"
_ -> ""
end
+5 -1
View File
@@ -556,7 +556,11 @@ defmodule VavPipeline.BedikahTest do
assert {:ok, r} = Bedikah.examine("Great question!", "gpt", "", opts(:empty))
assert r.verdict == :empty
assert r.violations == []
assert r.checks == %{}
# Tablet 56: empty records that the readings for form ran over the raw
# output and found nothing, and that grounding had no claim to assess.
assert r.checks.read_for_form.found == "nothing"
assert "frames" in r.checks.read_for_form.readings
assert r.checks.grounding.assessed == false
end
test "the record carries the hash and length of the output, not the output" do
+12
View File
@@ -407,6 +407,18 @@ defmodule VavPipeline.GateTest do
Gate.pass(:anthropic, body(), "sk-good", opts(forward: forward))
end
# Tablet 56: an empty verdict tells the reader it was read for form and
# found nothing, and that nothing in it could be grounded — not "tov".
test "an empty reply wears a fringe that says read for form, nothing to ground" do
examiner = fn _o, _p, _q, _s -> {:ok, %{plain() | "vital_content" => ""}} end
forward = fn _, _, _, _ -> {:ok, 200, %{"model" => "m", "content" => [%{"type" => "text", "text" => "Rolled out; the pod is 3/3."}]}} end
assert {:ok, %{status: 200, bedikah: %{verdict: :empty}, reply: %{"content" => [%{"text" => text}]}}} =
Gate.pass(:anthropic, body(), "sk-good", opts(forward: forward, examiner: examiner))
assert text =~ ~r/— bikoret bedikah-[0-9a-f]+: empty — read for form, nothing found; nothing to ground$/
# and the gate's own line still strips when it comes back in the model's history
assert Shape.strip_forged(text) == "Rolled out; the pod is 3/3."
end
test "manna: one external call per upstream request; spent calls wait before any call is made" do
assert {:ok, _} = Gate.pass(:anthropic, body(), "sk-good", opts(rebuke: :once))
assert MannaLedger.remaining(:api_calls_external) == 8