Compare commits
4
Commits
worktree-sugya-3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbf151957f | ||
|
|
f6651425ff | ||
|
|
94ec324cc0 | ||
|
|
ba3ff4a2f4 |
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
%% ============================================================
|
%% ============================================================
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
%% ============================================================
|
%% ============================================================
|
||||||
|
|||||||
@@ -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, _} ->
|
||||||
|
|||||||
@@ -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()]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user