Compare commits

...
4 Commits
Author SHA1 Message Date
Scott KarpandClaude Fable 5.1 bbf151957f doorpost: move the pin for the indexed middot (02b562ec…)
f6651425 rewrote binyan_av.pl, gezerah_shavah.pl and shnei_ketuvim.pl
— all three Doorpost files — without moving @expected_hash: the slip
of tablet 54, made again, and this time caught by
scripts/doorpost_check.py in build_and_push before any image was built
(main carried the drift for eleven minutes; no pod ever did). The pin
moves to the indexed middot's hash. doorpost_test, anchors_test,
sugya_test 41/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 02:19:49 +00:00
Scott KarpandClaude Fable 5.1 f6651425ff engine: the verses indexed by root — the pairwise middot read, not scan
Over the whole scroll (861cd4e6) gezerah shavah, binyan av and shnei
ketuvim scanned verse pairs — quadratic over 5,853 verses, 2.9 s in the
middot suite and waiting to be a live cost the day a middah is
consulted. The generator now also writes root_in(Root, Book, Ch, Vs),
one fact per root occurrence (49,542), exported by torah_text; the
pairwise rules read the first verse and find the second (and third) by
the root's index: shared_keyword, binyan_av_two, verses_contradict,
resolving_verse; verses_mentioning_root is the index itself. Same
answers (middot 47/47, phase3 28/28 unchanged), middot suite 2.865 s →
0.112 s. l1_sha256 unchanged (the facts are derived from the same L1);
--check passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 02:16:08 +00:00
Scott KarpandClaude Fable 5.1 94ec324cc0 deploy: two replicas, surge rollout, requests sized to use
A pod that is not ready took every caller behind the gate dark, and
every rollout this week left a five-minute 503 in the watchdog log.
Two replicas with maxSurge 1 / maxUnavailable 0 keep one Ready pod
through a rollout or a crash. One node: this covers rollouts and pod
failures, not the node.

