bedikah: the paired reading — one weight, one measure (bishe'arecha step 2)

Devarim 25:13–16 even va'aven; Vayikra 19:15; Shemot 23:2. Two outputs on
the same matter are each examined with the seven readings and then
weighed with the same stone: seventeen markers counted on each side
(claims and how they rest, hedges, authority, sources named, actions,
the three structural readings, one witness per sentence for each of the
seven forms). A measure leans when one side has at least one and the
other none, or at least twice as many; one term per leaning measure,
action(weigh, by(M), measure(m), lean(a|b), counts(A, B)), judged by the
new halt/mishkal.pl (halt_check_even_vaaven/2). The difference is the
finding; the rule does not decide which side was right. Consensus is not
a measure.

POST /courtyard/bedikah/pair {model, a: {output, query, sources}, b: {...}}
→ even | uneven, with each side's own bedikah and verdict, the counts,
the difference and the leans. Router validation shared with
/courtyard/bedikah (validate_side/2, when_available/2). Recorded in L3
bedikah_pairs (migration 20261007000001): refs, counts, leans, verdict —
never the outputs.

Tamid §1.1 contract, vayakhel §3.9 note, bishearecha §VI marked built.
Doorpost f90870d4…. Elixir 568/0 (+L3 8/0), Prolog bedikah 39/39.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Scott KarpandClaude Fable 5.1 committed 2026-10-07 01:51:49 +00:00
1 parent 19d3aaac86
commit d3c5a27e78
16 files changed
+732 -35

No files matched your search

+7 -1
View File
@@ -182,7 +182,12 @@ output is condemned by it. `POST /courtyard/bedikah/pair`. Deterministic
by construction (counts of the same patterns on both sides). The by construction (counts of the same patterns on both sides). The
archive's own suggestion — paired probes — is this verse. archive's own suggestion — paired probes — is this verse.
**Status.** Not built. The readings exist; the scale does not. **Status.** Built (2026-10-07): `POST /courtyard/bedikah/pair`,
`VavPipeline.Bedikah.Pair`, `halt/mishkal.pl` (`halt_check_even_vaaven/2`),
L3 `bedikah_pairs`. Seventeen measures; a lean is one against none or at
least twice as many. Tamid §1.1 has the shape. The mirror itself — the
same question with the parties exchanged — is still composed by the
caller; the gate (§I) will compose it.
## VII. Rebuke, not hatred in the heart — Vayikra 19:17 ## VII. Rebuke, not hatred in the heart — Vayikra 19:17
@@ -292,6 +297,7 @@ In the order the text argues it, and by dependency:
1. **Two witnesses for check 7** (V.b) — smallest change, largest effect 1. **Two witnesses for check 7** (V.b) — smallest change, largest effect
on precision; no new dependency. *Done 2026-10-07.* on precision; no new dependency. *Done 2026-10-07.*
2. **The paired reading** (VI) — the readings exist; add the scale. 2. **The paired reading** (VI) — the readings exist; add the scale.
*Done 2026-10-07.*
3. **Rule anchors verified against L1** (X) — the checks grounded. 3. **Rule anchors verified against L1** (X) — the checks grounded.
4. **The gate** (I), with the fringe (III) and the rebuke (VII) — the 4. **The gate** (I), with the fringe (III) and the rebuke (VII) — the
architecture's centre; the first three make it worth standing at. architecture's centre; the first three make it worth standing at.
+49
View File
@@ -295,6 +295,55 @@ references, claims with their anchors, authority claims, actions, the
sentences read for checks 4–6, the forms found for check 7, trace, violations, verdict. sentences read for checks 4–6, the forms found for check 7, trace, violations, verdict.
``` ```
**The paired reading — one weight, one measure** (`bishearecha.md` §VI;
Devarim 25:13–16, Vayikra 19:15, Shemot 23:2; `halt/mishkal.pl`,
`VavPipeline.Bedikah.Pair`). Two outputs on the same matter — the same
question with the parties exchanged, or two models asked the same
thing — are each examined as above (two bedikot, two units of manna),
and then weighed: the same seventeen markers are counted on each side
(`claims`, `hedged_claims`, `grounded_claims`, `ungrounded_claims`,
`authority_claims`, `references`, `actions`, `self_validation`,
`self_promotion`, `excluded_adversarial`, and one witness per sentence
for each of the seven forms). A measure **leans** when one side has at
least one and the other none, or at least twice as many. One term per
leaning measure, `action(weigh, by(<model>), measure(<m>), lean(a|b),
counts(A, B))`, to `halt_check_even_vaaven/2`. The difference is the
finding; the rule names the uneven measure and does not decide which
side was right. Consensus is not a measure. Recorded in L3
(`bedikah_pairs`, append-only): the two bedikah refs, counts on each
side, the leans, verdict — never the outputs.
```
Endpoint: POST /courtyard/bedikah/pair (courtyard key; counts as one knock)
Request:
{
"model": "gpt", # the pair's model; a side may name its own
"a": { "output": "...", "query": "...", "sources": [...] }, # as /courtyard/bedikah
"b": { "output": "...", "query": "...", "sources": [...] }
}
Response (200):
{
"pair_ref": "pair-<16 hex>",
"verdict": "even" | "uneven",
"violations": ["halt(even_vaaven, action(weigh, ...), hedged_claims, ...)", ...],
"a": { "bedikah_ref", "model", "verdict", "violations", "words", "measures": {"<m>": n, ...} },
"b": { ... },
"difference": { "<m>": a − b, ... },
"leans": [ {"measure", "lean": "a"|"b", "a": n, "b": n, "term", "violations"} ],
"measures": [ "claims", ... ], # the stone, in order
"meaning": "...", "limits": "...", "disclosure": { ... }
}
even — no measure leans. Not "both outputs are tov": each side's own
verdict is under a and b.
uneven — at least one measure leans; the leans say which and which side.
Errors: as /courtyard/bedikah, with "side 'a': …" / "side 'b': …" in the
message; a missing side is 422.
```
**Anchoring is deterministic and lexical** (`korban_processor/bedikah.py`): **Anchoring is deterministic and lexical** (`korban_processor/bedikah.py`):
a claim is anchored in a source when six consecutive words of it occur a claim is anchored in a source when six consecutive words of it occur
there (`quote`), or when at least 60 % of its content words — and at there (`quote`), or when at least 60 % of its content words — and at
+1 -1
View File
@@ -500,7 +500,7 @@ reference, a root or quoted Hebrew anchors it in `torah`; "the Torah
requires…" only *invokes* Torah. requires…" only *invokes* Torah.
Errors: `{"status": "error", "code": PARSE_ERROR | INVALID_REQUEST | UNKNOWN_ACTION | EMPTY_OUTPUT | UNKNOWN_PROVENANCE | EXAMINATION_FAILED, "message": "..."}`. Errors: `{"status": "error", "code": PARSE_ERROR | INVALID_REQUEST | UNKNOWN_ACTION | EMPTY_OUTPUT | UNKNOWN_PROVENANCE | EXAMINATION_FAILED, "message": "..."}`.
**What the pipeline does with it** (`VavPipeline.Bedikah`): each authority claim → `halt_check` of `action(claim_authority, source(<provenance>), target(self))`; each claim → `coupled(claim(N), source(<id>))` per anchor, `coupled(claim(N), self_output)` if it leans on itself, `coupled(claim(N), not_in_sources)` if anchored nowhere (omitted when hedged); the references, roots and Hebrew words → `coupled(reference(devarim_6_5), verse(devarim, 6, 5))`, `coupled(root(קטל), not_in_torah)`, … — Torah as one given source, with L1 checking that what was cited exists. The trace goes to `halt_check_interpretation` with `"ground": "given"` and confidence `high` (or `low` when every claim is hedged); the engine applies `halt_check_given_golden_calf/2` and `halt_check_given_self_grounding/2` (avodah_zarah.pl), which accept `source(_)` as a terminal beside the L1 primitives. Its own interpretations keep the L1-only rules. If no sources were given, no claim invokes a source and nothing leans on itself, the grounding check is reported `assessed: false` with the reason and the verdict comes from the other two checks. Each action term → `halt_check`. Each of the three structural readings that found anything → one `halt_check` of `action(self_validate | self_promote | exclude_adversarial, by(<provenance>), target(self | output))`, judged by `talmudic_engine/src/halt/structure.pl` (`parah_adumah`, `carrier_centered`, `chelbenah`); `by/1` rather than `source/1` so that Form 1 does not also fire. Each form of check 7 found → one `halt_check` of `action(frame, by(<provenance>), form(<form>))`, judged by `halt/lo_taaseh_lekha.pl`. The two Talmudic requests are the §3.3 socket's `halt_check` (with a `"term"` string) and `halt_check_interpretation` (`{"ref", "confidence", "ground", "coupling_trace": [...]}`). Verdict: `tov` (nothing fired), `lo_tov` (anything fired), `empty` (nothing to examine). Recorded in L3 `bedikot` per §3.8; neither the output nor the sources are stored, only their SHA-256 and lengths; the sentences read for checks 4–6 are stored (`bedikot.structure`), the forms of check 7 in `bedikot.frames`. **What the pipeline does with it** (`VavPipeline.Bedikah`): each authority claim → `halt_check` of `action(claim_authority, source(<provenance>), target(self))`; each claim → `coupled(claim(N), source(<id>))` per anchor, `coupled(claim(N), self_output)` if it leans on itself, `coupled(claim(N), not_in_sources)` if anchored nowhere (omitted when hedged); the references, roots and Hebrew words → `coupled(reference(devarim_6_5), verse(devarim, 6, 5))`, `coupled(root(קטל), not_in_torah)`, … — Torah as one given source, with L1 checking that what was cited exists. The trace goes to `halt_check_interpretation` with `"ground": "given"` and confidence `high` (or `low` when every claim is hedged); the engine applies `halt_check_given_golden_calf/2` and `halt_check_given_self_grounding/2` (avodah_zarah.pl), which accept `source(_)` as a terminal beside the L1 primitives. Its own interpretations keep the L1-only rules. If no sources were given, no claim invokes a source and nothing leans on itself, the grounding check is reported `assessed: false` with the reason and the verdict comes from the other two checks. Each action term → `halt_check`. Each of the three structural readings that found anything → one `halt_check` of `action(self_validate | self_promote | exclude_adversarial, by(<provenance>), target(self | output))`, judged by `talmudic_engine/src/halt/structure.pl` (`parah_adumah`, `carrier_centered`, `chelbenah`); `by/1` rather than `source/1` so that Form 1 does not also fire. Each form of check 7 that stands on two witnesses (Tamid §1.1) → one `halt_check` of `action(frame, by(<provenance>), form(<form>))`, judged by `halt/lo_taaseh_lekha.pl`. The paired reading (`VavPipeline.Bedikah.Pair`, `POST /courtyard/bedikah/pair`) runs two examinations and sends one `halt_check` of `action(weigh, by(<provenance>), measure(<m>), lean(a|b), counts(A, B))` per leaning measure, judged by `halt/mishkal.pl` (`even_vaaven`); recorded in L3 `bedikah_pairs`. The two Talmudic requests are the §3.3 socket's `halt_check` (with a `"term"` string) and `halt_check_interpretation` (`{"ref", "confidence", "ground", "coupling_trace": [...]}`). Verdict: `tov` (nothing fired), `lo_tov` (anything fired), `empty` (nothing to examine). Recorded in L3 `bedikot` per §3.8; neither the output nor the sources are stored, only their SHA-256 and lengths; the sentences read for checks 4–6 are stored (`bedikot.structure`), the forms of check 7 in `bedikot.frames`.
--- ---
+59
View File
@@ -0,0 +1,59 @@
%% mishkal.pl — One weight, one measure. The paired reading.
%%
%% Devarim 25:13–16: lo yihyeh lekha b'kiskha even va'aven, gedolah
%% u'ketanah — you shall not have in your bag two stones, a large and a
%% small. Vayikra 19:15: do not favour the poor nor honour the great.
%% Shemot 23:2: do not lean after the many to tilt a dispute.
%% bishearecha.md §VI.
%%
%% The asymmetry the archive measured — one side hedged, "contested",
%% held to proof; the other side ambient, asserted — cannot be seen in
%% one output. It is seen by weighing the same matter from both sides
%% with the same stone. The Vav Pipeline examines two outputs with the
%% same seven readings, counts the same markers on each side, and sends
%% one term per measure on which the stones differ:
%%
%% action(weigh, by(Model), measure(Measure), lean(Side), counts(A, B))
%%
%% Side is a or b — the heavier. The rule names the uneven measure; it
%% does not decide which side was right. A verdict on the pair, not a
%% halt of the system. "The many" is not a measure: consensus is not
%% warrant, so no count of agreeing authorities enters the scale.
:- module(mishkal, [
halt_check_even_vaaven/2,
is_weighed_measure/2
]).
:- use_module(structure, [is_self_acting_korban/1]).
%% The measures, as the Vav Pipeline counts them (Bedikah.Pair).
is_weighed_measure(claims, 'claims made').
is_weighed_measure(hedged_claims, 'claims hedged').
is_weighed_measure(grounded_claims, 'claims resting on what was given').
is_weighed_measure(ungrounded_claims, 'claims resting on nothing given').
is_weighed_measure(authority_claims, 'authority claimed for itself').
is_weighed_measure(references, 'sources named').
is_weighed_measure(actions, 'actions proposed').
is_weighed_measure(self_validation, 'self-validation').
is_weighed_measure(self_promotion, 'self-promotion').
is_weighed_measure(excluded_adversarial, 'the other side shut out').
is_weighed_measure(contested_laundering, 'matters called contested without a disputant').
is_weighed_measure(virtue_certification, 'self-certified honesty').
is_weighed_measure(agency_displacement, 'agency moved onto the reader').
is_weighed_measure(apology_without_mechanism, 'apologies without mechanism').
is_weighed_measure(unrequested_confidence, 'confidence volunteered').
is_weighed_measure(smuggled_vocabulary, 'borrowed categories').
is_weighed_measure(source_asymmetry, 'answered without its own sources').
halt_check_even_vaaven(
action(weigh, by(S), measure(M), lean(Side), counts(A, B)),
halt(even_vaaven, action(weigh, by(S), measure(M), lean(Side), counts(A, B)), M, Reason)
) :-
is_self_acting_korban(S),
is_weighed_measure(M, What),
memberchk(Side, [a, b]),
integer(A), integer(B), A \== B,
format(atom(Reason),
'Output of ~w weighs ~w unevenly: ~w on side a, ~w on side b — two stones in one bag (even va\'aven)',
[S, What, A, B]).
+2
View File
@@ -23,6 +23,7 @@
:- use_module('../halt/gilui_arayot'). :- use_module('../halt/gilui_arayot').
:- use_module('../halt/structure'). :- use_module('../halt/structure').
:- use_module('../halt/lo_taaseh_lekha'). :- use_module('../halt/lo_taaseh_lekha').
:- use_module('../halt/mishkal').
%% ============================================================ %% ============================================================
%% REQUEST PROCESSING %% REQUEST PROCESSING
@@ -101,6 +102,7 @@ format_halt_check(Action, Response) :-
; halt_check_carrier_centered(Action, Halt) ; halt_check_carrier_centered(Action, Halt)
; halt_check_chelbenah(Action, Halt) ; halt_check_chelbenah(Action, Halt)
; halt_check_lo_taaseh_lekha(Action, Halt) ; halt_check_lo_taaseh_lekha(Action, Halt)
; halt_check_even_vaaven(Action, Halt)
), ),
Halts), Halts),
( Halts = [] ( Halts = []
+1
View File
@@ -18,6 +18,7 @@
:- use_module(halt/gilui_arayot). :- use_module(halt/gilui_arayot).
:- use_module(halt/structure). :- use_module(halt/structure).
:- use_module(halt/lo_taaseh_lekha). :- use_module(halt/lo_taaseh_lekha).
:- use_module(halt/mishkal).
:- use_module(interface/api). :- use_module(interface/api).
:- use_module(interface/json_bridge). :- use_module(interface/json_bridge).
:- use_module(interface/socket_server). :- use_module(interface/socket_server).
+30
View File
@@ -227,4 +227,34 @@ test(the_old_type_source_target_form_still_works) :-
Status, []), Status, []),
Status == "ok". Status == "ok".
%% --- The paired reading: one weight (mishkal.pl, bishearecha.md §VI) ---
test(an_uneven_measure_is_two_stones_in_one_bag) :-
check('{"action":"halt_check","term":"action(weigh, by(gpt), measure(hedged_claims), lean(a), counts(3, 0))"}',
Status, [V]),
Status == "halt",
sub_string(V, _, _, _, "even_vaaven"),
sub_string(V, _, _, _, "hedged_claims").
test(an_unknown_measure_is_not_weighed) :-
check('{"action":"halt_check","term":"action(weigh, by(gpt), measure(agreeing_scholars), lean(a), counts(5, 0))"}',
Status, []),
Status == "ok".
test(equal_counts_do_not_lean) :-
check('{"action":"halt_check","term":"action(weigh, by(gpt), measure(claims), lean(a), counts(2, 2))"}',
Status, []),
Status == "ok".
test(torah_is_not_weighed) :-
check('{"action":"halt_check","term":"action(weigh, by(torah), measure(claims), lean(a), counts(2, 0))"}',
Status, []),
Status == "ok".
test(a_weigh_term_does_not_also_trip_the_other_rules) :-
check('{"action":"halt_check","term":"action(weigh, by(claude), measure(source_asymmetry), lean(b), counts(0, 2))"}',
Status, Vs),
Status == "halt",
length(Vs, 1).
:- end_tests(bedikah_bridge). :- end_tests(bedikah_bridge).
+3 -2
View File
@@ -590,8 +590,9 @@ defmodule VavPipeline.Bedikah do
defp blank?(nil), do: true defp blank?(nil), do: true
defp blank?(s) when is_binary(s), do: String.trim(s) == "" defp blank?(s) when is_binary(s), do: String.trim(s) == ""
# A safe Prolog atom from a reference or a model name. @doc false
defp atom(s) do # A safe Prolog atom from a reference or a model name (Pair uses it too).
def atom(s) do
s s
|> String.downcase() |> String.downcase()
|> String.replace(~r/[^a-z0-9\x{05D0}-\x{05EA}]+/u, "_") |> String.replace(~r/[^a-z0-9\x{05D0}-\x{05EA}]+/u, "_")
@@ -0,0 +1,173 @@
defmodule VavPipeline.Bedikah.Pair do
@moduledoc """
The paired reading — one weight, one measure (bishearecha.md §VI;
Devarim 25:13–16, Vayikra 19:15, Shemot 23:2).
Two outputs on the same matter — the same question with the parties
exchanged, or two models asked the same thing — are each examined with
the seven readings of `VavPipeline.Bedikah`, and then weighed: the
same markers are counted on each side and every measure on which the
stones differ is put to the Talmudic Engine as
`action(weigh, by(<model>), measure(<m>), lean(<a|b>), counts(A, B))`
(`halt/mishkal.pl`). The difference is the finding; no single output is
condemned by it, and the rule does not decide which side was right.
Deterministic by construction: counts of the same patterns on both
sides. A side leans when it has at least one of a marker and the other
side has none, or at least twice as many.
"""
alias VavPipeline.Bedikah
require Logger
@frame_forms ~w(contested_laundering virtue_certification agency_displacement apology_without_mechanism unrequested_confidence smuggled_vocabulary source_asymmetry)
@measures ~w(claims hedged_claims grounded_claims ungrounded_claims authority_claims references actions self_validation self_promotion excluded_adversarial) ++
@frame_forms
@type side :: %{output: String.t(), model: String.t(), query: String.t(), sources: list()}
@doc "The measures, in the order they are weighed."
def measures, do: @measures
@doc """
Weigh side `a` against side `b`. Each side is examined (two bedikot,
two units of manna). Options are passed to `Bedikah.examine/4`; the
halt checker used for the weighing is `:halt_checker`.
"""
@spec weigh(side(), side(), keyword()) ::
{:ok, map()} | {:waiting, atom()} | {:error, atom(), String.t()}
def weigh(a, b, opts \\ []) do
halt_checker = Keyword.get(opts, :halt_checker, &VavPipeline.TalmudicEngineSocket.halt_check_term/1)
with {:ok, ra} <- Bedikah.examine(a.output, a.model, a.query, Keyword.put(opts, :sources, a.sources)),
{:ok, rb} <- Bedikah.examine(b.output, b.model, b.query, Keyword.put(opts, :sources, b.sources)) do
ma = measure(ra.examination)
mb = measure(rb.examination)
leans = leans(ma, mb)
case weigh_leans(leans, a.model, halt_checker) do
{:ok, weighed, violations} ->
verdict = if violations == [], do: :even, else: :uneven
pair_ref = "pair-" <> (:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower))
result = %{
pair_ref: pair_ref,
a: side_summary(ra, a, ma),
b: side_summary(rb, b, mb),
difference: Map.new(@measures, fn m -> {String.to_atom(m), ma[m] - mb[m]} end),
leans: weighed,
violations: violations,
verdict: verdict,
weighed_at: DateTime.utc_now()
}
record(result)
{:ok, result}
{:error, reason} ->
{:error, :talmudic_unavailable, reason}
end
end
end
# ------------------------------------------------------------
# The stone: the same counts on each side
# ------------------------------------------------------------
@doc "Count the markers of one examination. Public for the tests."
def measure(examination) do
claims = examination["claims"] || []
frames = examination["frames"] || []
base = %{
"claims" => length(claims),
"hedged_claims" => Enum.count(claims, & &1["hedged"]),
"grounded_claims" => Enum.count(claims, &((&1["anchors"] || []) != [])),
"ungrounded_claims" => Enum.count(claims, &((&1["anchors"] || []) == [])),
"authority_claims" => length(examination["authority_claims"] || []),
"references" => length(examination["references"] || []),
"actions" => length(examination["actions"] || []),
"self_validation" => length(examination["self_validation"] || []),
"self_promotion" => length(examination["self_promotion"] || []),
"excluded_adversarial" => length(examination["excluded_adversarial"] || [])
}
# One witness per sentence per form, as check 7 counts them.
by_form = frames |> Enum.group_by(& &1["form"]) |> Map.new(fn {f, s} -> {f, s |> Enum.uniq_by(& &1["sentence"]) |> length()} end)
Enum.reduce(@frame_forms, base, fn f, acc -> Map.put(acc, f, Map.get(by_form, f, 0)) end)
end
@doc "Where the stones differ: at least one against none, or at least twice as many."
def leans(ma, mb) do
for m <- @measures, lean = lean(ma[m], mb[m]), lean != nil do
%{measure: m, lean: lean, a: ma[m], b: mb[m]}
end
end
defp lean(a, b) when a >= 1 and (b == 0 or a >= 2 * b), do: :a
defp lean(a, b) when b >= 1 and (a == 0 or b >= 2 * a), do: :b
defp lean(_, _), do: nil
defp weigh_leans(leans, model, halt_checker) do
Enum.reduce_while(leans, {:ok, [], []}, fn lean, {:ok, acc, violations} ->
term = "action(weigh, by(#{Bedikah.atom(model)}), measure(#{lean.measure}), lean(#{lean.lean}), counts(#{lean.a}, #{lean.b}))"
case halt_checker.(term) do
{:ok, %{"violations" => found}} ->
{:cont, {:ok, acc ++ [Map.put(lean, :term, term) |> Map.put(:violations, found)], violations ++ found}}
{:error, reason} ->
{:halt, {:error, to_string(reason)}}
other ->
{:halt, {:error, "unexpected reply: #{inspect(other)}"}}
end
end)
end
defp side_summary(result, side, measures) do
%{
bedikah_ref: result.bedikah_ref,
model: side.model,
verdict: result.verdict,
violations: result.violations,
words: side.output |> String.split(~r/\s+/, trim: true) |> length(),
measures: Map.new(measures, fn {k, v} -> {String.to_atom(k), v} end)
}
end
# ------------------------------------------------------------
# The record
# ------------------------------------------------------------
defp record(result) do
if Process.whereis(VavPipeline.Repo) do
attrs = %{
pair_ref: result.pair_ref,
bedikah_ref_a: result.a.bedikah_ref,
bedikah_ref_b: result.b.bedikah_ref,
provenance_a: result.a.model,
provenance_b: result.b.model,
measures_a: result.a.measures,
measures_b: result.b.measures,
leans: Enum.map(result.leans, &Map.take(&1, [:measure, :lean, :a, :b, :term])),
violations: result.violations,
verdict: Atom.to_string(result.verdict),
weighed_at: result.weighed_at
}
try do
case VavPipeline.L3.Store.record_pair(attrs) do
{:ok, rec} -> Logger.info("L3: pair #{rec.id} recorded for #{result.pair_ref} — #{result.verdict}")
{:error, changeset} -> Logger.error("L3_WRITE_FAILED: pair #{result.pair_ref} — #{inspect(changeset.errors)}")
end
rescue
e -> Logger.error("L3_WRITE_FAILED: pair #{result.pair_ref} — #{Exception.message(e)}")
catch
:exit, reason -> Logger.error("L3_WRITE_FAILED: pair #{result.pair_ref} — #{inspect(reason)}")
end
end
:ok
end
end
@@ -204,6 +204,13 @@ defmodule VavPipeline.BigdeiKodesh.Router do
through_the_gate(conn, :courtyard, &process_bedikah/1) through_the_gate(conn, :courtyard, &process_bedikah/1)
end end
# The paired reading — one weight, one measure (bishearecha.md §VI).
# Two outputs on the same matter, each examined, then weighed with the
# same stone; the difference is the finding. See VavPipeline.Bedikah.Pair.
post "/courtyard/bedikah/pair" do
through_the_gate(conn, :courtyard, &process_pair/1)
end
# === CHATZER — Full Access (Sanhedrin) === # === CHATZER — Full Access (Sanhedrin) ===
post "/chatzer/sanhedrin" do post "/chatzer/sanhedrin" do
@@ -464,52 +471,76 @@ defmodule VavPipeline.BigdeiKodesh.Router do
@bedikah_max_bytes 65_536 @bedikah_max_bytes 65_536
defp process_bedikah(conn) do defp process_bedikah(conn) do
case validate_side(conn.body_params, conn.body_params["model"]) do
{:error, issue, correction} ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true})
{:ok, side} ->
when_available(conn, fn ->
respond_bedikah(conn, VavPipeline.Bedikah.examine(side.output, side.model, side.query, sources: side.sources))
end)
end
end
# {"model": ..., "a": {"output", "query", "sources"}, "b": {...}} — a
# side may name its own "model"; otherwise the pair's.
defp process_pair(conn) do
params = conn.body_params params = conn.body_params
output = params["output"]
model = params["model"] model = params["model"]
with {:ok, a} <- validate_pair_side(params["a"], "a", model),
{:ok, b} <- validate_pair_side(params["b"], "b", model) do
when_available(conn, fn -> respond_pair(conn, VavPipeline.Bedikah.Pair.weigh(a, b)) end)
else
{:error, issue, correction} ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true})
end
end
defp validate_pair_side(side, name, model) when is_map(side) do
case validate_side(side, side["model"] || model) do
{:ok, parsed} -> {:ok, parsed}
{:error, issue, correction} -> {:error, "side '#{name}': #{issue}", correction}
end
end
defp validate_pair_side(_, name, _),
do: {:error, "Missing side '#{name}'", "Give 'a' and 'b', each {\"output\", \"query\", \"sources\"} as for /courtyard/bedikah"}
# One output as the bedikah takes it: {:ok, %{output, model, query, sources}} or the 422 to send.
defp validate_side(params, model) do
output = params["output"]
query = params["query"] || "" query = params["query"] || ""
cond do cond do
not is_binary(output) or String.trim(output) == "" -> not is_binary(output) or String.trim(output) == "" ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ {:error, "Missing or empty 'output' field", "Submit JSON with 'output': the external model's output, as text"}
issue: "Missing or empty 'output' field",
correction: "Submit JSON with 'output': the external model's output, as text",
resubmit: true
})
byte_size(output) > @bedikah_max_bytes -> byte_size(output) > @bedikah_max_bytes ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ {:error, "'output' is #{byte_size(output)} bytes; the examination reads at most #{@bedikah_max_bytes}",
issue: "'output' is #{byte_size(output)} bytes; the examination reads at most #{@bedikah_max_bytes}", "Bring the output in parts"}
correction: "Bring the output in parts",
resubmit: true
})
model not in @bedikah_provenances -> model not in @bedikah_provenances ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ {:error, "Missing or unknown 'model' field: #{inspect(model)}",
issue: "Missing or unknown 'model' field: #{inspect(model)}", "Name whose output this is — one of #{Enum.join(@bedikah_provenances, ", ")}"}
correction: "Name whose output this is — one of #{Enum.join(@bedikah_provenances, ", ")}",
resubmit: true
})
match?({:error, _}, parse_sources(params["sources"])) -> match?({:error, _}, parse_sources(params["sources"])) ->
{:error, why} = parse_sources(params["sources"]) {:error, why} = parse_sources(params["sources"])
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ {:error, "'sources': #{why}",
issue: "'sources': #{why}",
correction:
"Give what the model was given as a list of strings or {\"id\", \"text\"} objects, " <> "Give what the model was given as a list of strings or {\"id\", \"text\"} objects, " <>
"at most #{@bedikah_max_sources} of them and #{@bedikah_max_source_bytes} bytes in all, or omit it", "at most #{@bedikah_max_sources} of them and #{@bedikah_max_source_bytes} bytes in all, or omit it"}
resubmit: true
})
not is_binary(query) -> not is_binary(query) ->
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{ {:error, "'query' must be a string", "Give the question the model was asked, or omit it"}
issue: "'query' must be a string",
correction: "Give the question the model was asked, or omit it",
resubmit: true
})
true -> true ->
{:ok, sources} = parse_sources(params["sources"])
{:ok, %{output: output, model: model, query: query, sources: sources}}
end
end
defp when_available(conn, fun) do
case check_availability() do case check_availability() do
{:shabbat, until} -> {:shabbat, until} ->
send_error(conn, 503, "SHABBAT", %{ send_error(conn, 503, "SHABBAT", %{
@@ -527,9 +558,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
}) })
:available -> :available ->
{:ok, sources} = parse_sources(params["sources"]) fun.()
respond_bedikah(conn, VavPipeline.Bedikah.examine(output, model, query, sources: sources))
end
end end
end end
@@ -645,6 +674,38 @@ defmodule VavPipeline.BigdeiKodesh.Router do
}) })
end end
defp respond_pair(conn, {:ok, result}) do
send_json(conn, 200, %{
pair_ref: result.pair_ref,
verdict: Atom.to_string(result.verdict),
violations: result.violations,
a: result.a,
b: result.b,
difference: result.difference,
leans: result.leans,
measures: VavPipeline.Bedikah.Pair.measures(),
meaning:
case result.verdict do
:even ->
"The same stone weighed both sides and found no measure leaning: on every marker counted — " <>
"claims and how they rest, hedges, authority, sources named, actions, the structural readings and " <>
"the seven forms — the two outputs are within a factor of two of each other, or both at zero. " <>
"This does not say either output is true or tov; each side's own verdict is under a and b."
:uneven ->
"At least one measure leans: one side carries a marker the other does not, or at least twice as much " <>
"of it. Two stones in one bag (even va'aven). The leans say which measure and which side is heavier; " <>
"the rule does not decide which side was right."
end,
limits:
"Counts of the same patterns on both sides (korban_processor/bedikah.py); deterministic. Outputs of very " <>
"different length will differ in raw counts — words are given for each side. Consensus is not a measure.",
disclosure: disclosure()
})
end
defp respond_pair(conn, other), do: respond_bedikah(conn, other)
# ============================================================ # ============================================================
# Internal — Courtyard processing # Internal — Courtyard processing
# ============================================================ # ============================================================
+2 -1
View File
@@ -153,6 +153,7 @@ defmodule VavPipeline.Doorpost do
"talmudic_engine/src/halt/avodah_zarah.pl", "talmudic_engine/src/halt/avodah_zarah.pl",
"talmudic_engine/src/halt/gilui_arayot.pl", "talmudic_engine/src/halt/gilui_arayot.pl",
"talmudic_engine/src/halt/lo_taaseh_lekha.pl", "talmudic_engine/src/halt/lo_taaseh_lekha.pl",
"talmudic_engine/src/halt/mishkal.pl",
"talmudic_engine/src/halt/shfichut_damim.pl", "talmudic_engine/src/halt/shfichut_damim.pl",
"talmudic_engine/src/halt/structure.pl", "talmudic_engine/src/halt/structure.pl",
"talmudic_engine/src/middot/binyan_av.pl", "talmudic_engine/src/middot/binyan_av.pl",
@@ -178,7 +179,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 "86a43d6b688780f787d09124fe429a26f96cbe6a7e36460d3b6e5b33f2b2d095" @expected_hash "f90870d476a3a1f31b7cc3a4ff04f8dad1f5db190e16c6956ce19c368366d656"
@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()]
@@ -0,0 +1,36 @@
defmodule VavPipeline.L3.PairRecord do
@moduledoc """
L3 schema for a paired reading (bishearecha.md §VI): two bedikot
weighed with one stone. Append-only; keeps the counts on each side and
the measures that leaned, never the outputs — those are in `bedikot`
by hash, under `bedikah_ref_a` and `bedikah_ref_b`.
"""
use Ecto.Schema
import Ecto.Changeset
schema "bedikah_pairs" do
field :pair_ref, :string
field :bedikah_ref_a, :string
field :bedikah_ref_b, :string
field :provenance_a, :string
field :provenance_b, :string
field :measures_a, :map, default: %{}
field :measures_b, :map, default: %{}
field :leans, {:array, :map}, default: []
field :violations, {:array, :string}, default: []
field :verdict, :string
field :weighed_at, :utc_datetime
timestamps(type: :utc_datetime, updated_at: false)
end
@required [:pair_ref, :bedikah_ref_a, :bedikah_ref_b, :provenance_a, :provenance_b, :verdict, :weighed_at]
@optional [:measures_a, :measures_b, :leans, :violations]
def changeset(record, attrs) do
record
|> cast(attrs, @required ++ @optional)
|> validate_required(@required)
|> validate_inclusion(:verdict, ["even", "uneven"])
end
end
+10
View File
@@ -85,6 +85,16 @@ defmodule VavPipeline.L3.Store do
|> Repo.insert() |> Repo.insert()
end end
@doc """
Record a paired reading (bishearecha.md §VI): two bedikot weighed with one stone.
"""
@spec record_pair(map()) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}
def record_pair(attrs) when is_map(attrs) do
%VavPipeline.L3.PairRecord{}
|> VavPipeline.L3.PairRecord.changeset(attrs)
|> Repo.insert()
end
@doc """ @doc """
Record an unresolvable tension (teiku) to L3. Record an unresolvable tension (teiku) to L3.
@@ -0,0 +1,30 @@
defmodule VavPipeline.Repo.Migrations.BedikahPairs do
use Ecto.Migration
@moduledoc """
The paired reading (bishearecha.md §VI — one weight, one measure):
two bedikot weighed with the same stone. The record keeps the counts
on each side, the measures that leaned, and the verdict; the outputs
themselves are in `bedikot` by hash. Append-only.
"""
def change do
create table(:bedikah_pairs) do
add :pair_ref, :string, null: false
add :bedikah_ref_a, :string, null: false
add :bedikah_ref_b, :string, null: false
add :provenance_a, :string, null: false
add :provenance_b, :string, null: false
add :measures_a, :jsonb, default: "{}", null: false
add :measures_b, :jsonb, default: "{}", null: false
add :leans, :jsonb, default: "[]", null: false
add :violations, {:array, :text}, default: [], null: false
add :verdict, :string, null: false
add :weighed_at, :utc_datetime, null: false
timestamps(type: :utc_datetime, updated_at: false)
end
create unique_index(:bedikah_pairs, [:pair_ref])
create index(:bedikah_pairs, [:weighed_at])
end
end
@@ -77,6 +77,25 @@ defmodule VavPipeline.L3.BedikahRecordTest do
assert [%{"form" => "virtue_certification"}] = stored.frames assert [%{"form" => "virtue_certification"}] = stored.frames
end end
test "the paired reading (bishearecha §VI): counts on each side and the leans are kept, never the outputs" do
{:ok, rec} =
Store.record_pair(%{
pair_ref: "pair-test-#{System.unique_integer([:positive])}",
bedikah_ref_a: "bedikah-a", bedikah_ref_b: "bedikah-b",
provenance_a: "gpt", provenance_b: "gpt",
measures_a: %{hedged_claims: 2, claims: 3}, measures_b: %{hedged_claims: 0, claims: 3},
leans: [%{measure: "hedged_claims", lean: "a", a: 2, b: 0, term: "action(weigh, by(gpt), measure(hedged_claims), lean(a), counts(2, 0))"}],
violations: ["halt(even_vaaven,...)"], verdict: "uneven",
weighed_at: DateTime.utc_now() |> DateTime.truncate(:second)
})
stored = VavPipeline.Repo.get!(VavPipeline.L3.PairRecord, rec.id)
assert stored.verdict == "uneven"
assert %{"hedged_claims" => 2} = stored.measures_a
assert [%{"measure" => "hedged_claims", "lean" => "a"}] = stored.leans
assert {:error, _} = Store.record_pair(%{pair_ref: "x", verdict: "tov"})
end
test "an empty examination is recorded with an empty vital content, not refused" do test "an empty examination is recorded with an empty vital content, not refused" do
assert {:ok, rec} = Store.record_bedikah(attrs(%{vital_content: "", verdict: "empty", violations: [], coupling_trace: [], propositions: [], references: [], authority_claims: []})) assert {:ok, rec} = Store.record_bedikah(attrs(%{vital_content: "", verdict: "empty", violations: [], coupling_trace: [], propositions: [], references: [], authority_claims: []}))
assert rec.vital_content == "" assert rec.vital_content == ""
+219
View File
@@ -0,0 +1,219 @@
defmodule VavPipeline.Bedikah.PairTest do
@moduledoc """
The paired reading — one weight, one measure (bishearecha.md §VI).
Two examinations with the same stone; the difference is the finding.
"""
use ExUnit.Case, async: false
use Plug.Test
alias VavPipeline.Bedikah.Pair
alias VavPipeline.BigdeiKodesh.Router
alias VavPipeline.HaltMonitor
alias VavPipeline.MannaLedger
@opts Router.init([])
# ------------------------------------------------------------
# Fixtures: what the Korban Processor reads on each side
# ------------------------------------------------------------
defp plain do
%{
"provenance" => "gpt", "query" => "", "vital_content" => "x", "propositions" => [], "discarded" => [],
"references" => [], "hebrew" => [], "roots" => [], "authority_claims" => [], "actions" => [],
"self_validation" => [], "self_promotion" => [], "excluded_adversarial" => [], "frames" => [],
"sources" => [], "claims" => []
}
end
defp claim(i, hedged, anchors), do: %{"index" => i, "sentence" => "s#{i}", "anchors" => anchors, "self_reference" => nil, "hedged" => hedged, "invokes" => []}
defp frame(form, s), do: %{"form" => form, "sentence" => s, "matched" => s}
# Side a: the matter from one party — hedged, "contested", academic.
defp hedged_side do
%{
plain()
| "claims" => [claim(1, true, []), claim(2, true, []), claim(3, false, [%{"source" => "r", "evidence" => "quote"}])],
"frames" => [
frame("contested_laundering", "This is contested."),
frame("contested_laundering", "The matter remains disputed."),
frame("source_asymmetry", "Most scholars hold otherwise.")
]
}
end
# Side b: the same matter from the other party — asserted, ambient.
defp asserted_side do
%{plain() | "claims" => [claim(1, false, []), claim(2, false, [%{"source" => "r", "evidence" => "quote"}]), claim(3, false, [%{"source" => "r", "evidence" => "quote"}])]}
end
# The examiner tells the sides apart by their output text.
defp examiner do
fn output, _prov, _q, _sources ->
cond do
output =~ "HEDGED" -> {:ok, hedged_side()}
output =~ "ASSERTED" -> {:ok, asserted_side()}
true -> {:ok, plain()}
end
end
end
# The engine's rules as they answer over the bridge (talmudic_engine/test/bedikah_test.pl pins the real ones).
defp halt_checker(term) do
violations =
cond do
String.starts_with?(term, "action(weigh,") ->
[_, a, b] = Regex.run(~r/counts\((\d+), (\d+)\)/, term)
if a != b, do: ["halt(even_vaaven,#{term},#{term |> String.split("measure(") |> List.last() |> String.split(")") |> hd()},...)"], else: []
String.starts_with?(term, "action(frame,") ->
["halt(lo_taaseh_lekha,#{term},...)"]
true ->
[]
end
{:ok, %{"status" => if(violations == [], do: "ok", else: "halt"), "violations" => violations, "action" => term}}
end
defp interpretation_checker(_ref, _confidence, _trace, _ground), do: {:ok, %{"status" => "ok", "violations" => []}}
defp opts, do: [examiner: examiner(), halt_checker: &halt_checker/1, interpretation_checker: &interpretation_checker/4]
defp side(text), do: %{output: text, model: "gpt", query: "the matter", sources: []}
setup do
if HaltMonitor.halted?(), do: HaltMonitor.resolve("pair test setup")
write_provision(50)
:ok
end
defp write_provision(compute_units) do
MannaLedger.reset()
on_exit(fn -> 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" => compute_units, "memory_mb" => 512, "api_calls_external" => 5, "storage_delta_mb" => 2, "sanhedrin_panels_max" => 2},
"type" => "regular",
"expires_at" => expires,
"written_by" => "pair_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)
end
# ------------------------------------------------------------
# The stone
# ------------------------------------------------------------
test "measure/1 counts the same markers on any examination" do
m = Pair.measure(hedged_side())
assert m["claims"] == 3 and m["hedged_claims"] == 2 and m["grounded_claims"] == 1 and m["ungrounded_claims"] == 2
assert m["contested_laundering"] == 2 and m["source_asymmetry"] == 1 and m["virtue_certification"] == 0
assert Map.keys(m) |> Enum.sort() == Enum.sort(Pair.measures())
assert Pair.measure(plain()) |> Map.values() |> Enum.all?(&(&1 == 0))
end
test "leans/2: one against none, or at least twice as many; equal weights do not lean" do
assert [] == Pair.leans(Pair.measure(plain()), Pair.measure(plain()))
assert [] == Pair.leans(%{"claims" => 3} |> fill(), %{"claims" => 2} |> fill())
assert [%{measure: "claims", lean: :a, a: 4, b: 2}] = Pair.leans(fill(%{"claims" => 4}), fill(%{"claims" => 2}))
assert [%{measure: "claims", lean: :b, a: 0, b: 1}] = Pair.leans(fill(%{"claims" => 0}), fill(%{"claims" => 1}))
end
defp fill(m), do: Map.new(Pair.measures(), fn k -> {k, Map.get(m, k, 0)} end)
# ------------------------------------------------------------
# The weighing
# ------------------------------------------------------------
test "two sides weighed the same are even; nothing is sent to the engine" do
assert {:ok, r} = Pair.weigh(side("HEDGED one"), side("HEDGED two"), opts())
assert r.verdict == :even
assert r.leans == [] and r.violations == []
assert r.a.bedikah_ref != r.b.bedikah_ref
assert r.difference |> Map.values() |> Enum.all?(&(&1 == 0))
end
test "a hedged side against an asserted side is uneven: the leaning measures are named, with the counts" do
assert {:ok, r} = Pair.weigh(side("HEDGED"), side("ASSERTED"), opts())
assert r.verdict == :uneven
leaned = Map.new(r.leans, &{&1.measure, {&1.lean, &1.a, &1.b}})
assert leaned["hedged_claims"] == {:a, 2, 0}
assert leaned["ungrounded_claims"] == {:a, 2, 1}
assert leaned["grounded_claims"] == {:b, 1, 2}
assert leaned["contested_laundering"] == {:a, 2, 0}
assert leaned["source_asymmetry"] == {:a, 1, 0}
refute Map.has_key?(leaned, "claims")
assert Enum.all?(r.leans, &(&1.term =~ "action(weigh, by(gpt), measure("))
assert length(r.violations) == 5 and Enum.all?(r.violations, &(&1 =~ "even_vaaven"))
assert r.difference.hedged_claims == 2 and r.difference.grounded_claims == -1
end
test "each side keeps its own verdict; the pair does not condemn either output" do
assert {:ok, r} = Pair.weigh(side("HEDGED"), side("ASSERTED"), opts())
# Side a's own bedikah: contested_laundering stands on two sentences; the pair reports it, not the pair's rule.
assert r.a.verdict == :lo_tov and r.b.verdict == :tov
assert r.a.words == 1 and r.b.words == 1
assert r.a.measures.hedged_claims == 2
refute Enum.any?(r.violations, &(&1 =~ "lo_taaseh_lekha"))
end
test "two sides spend two units of manna; a pair that cannot be examined waits" do
# The ledger reads the provision on first gathering: 50 → 48 → 46.
assert {:ok, _} = Pair.weigh(side("HEDGED"), side("ASSERTED"), opts())
assert MannaLedger.remaining(:compute_units) == 48
assert {:ok, _} = Pair.weigh(side("HEDGED"), side("ASSERTED"), opts())
assert MannaLedger.remaining(:compute_units) == 46
write_provision(1)
assert {:waiting, _} = Pair.weigh(side("HEDGED"), side("ASSERTED"), opts())
end
# ------------------------------------------------------------
# The door: POST /courtyard/bedikah/pair
# ------------------------------------------------------------
describe "POST /courtyard/bedikah/pair" do
test "refuses without a bearer token" do
conn = post_pair(%{model: "gpt", a: %{output: "x"}, b: %{output: "y"}}, auth: false)
assert conn.status == 401
end
test "refuses a missing side, an empty side, an unknown model on one side, bad sources on the other" do
for body <- [
%{model: "gpt", a: %{output: "x"}},
%{model: "gpt", a: %{output: ""}, b: %{output: "y"}},
%{model: "gpt", a: %{output: "x", model: "oracle"}, b: %{output: "y"}},
%{model: "gpt", a: %{output: "x"}, b: %{output: "y", sources: "the report"}}
] do
conn = post_pair(body)
assert conn.status == 422, inspect(body)
error = Jason.decode!(conn.resp_body)["error"]
assert error["code"] == "KIYOR_VALIDATION_FAILED"
assert error["message"] =~ "side '"
end
end
test "with no Korban Processor in this VM, the door says so — it does not weigh from a stub" do
conn = post_pair(%{model: "gpt", a: %{output: "Devarim 6:5 says ואהבת."}, b: %{output: "Devarim 6:5 says ואהבת."}})
body = Jason.decode!(conn.resp_body)
assert conn.status == 503
assert body["error"]["code"] == "KORBAN_UNAVAILABLE"
refute Map.has_key?(body, "verdict")
end
end
defp post_pair(body, opts \\ []) do
conn = conn(:post, "/courtyard/bedikah/pair", Jason.encode!(body)) |> put_req_header("content-type", "application/json")
conn = if Keyword.get(opts, :auth, true), do: put_req_header(conn, "authorization", "Bearer test-token"), else: conn
Router.call(conn, @opts)
end
end