Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf8b5e3dc4 | ||
|
|
53778f372e | ||
|
|
861cd4e64e |
No files matched your search
@@ -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`). |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: /
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+40
@@ -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"
|
||||
@@ -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"
|
||||
Executable
+18
@@ -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
|
||||
@@ -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
|
||||
|
||||
Executable
+111
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user