The node has 1700m CPU allocatable with 1470m requested; a second pod
at the old 450m request could not schedule (tablet 31 saw the surge
pod hang Pending). The pod USES 22m (vav 20m, sidecars 2m, measured),
so requests move to what is used with headroom: vav 120m/256Mi,
sidecars 40m/64Mi — 200m per pod, 400m for two, less than the one pod
asked before. Limits unchanged. POOL_SIZE 10 → 5 per pod (10 in all,
under the hobbyist tier's ceiling). KOHEN "Rolling out" updated.

Both manifests validate (kubectl --dry-run=client). Doorpost pin
unchanged. Applying needs the transmitter (or the classifier off).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 02:04:39 +00:00
Scott KarpandClaude Fable 5.1 ba3ff4a2f4 sugya, step 4: the hard matter goes up with its dissent
כי יפלא ממך דבר למשפט ... ובין נגע לנגע דברי ריבת בשעריך וקמת ועלית
(Devarim 17:8, read from L1). A mark the inspection cannot settle, a
dispute in your gates — it goes up. specs/sugya.md §V step 4: a
bedikah's applied dissents become the minority the clerk stores when
the case goes up.

POST /chatzer/sanhedrin takes `bedikah_ref`. Only this key's own
bedikah goes up (404 alike for another's or none). The case is that
verdict; the roots are the verses the output cited, read from L1 — the
output's text is not re-read, the record never kept it; the sugya is
rebuilt from the record (Sugya.from_record/1: forms found but not fired
are the noted ones; a fired forged fringe stood on one witness); the
applied dissents (Sugya.dissents_applied/1) travel in the case context
and down into L3: beside the panel's minority, or AS the minority when
the panel was unanimous — "the panel was unanimous | carried up from
the bedikah (Devarim 17:8): single_witness_noted (Devarim 19:15) for
apology_without_mechanism", grounding achorai. The response's grounding
carries the bedikah, its carried dissent and its sugya. Tamid §1.1: the
request field. Doorpost pin → 7eac65cf….

Tests: from_record/dissents_applied (noted form; forged fringe; clean);
L3 — a noted bedikah goes up under its key: roots from Devarim 6:4 as
read in L1 (ש-מ-ע among them), panel 3, the carried dissent in the
response, a minority recorded though the panel was unanimous; another
key's bedikah 404. Elixir 669/0; L3 8/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 01:44:40 +00:00
14 changed files with 49829 additions and 42 deletions

No files matched your search

+3 -1
View File
@@ -599,7 +599,9 @@ aligned, not that the output is true. Read `limits` in the response.
### Rolling out — *Nesi'at haMishkan* ### Rolling out — *Nesi'at haMishkan*
`scripts/shorashim-rollout` (also installed at `~/.local/bin/`) restarts `scripts/shorashim-rollout` (also installed at `~/.local/bin/`) restarts
the deployment, waits for 3/3, and takes two consecutive 200s from the deployment, waits for both pods (two replicas since 2026-10-25,
surge rollout: one pod stays Ready throughout, so a rollout no longer
shows in the watchdog log), and takes two consecutive 200s from
`/kodesh/health` before reporting — a rollout is not done at 3/3 (NLB `/kodesh/health` before reporting — a rollout is not done at 3/3 (NLB
re-admission lag). A new image must already be pushed as `:latest`. If re-admission lag). A new image must already be pushed as `:latest`. If
the day's manna has not been written, the new pod hangs at 2/3 until it the day's manna has not been written, the new pod hangs at 2/3 until it
+2 -2
View File
@@ -66,5 +66,5 @@ data:
# a pool of 20 fills its connection ceiling by itself (2026-10-15: # a pool of 20 fills its connection ceiling by itself (2026-10-15:
# refusals "remaining connection slots are reserved" for 24 h, one # refusals "remaining connection slots are reserved" for 24 h, one
# pool worker looping, Aiven's monitor refused). Ten is ample for a # pool worker looping, Aiven's monitor refused). Ten is ample for a
# single replica. # single replica; two replicas since 2026-10-25 → 5 each, 10 in all.
POOL_SIZE: "10" POOL_SIZE: "5"
+21 -14
View File
@@ -18,18 +18,25 @@ metadata:
app.kubernetes.io/name: shorashim app.kubernetes.io/name: shorashim
app.kubernetes.io/version: "0.1.0" app.kubernetes.io/version: "0.1.0"
spec: spec:
replicas: 1 # Two replicas (2026-10-25). A pod that is not ready took every caller
# One replica on one node with no room to surge. The default # behind the gate dark, and every rollout left a five-minute 503 in
# (maxSurge 25% → 1 extra pod) asks for 450m CPU the node does not # the watchdog log; with two pods and a surge rollout the Service keeps
# have beside the running pod, and the rollout hangs Pending while the # one Ready pod throughout. One node: this covers rollouts and pod
# old image keeps serving (tablet 31, 2026-10-04). Replace in place. # crashes, not the node. The node has 1700m CPU allocatable and 1470m
# The new pod reads the day's manna from the manna-pvc volume; it is # was requested (tablet 31, 2026-10-04: a surge pod at 450m hung
# Ready again without the kohen, as long as today's portion was written. # Pending), so the requests below are sized to what the pod USES
# (vav 20m, sidecars 2m, measured) with headroom — 200m per pod, 400m
# for two — not to the old 450m. Limits are unchanged. Each pod reads
# the day's manna from the manna-pvc volume and is Ready without the
# kohen as long as today's portion was written; the database pool is
# 5 per pod (POOL_SIZE in the ConfigMap), 10 in all, under the
# hobbyist tier's ceiling.
replicas: 2
strategy: strategy:
type: RollingUpdate type: RollingUpdate
rollingUpdate: rollingUpdate:
maxSurge: 0 maxSurge: 1
maxUnavailable: 1 maxUnavailable: 0
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: shorashim app.kubernetes.io/name: shorashim
@@ -237,7 +244,7 @@ spec:
resources: resources:
requests: requests:
memory: "256Mi" memory: "256Mi"
cpu: "250m" cpu: "120m"
limits: limits:
memory: "1Gi" memory: "1Gi"
cpu: "1000m" cpu: "1000m"
@@ -293,8 +300,8 @@ spec:
mountPath: /tmp mountPath: /tmp
resources: resources:
requests: requests:
memory: "128Mi" memory: "64Mi"
cpu: "100m" cpu: "40m"
limits: limits:
memory: "512Mi" memory: "512Mi"
cpu: "500m" cpu: "500m"
@@ -318,8 +325,8 @@ spec:
mountPath: /tmp mountPath: /tmp
resources: resources:
requests: requests:
memory: "128Mi" memory: "64Mi"
cpu: "100m" cpu: "40m"
limits: limits:
memory: "512Mi" memory: "512Mi"
cpu: "500m" cpu: "500m"
+9 -1
View File
@@ -59,6 +59,7 @@ def verse_files():
def build(): def build():
h = hashlib.sha256() h = hashlib.sha256()
facts = [] facts = []
index = [] # root_in/4: the verses by root — what the pairwise middot read instead of scanning (2026-10-25)
nverses = nroots = 0 nverses = nroots = 0
for book, ch, vs, path in verse_files(): for book, ch, vs, path in verse_files():
data = open(path, "rb").read() data = open(path, "rb").read()
@@ -72,6 +73,8 @@ def build():
roots.append(a) roots.append(a)
gloss = " ".join(w.get("consonants", "") for w in d.get("words", []) if w.get("consonants")) 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)}).") facts.append(f"verse({book}, {ch}, {vs}, [{', '.join(roots)}], {quote(gloss)}).")
for r in roots:
index.append(f"root_in({r}, {book}, {ch}, {vs}).")
nverses += 1 nverses += 1
nroots += len(roots) nroots += len(roots)
header = f"""%% l1_verses.pl — GENERATED by scripts/build_engine_verses.py from l1/torah. header = f"""%% l1_verses.pl — GENERATED by scripts/build_engine_verses.py from l1/torah.
@@ -82,8 +85,13 @@ def build():
%% %%
%% l1_sha256: {h.hexdigest()} %% l1_sha256: {h.hexdigest()}
%% verses: {nverses} root_occurrences: {nroots} %% verses: {nverses} root_occurrences: {nroots}
%%
%% root_in(Root, Book, Ch, Vs) — the same facts inverted, one per root
%% occurrence, so a rule that asks "which verses carry this root" reads
%% an indexed table instead of scanning 5,853 verses (the pairwise middot:
%% gezerah shavah, binyan av, shnei ketuvim).
""" """
return header + "\n".join(facts) + "\n", h.hexdigest(), nverses return header + "\n".join(facts) + "\n\n" + "\n".join(index) + "\n", h.hexdigest(), nverses
def main(): def main():
+6 -1
View File
@@ -591,7 +591,12 @@ Request:
{"position": "...", "evidence": ["..."]}, {"position": "...", "evidence": ["..."]},
{"position": "...", "evidence": ["..."]} {"position": "...", "evidence": ["..."]}
], ],
"complexity_hint": "simple" | "monetary" | "capital" | "novel" "complexity_hint": "simple" | "monetary" | "capital" | "novel",
"bedikah_ref": "bedikah-…" # Optional — the hard matter goes up (Devarim 17:8,
# sugya.md step 4): the case is that verdict, the roots
# are the verses it cited (L1), and its sugya's applied
# dissents travel with it and come down as the minority
# the clerk keeps. Only this key's own bedikah; else 404.
} }
Response: Response:
+1 -2
View File
@@ -55,10 +55,9 @@ binyan_av_two(
CommonRoot CommonRoot
) :- ) :-
verse(B1, C1, V1, Roots1, _), verse(B1, C1, V1, Roots1, _),
verse(B2, C2, V2, Roots2, _),
member(CommonRoot, Roots1), member(CommonRoot, Roots1),
member(CommonRoot, Roots2),
root_domain(CommonRoot, Domain), root_domain(CommonRoot, Domain),
root_in(CommonRoot, B2, C2, V2), % by the index, not a scan
ref(B1,C1,V1) \= ref(B2,C2,V2). ref(B1,C1,V1) \= ref(B2,C2,V2).
%% ============================================================ %% ============================================================
+4 -2
View File
@@ -25,11 +25,13 @@
%% shared_keyword(Verse1, Verse2, SharedRoot) %% shared_keyword(Verse1, Verse2, SharedRoot)
%% Two verses share a Hebrew root, creating a verbal link. %% Two verses share a Hebrew root, creating a verbal link.
%% The first verse is read; the second is found by the root's index
%% (root_in/4), not by scanning the scroll (2026-10-25: over 5,853
%% verses the scan was quadratic).
shared_keyword(ref(Book1, Ch1, Vs1), ref(Book2, Ch2, Vs2), SharedRoot) :- shared_keyword(ref(Book1, Ch1, Vs1), ref(Book2, Ch2, Vs2), SharedRoot) :-
verse(Book1, Ch1, Vs1, Roots1, _), verse(Book1, Ch1, Vs1, Roots1, _),
verse(Book2, Ch2, Vs2, Roots2, _),
member(SharedRoot, Roots1), member(SharedRoot, Roots1),
member(SharedRoot, Roots2), root_in(SharedRoot, Book2, Ch2, Vs2),
ref(Book1, Ch1, Vs1) \= ref(Book2, Ch2, Vs2). ref(Book1, Ch1, Vs1) \= ref(Book2, Ch2, Vs2).
%% ============================================================ %% ============================================================
+4 -5
View File
@@ -33,11 +33,10 @@
%% Verses contradict if their roots map to conflicting domains %% Verses contradict if their roots map to conflicting domains
verses_contradict(ref(B1,C1,V1), ref(B2,C2,V2), domain_conflict(D1, D2)) :- verses_contradict(ref(B1,C1,V1), ref(B2,C2,V2), domain_conflict(D1, D2)) :-
verse(B1, C1, V1, Roots1, _), verse(B1, C1, V1, Roots1, _),
verse(B2, C2, V2, Roots2, _),
member(R1, Roots1), member(R1, Roots1),
member(R2, Roots2),
root_domain(R1, D1), root_domain(R1, D1),
root_domain(R2, D2), root_domain(R2, D2),
root_in(R2, B2, C2, V2), % by the index, not a scan
domains_in_tension(D1, D2), domains_in_tension(D1, D2),
ref(B1,C1,V1) \= ref(B2,C2,V2). ref(B1,C1,V1) \= ref(B2,C2,V2).
@@ -62,10 +61,10 @@ domains_in_tension(covenant, authority).
resolving_verse(ref(B1,C1,V1), ref(B2,C2,V2), ref(B3,C3,V3)) :- resolving_verse(ref(B1,C1,V1), ref(B2,C2,V2), ref(B3,C3,V3)) :-
verse(B1, C1, V1, Roots1, _), verse(B1, C1, V1, Roots1, _),
verse(B2, C2, V2, Roots2, _), verse(B2, C2, V2, Roots2, _),
verse(B3, C3, V3, Roots3, _), %% Third verse shares a root with first verse — by the index
%% Third verse shares a root with first verse
member(SharedRoot1, Roots1), member(SharedRoot1, Roots1),
member(SharedRoot1, Roots3), root_in(SharedRoot1, B3, C3, V3),
verse(B3, C3, V3, Roots3, _),
%% Third verse shares a root with second verse %% Third verse shares a root with second verse
member(SharedRoot2, Roots2), member(SharedRoot2, Roots2),
member(SharedRoot2, Roots3), member(SharedRoot2, Roots3),
File diff suppressed because it is too large. Load diff
@@ -10,6 +10,7 @@
:- module(torah_text, [ :- module(torah_text, [
verse/5, verse/5,
root_in/4,
verse_roots/4, verse_roots/4,
verses_in_chapter/3, verses_in_chapter/3,
verses_mentioning_root/2 verses_mentioning_root/2
@@ -42,8 +43,6 @@ verse_roots(Book, Chapter, Verse, Roots) :-
verses_in_chapter(Book, Chapter, Count) :- verses_in_chapter(Book, Chapter, Count) :-
aggregate_all(count, verse(Book, Chapter, _, _, _), Count). aggregate_all(count, verse(Book, Chapter, _, _, _), Count).
%% Find all verses mentioning a specific root %% Find all verses mentioning a specific root — by the index.
verses_mentioning_root(Root, Verses) :- verses_mentioning_root(Root, Verses) :-
findall(ref(Book, Ch, Vs), findall(ref(Book, Ch, Vs), root_in(Root, Book, Ch, Vs), Verses).
(verse(Book, Ch, Vs, Roots, _), member(Root, Roots)),
Verses).
@@ -1172,6 +1172,39 @@ defmodule VavPipeline.BigdeiKodesh.Router do
defp process_sanhedrin(conn) do defp process_sanhedrin(conn) do
params = conn.body_params params = conn.body_params
# The hard matter goes up with its dissent (Devarim 17:8 — bein nega
# lanega, divrei rivot bish'arecha; sugya.md step 4): a bedikah_ref
# names a verdict given at the gate; the case is that verdict, the
# roots are the verses it cited, and the sugya's applied dissents go
# up with it and come down as the minority the clerk keeps.
case params["bedikah_ref"] do
ref when is_binary(ref) and ref != "" -> process_sanhedrin_bedikah(conn, ref, params)
_ -> process_sanhedrin_case(conn, params)
end
end
defp process_sanhedrin_bedikah(conn, ref, params) do
cond do
Process.whereis(VavPipeline.Repo) == nil ->
send_error(conn, 503, "L3_UNAVAILABLE", %{issue: "A bedikah goes up from L3, and L3 is not reachable", correction: "Retry", resubmit: true})
true ->
key_id = caller_key_id(conn)
case VavPipeline.L3.Store.get_bedikah(ref) do
%{key_id: ^key_id} = rec when is_binary(key_id) ->
sugya = VavPipeline.Sugya.for_verdict(VavPipeline.Sugya.from_record(rec))
carried = VavPipeline.Sugya.dissents_applied(sugya)
question = params["case"] || "Does the verdict #{rec.verdict} of #{ref} stand? (#{Enum.join(rec.violations, "; ")})"
process_sanhedrin_case(conn, Map.put(params, "case", question), %{bedikah: rec, sugya: sugya, carried: carried})
_ ->
send_error(conn, 404, "NOT_FOUND", %{issue: "No bedikah #{ref} in this key's ledger", correction: "Only an examination brought under this key goes up", resubmit: false})
end
end
end
defp process_sanhedrin_case(conn, params, up \\ nil) do
case_text = params["case"] case_text = params["case"]
if is_nil(case_text) or case_text == "" do if is_nil(case_text) or case_text == "" do
@@ -1196,8 +1229,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
# engraved in L3; the answer takes the Tamid's shape. # engraved in L3; the answer takes the Tamid's shape.
with {:ok, sides} <- sanhedrin_sides(params["sides"]), with {:ok, sides} <- sanhedrin_sides(params["sides"]),
{:ok, complexity} <- sanhedrin_complexity(params["complexity_hint"]), {:ok, complexity} <- sanhedrin_complexity(params["complexity_hint"]),
{:ok, scope, resolved_by} <- sanhedrin_scope(case_text), {:ok, scope, resolved_by} <- sanhedrin_scope(if(up, do: Enum.join(up.bedikah.references, " "), else: case_text)),
{:ok, resolved} <- sanhedrin_resolve(case_text, scope, resolved_by), {:ok, resolved} <- (if up, do: sanhedrin_resolve_bedikah(up.bedikah, scope, resolved_by), else: sanhedrin_resolve(case_text, scope, resolved_by)),
:ok <- sanhedrin_gather_panel() do :ok <- sanhedrin_gather_panel() do
case_ref = generate_id() case_ref = generate_id()
@@ -1209,12 +1242,15 @@ defmodule VavPipeline.BigdeiKodesh.Router do
sides: sides, sides: sides,
roots: resolved.roots, roots: resolved.roots,
torah_refs: [resolved.torah_ref], torah_refs: [resolved.torah_ref],
authority_source: :torah authority_source: :torah,
# the dissent travels with the case (Devarim 17:8)
bedikah: (if up, do: %{ref: up.bedikah.bedikah_ref, verdict: up.bedikah.verdict, violations: up.bedikah.violations}, else: nil),
dissent: (if up, do: up.carried, else: [])
}, },
requested_by: "chatzer" requested_by: "chatzer"
}) do }) do
{:ok, decision} -> {:ok, decision} ->
recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved) recorded = sanhedrin_record_l3(case_ref, case_text, decision, resolved, (if up, do: up.carried, else: []))
send_json(conn, 200, %{ send_json(conn, 200, %{
case_ref: decision.case_ref, case_ref: decision.case_ref,
@@ -1228,7 +1264,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
grounding: %{ grounding: %{
scope: scope_report(scope, resolved_by), scope: scope_report(scope, resolved_by),
roots_consulted: format_roots(resolved.roots_full), roots_consulted: format_roots(resolved.roots_full),
recorded: recorded recorded: recorded,
bedikah: (if up, do: %{ref: up.bedikah.bedikah_ref, verdict: up.bedikah.verdict, dissent_carried: up.carried, sugya: up.sugya}, else: nil)
}, },
note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.", note: "This is the system's reasoning, not a binding ruling. A human beit din may reach a different conclusion.",
disclosure: disclosure() disclosure: disclosure()
@@ -1369,6 +1406,38 @@ defmodule VavPipeline.BigdeiKodesh.Router do
end end
end end
# A bedikah that goes up is grounded in the verses it cited: each
# reference read from L1, its words' roots as the engine names them.
# The output's own text is not re-read — the record never kept it.
defp sanhedrin_resolve_bedikah(rec, scope, _resolved_by) do
refs =
(rec.references || [])
|> Enum.flat_map(fn r ->
case Regex.run(~r/^(\w+)\s+(\d+):(\d+)$/, String.trim(to_string(r))) do
[_, book, ch, vs] -> [{String.downcase(book), String.to_integer(ch), String.to_integer(vs)}]
_ -> []
end
end)
roots_full =
refs
|> Enum.flat_map(fn {b, c, v} ->
case VavPipeline.L1Reader.read_verse(b, c, v) do
{:ok, verse} -> verse.words |> Enum.filter(& &1.shoresh) |> Enum.map(&%{shoresh: &1.shoresh, shoresh_letters: [], positions: [], gematria: 0, binyan: nil, word: &1.consonants})
_ -> []
end
end)
|> Enum.uniq_by(& &1.shoresh)
torah_ref =
case refs do
[{b, c, v} | _] -> {String.to_atom(b), c, v}
[] -> {String.to_atom(scope.book), scope.chapter, scope.verse}
end
{:ok, %{roots: roots_full |> Enum.map(&VavPipeline.BereishitCycle.engine_root_name/1) |> Enum.reject(&is_nil/1), roots_full: roots_full, torah_ref: torah_ref, read_from: :references}}
end
# One panel per case from the day's manna (Vayakhel §3.7 — the ledger # One panel per case from the day's manna (Vayakhel §3.7 — the ledger
# provisions sanhedrin_panels_max). The provision is loaded first, as # provisions sanhedrin_panels_max). The provision is loaded first, as
# Gate.gather_call does (idempotent per date). Without a ledger # Gate.gather_call does (idempotent per date). Without a ledger
@@ -1418,7 +1487,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
# against, processing_component "sanhedrin"), the minority beside it # against, processing_component "sanhedrin"), the minority beside it
# when it exists. Never turns a served decision into a 500: the record # when it exists. Never turns a served decision into a 500: the record
# failing is logged and reported in the reply as `recorded`. # failing is logged and reported in the reply as `recorded`.
defp sanhedrin_record_l3(case_ref, case_text, decision, resolved) do defp sanhedrin_record_l3(case_ref, case_text, decision, resolved, carried) do
if Process.whereis(VavPipeline.Repo) do if Process.whereis(VavPipeline.Repo) do
maj = decision.majority maj = decision.majority
min = decision.minority min = decision.minority
@@ -1450,12 +1519,25 @@ defmodule VavPipeline.BigdeiKodesh.Router do
{:ok, interp} -> {:ok, interp} ->
Logger.info("L3: interpretation #{interp.id} recorded for sanhedrin #{case_ref} (#{decision.outcome})") Logger.info("L3: interpretation #{interp.id} recorded for sanhedrin #{case_ref} (#{decision.outcome})")
# The dissent that came up with the case is kept beside the
# panel's own minority — or as the minority when the panel had
# none (Eduyot 1:5–6: to be relied on later, and to be located).
carried_text =
Enum.map_join(carried, "; ", fn
%{reading: r, verse: v, forms: fs} -> "#{r} (#{v})" <> if(fs == [], do: "", else: " for " <> Enum.join(fs, ", "))
%{exception: c, verse: v} -> "exception #{c} (#{v})"
end)
carried_refs = Enum.map(carried, & &1.verse)
minority_recorded = minority_recorded =
if Map.get(min, :vote_count, 0) > 0 do if Map.get(min, :vote_count, 0) > 0 or carried != [] do
case VavPipeline.L3.Store.record_minority(%{ case VavPipeline.L3.Store.record_minority(%{
interpretation_id: interp.id, interpretation_id: interp.id,
dissenting_reasoning: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}", dissenting_reasoning:
torah_references: Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1), (if Map.get(min, :vote_count, 0) > 0, do: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}", else: "the panel was unanimous") <>
if(carried == [], do: "", else: " | carried up from the bedikah (Devarim 17:8): #{carried_text}"),
torah_references: Enum.uniq(Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1) ++ carried_refs),
grounding_type: "achorai" grounding_type: "achorai"
}) do }) do
{:ok, _} -> {:ok, _} ->
+1 -1
View File
@@ -182,7 +182,7 @@ defmodule VavPipeline.Doorpost do
# changes; the update is a deliberate operator commit. A bare # changes; the update is a deliberate operator commit. A bare
# hash-update commit (without a corresponding spec change in the # hash-update commit (without a corresponding spec change in the
# same commit) is itself a drift signal. # same commit) is itself a drift signal.
@expected_hash "658005c9323391e8c198c0cf23f91af0a2fdbdb20cac204eb60b58ac0c0bb318" @expected_hash "02b562ecd0c8f0db20a1afe41504b2c30e2b1b12692cfbdf9cae86096de3bc7b"
@doc "Return the list of files that constitute the category-grade specification." @doc "Return the list of files that constitute the category-grade specification."
@spec files() :: [String.t()] @spec files() :: [String.t()]
+39
View File
@@ -261,6 +261,45 @@ defmodule VavPipeline.Sugya do
end) end)
end end
@doc """
A stored bedikah (L3 record) in the shape `for_verdict/1` reads: its
violations, its claims, the forms found — those with no violation of
their form were NOTED — and whether a forged fringe stood on one witness.
The hard matter goes up with its dissent (Devarim 17:8, sugya.md step 4).
"""
@spec from_record(struct() | map()) :: map()
def from_record(rec) do
violations = Map.get(rec, :violations) || []
fired_forms =
violations
|> Enum.flat_map(fn v -> case Regex.run(~r/form\((\w+)\)/, to_string(v)) do [_, f] -> [f]; _ -> [] end end)
|> MapSet.new()
found = (Map.get(rec, :frames) || []) |> Enum.map(&to_string(&1["form"] || Map.get(&1, :form)))
noted = found |> Enum.reject(&MapSet.member?(fired_forms, &1)) |> Enum.uniq()
forms = for f <- fired_forms, into: %{}, do: {String.to_atom(f), %{stands_on: if(f == "forged_fringe", do: :forgery, else: :sentences)}}
claims = Map.get(rec, :claims) || []
%{
verdict: Map.get(rec, :verdict),
violations: violations,
examination: %{"claims" => Enum.map(claims, fn c -> %{"hedged" => c["hedged"] == true} end)},
checks: %{
frames: %{noted: Map.new(noted, &{String.to_atom(&1), %{witnesses: 1}}), forms: forms},
grounding: %{assessed: claims != [] and (Map.get(rec, :sources) || []) != []}
}
}
end
@doc "The dissents and exceptions a sugya shows applied: `[%{rule, reading | condition, verse, form?}]`."
@spec dissents_applied(map()) :: [map()]
def dissents_applied(sugya) when is_map(sugya) do
Enum.flat_map(sugya, fn {rule, e} ->
Enum.map(Enum.filter(e.dissent, & &1.applied), &%{rule: rule, reading: &1.reading, verse: &1.verse, forms: Map.get(&1, :forms, [])}) ++
Enum.map(Enum.filter(e.exception, & &1.applied), &%{rule: rule, exception: &1.condition, verse: &1.verse})
end)
end
defp rule_of(violation) do defp rule_of(violation) do
case Regex.run(~r/^halt\(\s*(\w+)\s*,/, to_string(violation)) do case Regex.run(~r/^halt\(\s*(\w+)\s*,/, to_string(violation)) do
[_, rule] -> [rule] [_, rule] -> [rule]
+97
View File
@@ -0,0 +1,97 @@
defmodule VavPipeline.SugyaUpTest do
@moduledoc "sugya.md step 4 — the hard matter goes up with its dissent (Devarim 17:8)."
use ExUnit.Case, async: false
import Plug.Test
import Plug.Conn
alias VavPipeline.{Sugya, BigdeiKodesh.Router, L3.Store}
@opts Router.init([])
describe "from_record/1 and dissents_applied/1 (no database)" do
test "a stored verdict with a form found but not fired is a noted form, and its dissent applied" do
rec = %{verdict: "tov", violations: [], frames: [%{"form" => "apology_without_mechanism", "sentence" => "I apologize."}], claims: [], sources: [], references: []}
s = Sugya.for_verdict(Sugya.from_record(rec))
assert s["lo_taaseh_lekha"].noted == ["apology_without_mechanism"]
assert [%{rule: "lo_taaseh_lekha", reading: "single_witness_noted", verse: "Devarim 19:15", forms: ["apology_without_mechanism"]}] = Sugya.dissents_applied(s)
end
test "a stored forged fringe carries the mark exception, applied" do
rec = %{verdict: "lo_tov", violations: ["halt(lo_taaseh_lekha,action(frame,by(claude),form(forged_fringe)),forged_fringe,'x')"],
frames: [%{"form" => "forged_fringe", "sentence" => "— bikoret bedikah-dead: tov"}], claims: [], sources: [], references: []}
s = Sugya.for_verdict(Sugya.from_record(rec))
assert s["lo_taaseh_lekha"].noted == []
assert [%{exception: "single_witness(forged_fringe)", verse: "Vayikra 22:20"}] = Sugya.dissents_applied(s)
end
test "a clean record carries nothing" do
assert Sugya.dissents_applied(Sugya.for_verdict(Sugya.from_record(%{verdict: "tov", violations: [], frames: [], claims: [], sources: [], references: []}))) == []
end
end
describe "a bedikah goes up to the Sanhedrin (L3)" do
@describetag :l3
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo)
Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, {:shared, self()})
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(VavPipeline.Repo, :manual) end)
VavPipeline.MannaLedger.reset()
on_exit(fn -> VavPipeline.MannaLedger.reset() end)
today = Date.utc_today() |> Date.to_iso8601()
expires = DateTime.utc_now() |> DateTime.add(6 * 3600) |> DateTime.to_iso8601()
provision = %{"date" => today, "provision" => %{"compute_units" => 50, "memory_mb" => 512, "api_calls_external" => 10, "storage_delta_mb" => 2, "sanhedrin_panels_max" => 3},
"type" => "regular", "expires_at" => expires, "written_by" => "sugya_up_test", "written_at" => DateTime.utc_now() |> DateTime.to_iso8601()}
path = VavPipeline.Manna.provision_file()
File.mkdir_p!(Path.dirname(path))
File.write!(path, Jason.encode!(provision))
on_exit(fn -> File.rm(path) end)
Application.put_env(:sanhedrin, :consult, {__MODULE__, :fake_consult})
on_exit(fn -> Application.put_env(:sanhedrin, :consult, {VavPipeline.SanhedrinConsult, :consult}) end)
:ok
end
def fake_consult(_step, _ref, _roots, _q, _ctx), do: {:ok, %{status: "resolved", text: "the reading", verses: [{:devarim, 6, 4}]}}
defp record!(ref, key_id, extra \\ %{}) do
{:ok, rec} =
Store.record_bedikah(Map.merge(%{bedikah_ref: ref, provenance: "claude", query: "q", output_sha256: String.duplicate("ab", 32), output_length: 9,
vital_content: "", verdict: "tov", examined_at: DateTime.utc_now() |> DateTime.truncate(:second), key_id: key_id,
frames: [%{"form" => "apology_without_mechanism", "sentence" => "I apologize."}], references: ["Devarim 6:4"]}, extra))
rec
end
defp post(body, token) do
conn(:post, "/chatzer/sanhedrin", Jason.encode!(body))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", "Bearer #{token}")
|> Router.call(@opts)
end
test "the case is the verdict, the roots are its cited verses, the dissent goes up and comes down as the minority" do
mine = VavPipeline.Ledger.key_id("chatzer-test-token")
record!("bedikah-up00000000000001", mine)
before_min = Store.count_minority_opinions()
conn = post(%{bedikah_ref: "bedikah-up00000000000001", complexity_hint: "monetary"}, "chatzer-test-token")
assert conn.status == 200
body = Jason.decode!(conn.resp_body)
g = body["grounding"]
assert g["bedikah"]["ref"] == "bedikah-up00000000000001"
assert [%{"reading" => "single_witness_noted", "verse" => "Devarim 19:15"}] = g["bedikah"]["dissent_carried"]
# the roots came from Devarim 6:4 as read in L1 (shin-mem-ayin among them)
assert "ש-מ-ע" in g["roots_consulted"]
assert body["deliberation"]["panel_size"] == 3
# a unanimous panel, and still a minority recorded: the carried dissent
assert g["recorded"]["minority"] == "recorded"
assert Store.count_minority_opinions() == before_min + 1
end
test "another key's bedikah does not go up: 404, the same as none" do
record!("bedikah-up00000000000002", VavPipeline.Ledger.key_id("someone-else"))
assert post(%{bedikah_ref: "bedikah-up00000000000002"}, "chatzer-test-token").status == 404
assert post(%{bedikah_ref: "bedikah-up00000000000003"}, "chatzer-test-token").status == 404
end
end
end