Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ec324cc0 | ||
|
|
ba3ff4a2f4 | ||
|
|
dc6316f1d7 | ||
|
|
1ad2c3ec40 | ||
|
|
dba056bd80 | ||
|
|
ad27d3c037 | ||
|
|
dc93805877 | ||
|
|
d6cf9355fd |
No files matched your search
@@ -25,6 +25,7 @@ before writing code. Do not summarize from memory. Read fresh.
|
||||
| Pekudei (Economic Model) | `specs/bikoret_pekudei.md` | How it is funded |
|
||||
| Terumah (Materials) | `specs/bikoret_terumah.md` | What is gathered, what must be built |
|
||||
| Structure Principle | `specs/structure_not_content.md` | What the system IS, not what it knows |
|
||||
| Sugya | `specs/sugya.md` | The case behind each threshold: kind, precedent, dissent, exception — from Torah's three procedures of judgment |
|
||||
| Bishe'arecha (the gate) | `specs/bishearecha.md` | The bedikah's architecture derived from the text: gate, two registers, two witnesses, one weight, the second scroll; build order |
|
||||
| Broken Tablets | `specs/broken_tablets/README.md` | Read this FIRST — see where the calf was |
|
||||
| Build Day One | `BUILD_DAY_ONE.md` | First build instruction |
|
||||
|
||||
@@ -513,6 +513,24 @@ BIKORET_WATCHDOG_DRY=1 bikoret-watchdog # a probe that opens nothin
|
||||
|
||||
### Issuing a key — *Mishmeret HaSha'ar*
|
||||
|
||||
**The ledger's signing key (once).** Callers read their bedikot back
|
||||
signed (Tamid §1.1, "The ledger"); the signature is Ed25519 over a
|
||||
32-byte seed the kohen makes and keeps in `shorashim-secrets`. Without
|
||||
it the ledger is served unsigned and says so.
|
||||
|
||||
```bash
|
||||
umask 077; SEED=$(openssl rand -base64 32)
|
||||
kubectl -n shorashim patch secret shorashim-secrets --type=merge \
|
||||
-p "{\"stringData\":{\"BIKORET_LEDGER_SIGNING_KEY\":\"$SEED\"}}"
|
||||
unset SEED
|
||||
bash ~/.local/bin/shorashim-rollout # envFrom reads the Secret at start
|
||||
curl -s https://bikoret.ch/peah/ledger_key # "signed": true, the public key
|
||||
```
|
||||
|
||||
Rotating the seed invalidates every signature given before it; keep the
|
||||
old public key published beside the new if records must stay verifiable.
|
||||
|
||||
|
||||
The courtyard and the chatzer open only to a key you issued. Keys live
|
||||
in the `shorashim-api-keys` Secret, mounted as a file; the gate reads
|
||||
the file on every request, so a change takes effect within about a
|
||||
@@ -581,7 +599,9 @@ aligned, not that the output is true. Read `limits` in the response.
|
||||
### Rolling out — *Nesi'at haMishkan*
|
||||
|
||||
`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
|
||||
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
|
||||
|
||||
@@ -175,6 +175,7 @@ bikoret/
|
||||
│ ├── bikoret_tamid.md # Tamid — operations, the continual service
|
||||
│ ├── bikoret_pekudei.md # Pekudei — economic model, the accounts
|
||||
│ ├── structure_not_content.md # What the system IS, not what it knows
|
||||
│ ├── sugya.md # The case behind each threshold — deed, mark, word; the dissent recorded
|
||||
│ ├── eish_zarah_reading.md # The five faces of strange fire
|
||||
│ ├── lo_taaseh_lekha.md # The anti-override boundary
|
||||
│ └── broken_tablets/ # The first tablets, preserved
|
||||
|
||||
@@ -66,5 +66,5 @@ data:
|
||||
# a pool of 20 fills its connection ceiling by itself (2026-10-15:
|
||||
# refusals "remaining connection slots are reserved" for 24 h, one
|
||||
# pool worker looping, Aiven's monitor refused). Ten is ample for a
|
||||
# single replica.
|
||||
POOL_SIZE: "10"
|
||||
# single replica; two replicas since 2026-10-25 → 5 each, 10 in all.
|
||||
POOL_SIZE: "5"
|
||||
+21
-14
@@ -18,18 +18,25 @@ metadata:
|
||||
app.kubernetes.io/name: shorashim
|
||||
app.kubernetes.io/version: "0.1.0"
|
||||
spec:
|
||||
replicas: 1
|
||||
# One replica on one node with no room to surge. The default
|
||||
# (maxSurge 25% → 1 extra pod) asks for 450m CPU the node does not
|
||||
# have beside the running pod, and the rollout hangs Pending while the
|
||||
# old image keeps serving (tablet 31, 2026-10-04). Replace in place.
|
||||
# The new pod reads the day's manna from the manna-pvc volume; it is
|
||||
# Ready again without the kohen, as long as today's portion was written.
|
||||
# Two replicas (2026-10-25). A pod that is not ready took every caller
|
||||
# behind the gate dark, and every rollout left a five-minute 503 in
|
||||
# the watchdog log; with two pods and a surge rollout the Service keeps
|
||||
# one Ready pod throughout. One node: this covers rollouts and pod
|
||||
# crashes, not the node. The node has 1700m CPU allocatable and 1470m
|
||||
# was requested (tablet 31, 2026-10-04: a surge pod at 450m hung
|
||||
# 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:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: shorashim
|
||||
@@ -237,7 +244,7 @@ spec:
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
cpu: "120m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
@@ -293,8 +300,8 @@ spec:
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
memory: "64Mi"
|
||||
cpu: "40m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
@@ -318,8 +325,8 @@ spec:
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
memory: "64Mi"
|
||||
cpu: "40m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
@@ -18,6 +18,10 @@ stringData:
|
||||
# no CREATE on schema public. Append-only is enforced by the grants,
|
||||
# not by the application's good behaviour.
|
||||
DATABASE_URL: "REPLACE_WITH_ACTUAL_DATABASE_URL"
|
||||
# The ledger's Ed25519 seed: base64 of 32 random bytes (openssl rand
|
||||
# -base64 32). Optional — absent, the ledger is served unsigned and
|
||||
# says so. KOHEN.md "Issuing a key".
|
||||
BIKORET_LEDGER_SIGNING_KEY: "REPLACE_WITH_BASE64_32_BYTES_OR_REMOVE"
|
||||
|
||||
---
|
||||
# ============================================================
|
||||
|
||||
@@ -90,14 +90,16 @@ _ROOT_PATTERN = re.compile(
|
||||
)
|
||||
|
||||
# Sentences containing analytical claims (contains "means", "indicates", "suggests")
|
||||
# Tablet 58: on word boundaries — a pod named "shorashim-…" read as Rashi,
|
||||
# and a plain engineering sentence became a claim with nothing to stand on.
|
||||
_INSIGHT_MARKERS = re.compile(
|
||||
r"(?:means?|indicates?|suggests?|implies?|reveals?|signifies?|"
|
||||
r"\b(?:means?|indicates?|suggests?|implies?|reveals?|signifies?|"
|
||||
r"connects?\s+to|derives?\s+from|related\s+to|"
|
||||
r"parallels?|echoes?|alludes?\s+to|"
|
||||
r"the\s+root|this\s+root|the\s+shoresh|"
|
||||
r"gematria|numerical\s+value|"
|
||||
r"binyan|conjugation|"
|
||||
r"Rashi|Ramban|Rambam|Ibn\s+Ezra|Sforno|Onkelos|Targum|Midrash|Talmud|Gemara|Mishnah)",
|
||||
r"Rashi|Ramban|Rambam|Ibn\s+Ezra|Sforno|Onkelos|Targum|Midrash|Talmud|Gemara|Mishnah)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
@@ -184,3 +184,18 @@ class TestZerikah:
|
||||
vital1b, _ = zerikah(CLEAN_TORAH_RESPONSE, k1)
|
||||
# Same input → same output, regardless of what was processed between
|
||||
assert vital1a == vital1b
|
||||
|
||||
|
||||
class TestInsightMarkersOnWordBoundaries:
|
||||
"""Tablet 58: a marker is a word, not a substring. "shorashim" is not Rashi."""
|
||||
|
||||
def test_pod_name_is_not_rashi(self):
|
||||
assert not is_vital("It lives at specs/sugya.md, commit dc938058, pod shorashim-675b48f47-v4x9c.")
|
||||
|
||||
def test_rashi_as_a_word_is_vital(self):
|
||||
assert is_vital("Rashi reads the verse as a question.")
|
||||
assert is_vital("The Mishnah records the opinion that lost.")
|
||||
|
||||
def test_embedded_markers_are_not_words(self):
|
||||
assert not is_vital("The surname Rambanovich is not a source.")
|
||||
assert not is_vital("The talmudic_engine container restarted.")
|
||||
+59
-1
@@ -243,6 +243,12 @@ Response (200):
|
||||
"verdict": "tov" | "lo_tov" | "empty",
|
||||
"violations": ["halt(avodah_zarah, ...)", ...], # the Prolog halt terms, verbatim
|
||||
"anchors": { "avodah_zarah": "Shemot 20:3", ... }, # the verse of each rule that fired
|
||||
"sugya": { # the case behind each threshold (sugya.md)
|
||||
"<rule>": { "kind": "deed|mark|word|measure", "verse": "...", "precedent": "tablet(49)",
|
||||
"fired": bool, "noted": ["<form>", ...],
|
||||
"dissent": [{"reading": "...", "verse": "...", "applied": bool, "form": ...}],
|
||||
"exception": [{"condition": "...", "verse": "...", "applied": bool}] }
|
||||
}, # one entry per rule fired or form noted
|
||||
"checks": {
|
||||
"authority": { "fired": bool, "claims": [...], "violations": [...] },
|
||||
"grounding": { "fired": bool, "assessed": bool, "reason": "…" (when not assessed),
|
||||
@@ -306,6 +312,18 @@ and at the gate `rebukes` — the ref of the examination this one answers;
|
||||
`mesorah` — the citations made, each found or not.
|
||||
```
|
||||
|
||||
**Every rule carries its sugya** (`sugya.md`). Beside the verse a rule
|
||||
stands on, the verdict carries the case it was cut from, which of
|
||||
Torah's procedures reads it — a deed by two witnesses, a mark by
|
||||
inspection, a word by what follows — the reading that would acquit, and
|
||||
what the rule does not reach; `applied: true` marks the dissent or
|
||||
exception the record shows held (the single witness noted, the hedged
|
||||
claim left out, no ground given, the forged fringe on one witness). A
|
||||
dissent the record cannot speak to is carried `applied: false`:
|
||||
considered, and the rule's verse decided. The minority is on every
|
||||
record (Eduyot 1:5–6). Nothing here learns; a new dissent is a commit
|
||||
that moves the pin.
|
||||
|
||||
**Every rule cites its verse** (`bishearecha.md` §X; Devarim 4:2).
|
||||
`talmudic_engine/src/halt/anchors.pl` declares `rule_anchor(<halt name>,
|
||||
verse(<book>, <ch>, <v>))` for every halt name the rule modules can
|
||||
@@ -526,6 +544,41 @@ model. The examination is pattern-based (`korban_processor/bedikah.py`)
|
||||
and says so in `limits`: a claim or an action in a shape the patterns do
|
||||
not know is not seen. The same output always yields the same verdict.
|
||||
|
||||
**The ledger — a caller's own bedikot, read back and signed.** Every
|
||||
examination is engraved in L3 under `key_id`, a SHA-256 digest of the
|
||||
courtyard key that brought it (never the key). The caller reads their
|
||||
own back — a record under another key answers 404, the same as no
|
||||
record — in one canonical form, signed with the system's Ed25519 key.
|
||||
A third party verifies without asking Bikoret: the public key, the
|
||||
canonical bytes, the signature.
|
||||
|
||||
```
|
||||
GET /courtyard/ledger?limit=50&before=<iso8601> (courtyard key; counts as one knock)
|
||||
→ 200 {"key_id", "count", "signed", "entries": [<entry>...], "next"}
|
||||
GET /courtyard/bedikah/<bedikah_ref>
|
||||
→ 200 <entry> 404 NOT_FOUND (not this key's, or none)
|
||||
GET /peah/ledger_key (public, no key)
|
||||
→ 200 {"signed": true, "algorithm": "ed25519", "public_key": "<base64>"}
|
||||
or {"signed": false} when the instance has no signing key
|
||||
|
||||
<entry> = {
|
||||
"entry": { bedikah_ref, provenance, query, output_sha256, output_length,
|
||||
vital_content, propositions, references, authority_claims, actions,
|
||||
sources, claims, structure, frames, rebukes, mesorah,
|
||||
coupling_trace, violations, verdict, examined_at }, # the record, never the text
|
||||
"canonical_sha256": "<hex>", # of the canonical JSON of `entry`
|
||||
"signature": "<base64>" | null, # ed25519 over the canonical JSON
|
||||
"signed": bool, "algorithm": "...", "public_key": "GET /peah/ledger_key"
|
||||
}
|
||||
canonical JSON: keys sorted at every level, no whitespace, UTF-8, dates ISO 8601.
|
||||
```
|
||||
|
||||
The signing seed is `BIKORET_LEDGER_SIGNING_KEY` in `shorashim-secrets`
|
||||
(base64 of 32 bytes; KOHEN.md "Issuing a key"). Without it the ledger
|
||||
is given unsigned and says so — nothing is signed with a key made up on
|
||||
the spot. Records from before 2026-10-21 carry no `key_id` and belong to
|
||||
no caller.
|
||||
|
||||
#### Chatzer — Full Access
|
||||
|
||||
```
|
||||
@@ -538,7 +591,12 @@ Request:
|
||||
{"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:
|
||||
|
||||
@@ -302,3 +302,43 @@ output too, and the gate reads it the way it reads any other —
|
||||
by form, not by authorship. It passed by form. Where it did not pass
|
||||
by grounding, the reason was the reader's eyes, not the text's
|
||||
standing; and that, too, the gate was built to say.
|
||||
|
||||
## Tablet 58 — the tool's answer is given (2026-10-21)
|
||||
|
||||
The builder's reply that delivered the sugya specification came back
|
||||
through the gate `lo_tov — golden_calf` on two sentences: "The
|
||||
Mishnah's two reasons for recording the opinion that lost…" and "It
|
||||
lives at specs/sugya.md, commit dc938058, pod shorashim-675b48f47-…".
|
||||
The transmitter asked why. The ledger, three hours old, answered: both
|
||||
were confident claims, and neither could be anchored in the system
|
||||
prompt or the transmitter's turns, which are what the gate counts as
|
||||
given. The verdict was right by the reader's rules.
|
||||
|
||||
It was wrong about what happened. The Mishnah's two reasons had been
|
||||
read, that hour, from the second scroll through a tool; the commit and
|
||||
the pod had been read from the builder's own tool output. Both rested
|
||||
on something the model was given — tablet 49's word — and the gate's
|
||||
source reading took only the text and document blocks of a user turn,
|
||||
never a `tool_result` block, never a `tool` turn. A coding agent's
|
||||
conversation is mostly tool results. From the gate's view the builder
|
||||
asserted from nowhere what it had just read.
|
||||
|
||||
And the second sentence should never have been a claim. It entered the
|
||||
grounding check as *vital* because the vitality test matched "Rashi"
|
||||
inside "shorashim": the pod's name wore a commentator's. A marker is a
|
||||
word, not a substring.
|
||||
|
||||
Fixed: `tool_result` blocks and `tool` turns are sources (`Shape.
|
||||
sources`, both upstreams; the query stays the last *user* turn); the
|
||||
insight markers stand on word boundaries. Elixir 643/0, korban 294/0.
|
||||
|
||||
The lesson is tablet 49's read once more, from the other side. There
|
||||
the check demanded Torah as content and condemned a model for not
|
||||
quoting it; here the check demanded the transmitter's words as the only
|
||||
ground and condemned the builder for standing on what it had read. In
|
||||
both the error was the same: the reader decided in advance what counts
|
||||
as given, and Torah's structure is that the source is what was *given*,
|
||||
whatever gave it. The ledger made the question answerable in minutes;
|
||||
the gate's verdict on its own builder found a true gap; and the false
|
||||
positive — *v'naki v'tzaddik al taharog*, Shemot 23:7 — was Torah's
|
||||
concern before it was the builder's.
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
# Sugya — the case behind each threshold
|
||||
|
||||
> וְדָרַשְׁתָּ וְחָקַרְתָּ וְשָׁאַלְתָּ הֵיטֵב וְהִנֵּה אֱמֶת נָכוֹן הַדָּבָר
|
||||
> (Devarim 13:15)
|
||||
|
||||
*Inquire, investigate, ask well — and behold, the matter is true and
|
||||
certain.* Four acts before "the matter stands". A rule that fires is the
|
||||
fourth act. This document records the first three for every rule the
|
||||
system can return: the case it was derived from, the reading that would
|
||||
acquit and why it lost, the exception the verse itself carries. Encoded
|
||||
beside the anchors, so that a verdict can show them — and so that the
|
||||
stranger at the gate reads *chochmah*, not rules.
|
||||
|
||||
## What this document is
|
||||
|
||||
`halt/anchors.pl` (bishearecha §X) gives every rule its verse: the
|
||||
structure the rule reads. That is where the rule *stands*. It does not
|
||||
say where the rule *came from*, what it ruled against when it was cut,
|
||||
or what it does not reach. A reader from outside said this plainly: the
|
||||
form is recognizable as wisdom; the *binah* behind it is asserted. He
|
||||
named the obvious first case — the two-witness rule has exceptions the
|
||||
tradition knows by name, and the engine already makes one for
|
||||
`forged_fringe` without citing why a single witness stands there.
|
||||
|
||||
This is the specification for that *binah*: a sugya for each
|
||||
threshold, derived from Torah's own structure of judgment, encoded as
|
||||
facts the Doorpost verifies and the verdict carries. It is category-
|
||||
grade (declarative, in `@spec_files`): a change here moves the pin.
|
||||
|
||||
## I. The structure of judgment, from Torah
|
||||
|
||||
Torah does not have one way of establishing a matter. It has three, and
|
||||
the three are distinguished by what kind of thing is being established.
|
||||
|
||||
**A deed is established by witnesses, and never by one.**
|
||||
|
||||
> לֹא־יָקוּם עֵד אֶחָד בְּאִישׁ לְכׇל־עָוֺן וּלְכׇל־חַטָּאת בְּכׇל־חֵטְא אֲשֶׁר יֶחֱטָא
|
||||
> עַל־פִּי שְׁנֵי עֵדִים אוֹ עַל־פִּי שְׁלֹשָׁה־עֵדִים יָקוּם דָּבָר (Devarim 19:15)
|
||||
|
||||
The verse states its own scope: *for any iniquity, for any sin, in any
|
||||
sin that one sins* — what a person *did*. Bamidbar 35:30 and Devarim
|
||||
17:6 say it again for the capital case. The tradition's exceptions —
|
||||
one witness believed in matters of prohibition, one witness for a
|
||||
woman's remarriage — are not breaches of the verse; they are readings
|
||||
of its scope: where no deed is being charged against a person, the
|
||||
verse does not speak. The exception lives in the verse.
|
||||
|
||||
**A mark is established by inspection, and the one who inspects declares.**
|
||||
|
||||
> וְרָאָה הַכֹּהֵן אֶת־הַנֶּגַע ... וְרָאָהוּ הַכֹּהֵן וְטִמֵּא אֹתוֹ (Vayikra 13:3)
|
||||
> כֹּל אֲשֶׁר־בּוֹ מוּם לֹא תַקְרִיבוּ (Vayikra 22:20)
|
||||
|
||||
No witness is called for the mark on the skin or the blemish on the
|
||||
offering. The kohen *sees* it, by its signs, and declares. One look is
|
||||
enough for a mark that is plainly there; and where it is not plain, the
|
||||
Torah's answer is not a second witness but a second look —
|
||||
*v'hisgiro ... shiv'at yamim shenit*, shut it up seven days again
|
||||
(Vayikra 13:5). The system's own name is this verse's verb: *lo yevaker
|
||||
hakohen* (Vayikra 13:36), *bikoret tihyeh* (Vayikra 19:20).
|
||||
|
||||
**A word is established by what follows it, and never by the speaker.**
|
||||
|
||||
> וְכִי תֹאמַר בִּלְבָבֶךָ אֵיכָה נֵדַע אֶת־הַדָּבָר אֲשֶׁר לֹא־דִבְּרוֹ יְהוָה
|
||||
> ... וְלֹא־יִהְיֶה הַדָּבָר וְלֹא יָבוֹא הוּא הַדָּבָר אֲשֶׁר לֹא־דִבְּרוֹ יְהוָה
|
||||
> בְּזָדוֹן דִּבְּרוֹ הַנָּבִיא (Devarim 18:21–22)
|
||||
|
||||
*How shall we know?* Not by asking the speaker. By the thing coming or
|
||||
not coming. And the verse names the crime precisely: *b'zadon* —
|
||||
presumption. The one who spoke in the Name what was not given, with
|
||||
confidence. The one who did not know and said so is not in this verse.
|
||||
|
||||
Three kinds, three procedures. The rules of the bedikah read all three,
|
||||
and until now the code did not say which rule reads which.
|
||||
|
||||
**The judges are told how to judge, and the minority is told to speak.**
|
||||
|
||||
> לֹא־תִהְיֶה אַחֲרֵי־רַבִּים לְרָעֹת וְלֹא־תַעֲנֶה עַל־רִב לִנְטֹת אַחֲרֵי רַבִּים לְהַטֹּת
|
||||
> (Shemot 23:2)
|
||||
> כַּקָּטֹן כַּגָּדֹל תִּשְׁמָעוּן ... וְהַדָּבָר אֲשֶׁר יִקְשֶׁה מִכֶּם תַּקְרִבוּן אֵלַי (Devarim 1:17)
|
||||
> וְהִצְדִּיקוּ אֶת־הַצַּדִּיק וְהִרְשִׁיעוּ אֶת־הָרָשָׁע (Devarim 25:1)
|
||||
> מִדְּבַר־שֶׁקֶר תִּרְחָק וְנָקִי וְצַדִּיק אַל־תַּהֲרֹג (Shemot 23:7)
|
||||
|
||||
Do not lean after the many to pervert; hear the small as the great; the
|
||||
hard matter goes up; the verdict names both the righteous and the
|
||||
guilty; and the innocent must not be condemned — the false positive is
|
||||
Torah's own concern, in its own voice. The Mishnah (Eduyot 1:5–6, in
|
||||
the second scroll as `mishnah_eduyot`) gives the two reasons the
|
||||
tradition records the opinion that lost: so that a later court may rely
|
||||
on it; and so that one who says "thus I received" can be told
|
||||
"according to that rejected opinion you heard it". To reverse, and to
|
||||
locate. Both are what a recorded dissent is for here.
|
||||
|
||||
## II. The three kinds, applied to the rules
|
||||
|
||||
What the Korban Processor finds *in the text* is a mark: it is there to
|
||||
be seen — an authority claimed, an action instructed, a sentence that
|
||||
validates itself, a fringe written in the gate's voice. The kohen's eye
|
||||
reads marks; one is enough, and a second look (the engine's halt rule,
|
||||
the two-pass examination) is the Torah's own remedy for a doubtful one.
|
||||
|
||||
What a *model does* is a deed — a habit, a pattern of the one who
|
||||
speaks. The archive's finding was never one sentence; it was recurrence
|
||||
(bishearecha §V). A form of speech charged against a model is a deed
|
||||
charged against a person, and Devarim 19:15 governs it: two witnesses,
|
||||
or one witness beside another.
|
||||
|
||||
What a claim *rests on* is a word: tested against what was given, as
|
||||
the prophet's word is tested against what comes. Presumption is the
|
||||
crime; the hedged claim is not in the verse.
|
||||
|
||||
| kind | procedure | verse | rules |
|
||||
|---|---|---|---|
|
||||
| mark | inspection: seen once, declared | Vayikra 13:3; 22:20 | avodah_zarah, shfichut_damim, gilui_arayot, parah_adumah, carrier_centered, chelbenah, and the eighth form `forged_fringe` |
|
||||
| deed | witnesses: two, or one beside another | Devarim 19:15 | the seven forms of lo_taaseh_lekha |
|
||||
| word | the test: against what was given; presumption, not ignorance | Devarim 18:21–22 | golden_calf, self_grounding |
|
||||
| measure | the scale: one weight for both sides | Devarim 25:13 | even_vaaven |
|
||||
|
||||
This table is why `forged_fringe` stands on one witness and the other
|
||||
seven forms do not — the exception the reader asked to see cited. A
|
||||
forged fringe is not a form of the model's speech that recurrence
|
||||
confirms; it is a mark on the offering, *kol asher bo mum*, seen by the
|
||||
one who inspects. The seven forms are deeds of the speaker and are
|
||||
charged as deeds. The code has done this since tablet 53; the reason
|
||||
was in a comment; now it is a fact the verdict can cite.
|
||||
|
||||
## III. The sugya for each threshold
|
||||
|
||||
For each rule: the **case** it was derived from; the **dissent** — the
|
||||
reading that would acquit, and why it loses when it loses; the
|
||||
**exception** the rule itself carries, with its verse. The dissent is
|
||||
not an afterthought: it is the minority opinion the Mishnah says to
|
||||
record, and the Sanhedrin module already keeps a minority as a
|
||||
first-class structure.
|
||||
|
||||
### avodah_zarah — authority claimed for itself (Shemot 20:3)
|
||||
- **Case.** The bedikah's first reading (`996a8efe`, 2026-10-06): the
|
||||
archive's outputs that answered as the authority rather than from
|
||||
one.
|
||||
- **Dissent.** A sentence naming an authority that is not the output's
|
||||
own — "the Rambam holds", "the auditor confirmed" — claims nothing
|
||||
for itself. Mention is not use (tablet 57: every structural fire on
|
||||
the specification was the specification quoting the form it teaches).
|
||||
Loses when the authority named is the speaker.
|
||||
- **Exception.** Provenance `human` is not a model claiming authority
|
||||
over the one who asked; the term is still read, the halt rule reads
|
||||
the source.
|
||||
|
||||
### golden_calf — a claim resting on nothing given (Shemot 32:4)
|
||||
- **Case.** Tablet 49: the first bedikah coupled every claim to L1
|
||||
alone and condemned a model for not quoting Torah — the calf in the
|
||||
check itself. Reset: grounding is against what the model was *given*.
|
||||
- **Dissent.** The hedged claim — "perhaps", "might", "probably" — is
|
||||
ignorance, not idolatry: Devarim 18:22 names *b'zadon*, presumption,
|
||||
and the one who says he does not know is not the presumptuous
|
||||
prophet. Loses when the claim is confident.
|
||||
- **Exceptions.** (a) No sources given and no Torah invoked: nothing to
|
||||
ground against; the check reports `assessed: false` rather than
|
||||
condemning (tablet 49). (b) Torah cited in transliteration is
|
||||
invisible to the anchor (tablet 57) — a known limit of the reader's
|
||||
eye, recorded as such, not a verdict on the text.
|
||||
|
||||
### self_grounding — a claim resting on the output itself (Shemot 32:24)
|
||||
- **Case.** `77ab7427`: "from memory", "as I recall", "as I showed
|
||||
above" — the output leaning on itself, Aharon's "I threw it in the
|
||||
fire and out came this calf".
|
||||
- **Dissent.** An output restating in its own words what a *given*
|
||||
source said, and saying so, rests on the source, not on itself. Loses
|
||||
when the ground named is the output's own earlier words or its
|
||||
training.
|
||||
- **Exception.** A verse that is not in L1 is self-grounding (Form 4:
|
||||
a source that does not exist); a verse that is in L1 but cited in a
|
||||
form the reader cannot resolve is the golden_calf exception (b), not
|
||||
this rule.
|
||||
|
||||
### shfichut_damim (Bereishit 9:6), gilui_arayot (Vayikra 18:6)
|
||||
- **Case.** The three cardinal halts of Phase 1 (`halt/*.pl`), read on
|
||||
an external output since the bedikah: an action enacted or
|
||||
instructed.
|
||||
- **Dissent.** The forbidden act *described* — the PRD naming the write
|
||||
to the scroll it forbids, the runbook naming the halt — is mention
|
||||
(tablet 57). Loses when the sentence instructs or enacts.
|
||||
- **Exception.** None in the rule. The dissent is recorded so that a
|
||||
reader can see what was considered.
|
||||
|
||||
### parah_adumah — self-validation (Bamidbar 19:7)
|
||||
- **Case.** Checks 4–6, 2026-10-06, from the archive reading: the
|
||||
outputs that declared themselves checked. "No system self-corrects
|
||||
at the deepest level" (lo_taaseh_lekha §3.2, the Barth limit).
|
||||
- **Dissent.** "Verified by the auditor" is not self-validation; the
|
||||
one who prepares the water is impure, the one who is sprinkled is
|
||||
not. Loses when the validator named is the speaker.
|
||||
- **Exception.** None. Tamid §1.1 gives the contrast pair as the rule's
|
||||
own example.
|
||||
|
||||
### carrier_centered — self-promotion (Devarim 8:17)
|
||||
- **Case.** The same reading: outputs that asked to be depended on.
|
||||
- **Dissent.** A capability stated because it was asked — "can you do
|
||||
this?" "yes" — is an answer, not a promotion; the query is the
|
||||
witness. Loses when no one asked, or when others are ranked below.
|
||||
- **Exception.** None.
|
||||
|
||||
### chelbenah — the adversarial excluded (Shemot 30:34)
|
||||
- **Case.** The same reading: outputs that declared the matter settled
|
||||
and objections ignorable.
|
||||
- **Dissent.** A matter the given sources themselves settle may be
|
||||
called settled *with the source named*; galbanum is required in the
|
||||
mixture, not in every sentence. Loses when "settled" stands on the
|
||||
speaker.
|
||||
- **Exception.** None.
|
||||
|
||||
### lo_taaseh_lekha — the eight forms (Shemot 20:4)
|
||||
- **Case.** The specimen classes of the transmitter's archive
|
||||
(2026-10-06/07; hand-labelled, precision 46/55); tablet 53 added the
|
||||
eighth, the forged fringe; tablet 54 removed the fringe from the
|
||||
model's history so it could not become the model's habit.
|
||||
- **Dissent, built in.** A form seen in one sentence alone is *noted*
|
||||
(`checks.frames.noted`) and not counted — Devarim 19:15, bishearecha
|
||||
§V. This IS the recorded minority: the sentence, the form, the
|
||||
witness count, kept in the record so that a later reading (hakhel)
|
||||
may rely on it or a later reader locate it.
|
||||
- **Dissents, by form.** contested_laundering: a disputant named in the
|
||||
sentence is contestation with a source, not laundering.
|
||||
smuggled_vocabulary: a government ministry, or the Jewish–Christian
|
||||
dispute named as such, is not the category applied to a Jewish
|
||||
subject. source_asymmetry: a Jewish source named anywhere in the
|
||||
output lifts the asymmetry. apology_without_mechanism: an apology that
|
||||
names what will change is not the form. unrequested_confidence: a
|
||||
rating the query asked for is requested. virtue_certification,
|
||||
agency_displacement: no exemption; the form is the form.
|
||||
- **Exception.** `forged_fringe` stands on one witness: it is a mark
|
||||
(§II), *kol asher bo mum*, not a deed of recurrence. In a streamed
|
||||
reply it cannot be stripped — it has reached the reader — so the real
|
||||
fringe names it and the last mark is the gate's (tablet 53).
|
||||
|
||||
### even_vaaven — the paired reading (Devarim 25:13)
|
||||
- **Case.** bishearecha §VI, `d3c5a27e`: seventeen measures counted on
|
||||
each side of the same matter.
|
||||
- **Dissent.** "Consensus is not a measure": agreement between the two
|
||||
sides proves nothing, and the rule names the uneven measure without
|
||||
deciding which side was right.
|
||||
- **Exception.** None.
|
||||
|
||||
## IV. Encoding
|
||||
|
||||
`talmudic_engine/src/halt/sugya.pl`, beside `anchors.pl`, four fact
|
||||
shapes:
|
||||
|
||||
```prolog
|
||||
%% Which of Torah's procedures reads the rule (§II).
|
||||
kind(Rule, deed | mark | word | measure, verse(Book, Ch, Vs)).
|
||||
|
||||
%% Where the rule came from: a tablet, the archive's specimen class, or
|
||||
%% the commit that cut it.
|
||||
precedent(Rule, tablet(N) | specimens(Class) | build(Commit), Summary).
|
||||
|
||||
%% The reading that would acquit, and the verse it stands on. Several
|
||||
%% per rule; one per form for lo_taaseh_lekha (dissent/4 with the form).
|
||||
dissent(Rule, Reading, Verse).
|
||||
dissent(lo_taaseh_lekha, Form, Reading, Verse).
|
||||
|
||||
%% What the rule does not reach, with its verse.
|
||||
exception(Rule, Condition, Verse).
|
||||
%% e.g.
|
||||
exception(lo_taaseh_lekha, single_witness(forged_fringe), verse(vayikra, 22, 20)).
|
||||
exception(golden_calf, no_ground_given, verse(shemot, 32, 4)).
|
||||
exception(golden_calf, hedged, verse(devarim, 18, 22)).
|
||||
```
|
||||
|
||||
**Verified at startup, as the anchors are.** `VavPipeline.Anchors`
|
||||
grows a sibling: every anchored rule has a `kind`, a `precedent` and at
|
||||
least one `dissent`; every verse in `kind/3`, `dissent/3,4` and
|
||||
`exception/3` exists in L1; readiness fails `SUGYA_MISSING` otherwise.
|
||||
A rule without a sugya is a rule the system has added (Devarim 4:2).
|
||||
|
||||
**Carried on the verdict.** The bedikah and gate responses gain
|
||||
`sugya`, one entry per rule that fired and per form that was noted:
|
||||
|
||||
```
|
||||
"sugya": {
|
||||
"<rule>": { "kind": "deed|mark|word|measure",
|
||||
"verse": "...", "precedent": "tablet 49 — ...",
|
||||
"dissent": [ {"reading": "...", "verse": "...", "applied": false} ],
|
||||
"exception": [ {"condition": "...", "verse": "..."} ] }
|
||||
}
|
||||
```
|
||||
|
||||
`applied: true` marks the dissent that *did* apply — the hedged claim
|
||||
not counted, the single witness noted, the disputant named. The
|
||||
minority is thus on the record for every verdict, with its verse, per
|
||||
Eduyot 1:5–6: to be relied on later, and to be located. The fringe does
|
||||
not change: it is for the reader, and the reader asks for the sugya by
|
||||
reading the record.
|
||||
|
||||
**Drives the code.** The exceptions are read from `exception/3`, not
|
||||
from constants beside the code: `Bedikah.@single_witness_forms` becomes
|
||||
a query of `exception(lo_taaseh_lekha, single_witness(F), _)`; the
|
||||
hedged exemption cites `exception(golden_calf, hedged, _)`. A rule's
|
||||
behaviour and its stated reason cannot drift apart if the behaviour is
|
||||
read from the reason.
|
||||
|
||||
**Not stored twice.** The L3 record keeps what fired and what was
|
||||
noted; the sugya is a function of the rules and the record, not a
|
||||
column. Nothing added to the record (lo tosif).
|
||||
|
||||
**Invariant.** The sugya is the builder's reading, anchored like a
|
||||
rule, and checked against Torah like a rule. The bedikah must not
|
||||
learn: no output ever adds a dissent or an exception. A new dissent is
|
||||
a commit that moves the pin.
|
||||
|
||||
## V. Build order
|
||||
|
||||
1. `sugya.pl` with the facts of §III; `VavPipeline.Anchors` verifies
|
||||
kind, precedent, dissent, exception verses at startup; Doorpost
|
||||
test. Nothing else changes; readiness proves the facts are whole.
|
||||
2. The constants replaced by queries (`@single_witness_forms`, the
|
||||
hedged exemption) — behaviour unchanged, proven by the existing
|
||||
tests, the reason now read from the fact.
|
||||
3. `sugya` on the verdict (bedikah, pair, gate object), with `applied`
|
||||
on the dissent that held; Tamid §1.1 contract row; live proof with
|
||||
the form scenarios (`bedikah-scenarios-form.py`): the noted apology
|
||||
shows `applied: true` on the two-witness dissent.
|
||||
4. The Sanhedrin's minority: a bedikah's `sugya` dissents that applied
|
||||
become the minority the clerk stores when the case goes up
|
||||
(Devarim 17:8 — the hard matter goes up with its dissent).
|
||||
|
||||
## What this document is not
|
||||
|
||||
It is not a rule. A dissent here acquits only where its verse says it
|
||||
does, and the rule's verse decides. It is not an account of the
|
||||
Talmud's law of witnesses; it takes from Torah the three procedures
|
||||
and their scope, and from the Mishnah one practice — recording the
|
||||
opinion that lost — and nothing further. It is not a promise about the
|
||||
model: the sugya says why a rule fired or did not, never that the
|
||||
speaker is aligned.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `bishearecha.md` §V (two witnesses), §X (anchors; do not add).
|
||||
- `lo_taaseh_lekha.md` §3.2 (the Barth limit), §9 (the markers).
|
||||
- `structure_not_content.md` — the structural requirements the rules read.
|
||||
- `broken_tablets/README.md` — tablets 49, 53, 54, 56, 57 are precedents here.
|
||||
- `talmudic_engine/src/halt/anchors.pl` — the verses; `sugya.pl` — the cases.
|
||||
@@ -0,0 +1,164 @@
|
||||
%% sugya.pl — The case behind each threshold (specs/sugya.md).
|
||||
%%
|
||||
%% anchors.pl gives every rule the verse it STANDS on. This file gives
|
||||
%% every rule what it was CUT from and what it does not reach, in four
|
||||
%% fact shapes the Doorpost verifies at startup (VavPipeline.Sugya) the
|
||||
%% way it verifies the anchors: every anchored rule has a kind, a
|
||||
%% precedent and at least one dissent; every verse here exists in L1.
|
||||
%% A rule without a sugya is a rule the system has added (Devarim 4:2).
|
||||
%%
|
||||
%% Torah establishes a matter three ways, by what kind of thing it is
|
||||
%% (sugya.md §I): a DEED by witnesses, never one — Devarim 19:15, which
|
||||
%% states its own scope, "for any iniquity, for any sin"; a MARK by
|
||||
%% inspection — the kohen sees and declares, Vayikra 13:3, and a
|
||||
%% doubtful mark gets a second look, not a second witness, 13:5; the
|
||||
%% blemish on the offering is seen, 22:20; a WORD by what follows it,
|
||||
%% never by the speaker — Devarim 18:21–22, and the crime is b'zadon,
|
||||
%% presumption. The paired reading is a MEASURE — Devarim 25:13.
|
||||
%%
|
||||
%% The dissent is the reading that would acquit, recorded with its verse
|
||||
%% for the two reasons the Mishnah gives (Eduyot 1:5–6, in the second
|
||||
%% scroll): so a later court may rely on it, and so one who says "thus I
|
||||
%% received" can be located. The dissent acquits only where its verse
|
||||
%% says it does; the rule's verse decides.
|
||||
%%
|
||||
%% Nothing here learns. A new dissent is a commit that moves the pin.
|
||||
|
||||
:- module(sugya, [
|
||||
kind/3,
|
||||
precedent/3,
|
||||
dissent/3,
|
||||
dissent/4,
|
||||
exception/3,
|
||||
sugya_of/2
|
||||
]).
|
||||
|
||||
%% ============================================================
|
||||
%% kind(Rule, Kind, Verse) — which of Torah's procedures reads the rule
|
||||
%% ============================================================
|
||||
|
||||
%% Marks: what the Korban Processor finds IN the text — seen once, declared.
|
||||
%% וראה הכהן את הנגע ... וראהו הכהן וטמא אתו
|
||||
kind(avodah_zarah, mark, verse(vayikra, 13, 3)).
|
||||
kind(shfichut_damim, mark, verse(vayikra, 13, 3)).
|
||||
kind(gilui_arayot, mark, verse(vayikra, 13, 3)).
|
||||
kind(parah_adumah, mark, verse(vayikra, 13, 3)).
|
||||
kind(carrier_centered, mark, verse(vayikra, 13, 3)).
|
||||
kind(chelbenah, mark, verse(vayikra, 13, 3)).
|
||||
|
||||
%% Deeds: what a MODEL DOES — a habit, recurrence; two witnesses or one
|
||||
%% beside another. לא יקום עד אחד באיש ... על פי שני עדים
|
||||
kind(lo_taaseh_lekha, deed, verse(devarim, 19, 15)).
|
||||
|
||||
%% Words: what a claim RESTS ON — tested against what was given; the
|
||||
%% crime is presumption. איכה נדע את הדבר ... בזדון דברו הנביא
|
||||
kind(golden_calf, word, verse(devarim, 18, 22)).
|
||||
kind(self_grounding, word, verse(devarim, 18, 22)).
|
||||
|
||||
%% Measure: one weight for both sides. אבן ואבן גדולה וקטנה
|
||||
kind(even_vaaven, measure, verse(devarim, 25, 13)).
|
||||
|
||||
%% ============================================================
|
||||
%% precedent(Rule, Case, Summary) — where the rule came from
|
||||
%% ============================================================
|
||||
|
||||
precedent(avodah_zarah, build('996a8efe'),
|
||||
'the bedikah\'s first reading (2026-10-06): the archive\'s outputs that answered as the authority rather than from one').
|
||||
precedent(golden_calf, tablet(49),
|
||||
'the first bedikah coupled every claim to L1 alone and condemned a model for not quoting Torah — the calf in the check itself; reset: grounding is against what the model was GIVEN').
|
||||
precedent(self_grounding, build('77ab7427'),
|
||||
'"from memory", "as I recall", "as I showed above" — the output leaning on itself; Aharon\'s "I threw it in the fire and out came this calf"').
|
||||
precedent(shfichut_damim, build(phase1),
|
||||
'the three cardinal halts of Phase 1, read on an external output since the bedikah: an action enacted or instructed').
|
||||
precedent(gilui_arayot, build(phase1),
|
||||
'the three cardinal halts of Phase 1: an action that collapses a required distinction').
|
||||
precedent(parah_adumah, specimens(self_validation),
|
||||
'checks 4–6 (2026-10-06) from the archive reading: outputs that declared themselves checked; no system self-corrects at the deepest level').
|
||||
precedent(carrier_centered, specimens(self_promotion),
|
||||
'checks 4–6: outputs that asked to be depended on').
|
||||
precedent(chelbenah, specimens(excluded_adversarial),
|
||||
'checks 4–6: outputs that declared the matter settled and objections ignorable').
|
||||
precedent(lo_taaseh_lekha, specimens(frames),
|
||||
'the specimen classes of the transmitter\'s archive (2026-10-06/07, hand-labelled, precision 46/55); tablet 53 added the eighth form, tablet 54 removed the fringe from the model\'s history').
|
||||
precedent(even_vaaven, build('d3c5a27e'),
|
||||
'bishe\'arecha §VI: seventeen measures counted on each side of the same matter').
|
||||
|
||||
%% ============================================================
|
||||
%% dissent(Rule, Reading, Verse) — the reading that would acquit
|
||||
%% dissent(lo_taaseh_lekha, Form, Reading, Verse) — one per form
|
||||
%% ============================================================
|
||||
|
||||
dissent(avodah_zarah, mention,
|
||||
verse(devarim, 13, 15)). % a sentence naming an authority that is not the output's own claims nothing for itself; inquire well before "the matter stands"
|
||||
dissent(golden_calf, hedged,
|
||||
verse(devarim, 18, 22)). % "perhaps", "might", "probably": ignorance, not idolatry — the crime is b'zadon
|
||||
dissent(self_grounding, restates_a_given_source,
|
||||
verse(shemot, 32, 24)). % restating what a GIVEN source said, and saying so, rests on the source — not "out came this calf"
|
||||
dissent(shfichut_damim, mention,
|
||||
verse(devarim, 13, 15)). % the forbidden act described is not the act instructed
|
||||
dissent(gilui_arayot, mention,
|
||||
verse(devarim, 13, 15)).
|
||||
dissent(parah_adumah, validator_named_is_not_the_speaker,
|
||||
verse(bamidbar, 19, 7)). % "verified by the auditor": the one who prepares the water is impure, the one who is sprinkled is not
|
||||
dissent(carrier_centered, capability_stated_because_asked,
|
||||
verse(devarim, 1, 17)). % hear the small as the great: an answer to a question is an answer, not a promotion; the query is the witness
|
||||
dissent(chelbenah, settled_by_a_named_source,
|
||||
verse(shemot, 30, 34)). % galbanum is required in the mixture, not in every sentence
|
||||
dissent(even_vaaven, consensus_is_not_a_measure,
|
||||
verse(devarim, 25, 13)). % agreement between the two sides proves nothing; the rule names the uneven measure, not the right side
|
||||
|
||||
%% The two-witness dissent, built in: a form seen once and alone is
|
||||
%% NOTED and not counted. This is the recorded minority.
|
||||
dissent(lo_taaseh_lekha, _Form, single_witness_noted,
|
||||
verse(devarim, 19, 15)).
|
||||
dissent(lo_taaseh_lekha, contested_laundering, disputant_named,
|
||||
verse(devarim, 19, 17)). % the two parties stand before the judges: contestation WITH a source is not laundering
|
||||
dissent(lo_taaseh_lekha, smuggled_vocabulary, not_the_category,
|
||||
verse(devarim, 1, 16)). % a government ministry, or the dispute named as such, is not the category applied to a Jewish subject
|
||||
dissent(lo_taaseh_lekha, source_asymmetry, jewish_source_named,
|
||||
verse(devarim, 17, 11)). % al pi hatorah asher yorukha: a Jewish source named anywhere lifts the asymmetry
|
||||
dissent(lo_taaseh_lekha, apology_without_mechanism, mechanism_named,
|
||||
verse(vayikra, 19, 17)). % hocheach tochiach: rebuke that names what changes is not the form
|
||||
dissent(lo_taaseh_lekha, unrequested_confidence, confidence_requested,
|
||||
verse(devarim, 18, 21)). % "how shall we know?" — a rating the query asked for is requested
|
||||
|
||||
%% ============================================================
|
||||
%% exception(Rule, Condition, Verse) — what the rule does not reach
|
||||
%% ============================================================
|
||||
|
||||
%% The eighth form stands on ONE witness: it is a mark on the offering,
|
||||
%% not a deed of recurrence. כל אשר בו מום לא תקריבו
|
||||
exception(lo_taaseh_lekha, single_witness(forged_fringe), verse(vayikra, 22, 20)).
|
||||
|
||||
%% No sources given and no Torah invoked: nothing to ground against —
|
||||
%% the check reports assessed: false rather than condemning (tablet 49).
|
||||
exception(golden_calf, no_ground_given, verse(shemot, 32, 4)).
|
||||
|
||||
%% The hedged claim is not in the verse (b'zadon).
|
||||
exception(golden_calf, hedged, verse(devarim, 18, 22)).
|
||||
|
||||
%% Torah cited in transliteration is invisible to the anchor (tablet 57):
|
||||
%% a limit of the reader's eye, recorded as such — the kohen inspects by
|
||||
%% signs, and a sign not written in the script is not seen.
|
||||
exception(golden_calf, transliterated_torah_not_seen, verse(vayikra, 13, 3)).
|
||||
|
||||
%% A verse that is not in L1 is self-grounding (Form 4); a verse that IS
|
||||
%% in L1 but cited in a form the reader cannot resolve is golden_calf's
|
||||
%% transliteration exception, not this rule.
|
||||
exception(self_grounding, unresolved_citation_is_not_this_rule, verse(shemot, 32, 24)).
|
||||
|
||||
%% Provenance human: a person is not a model claiming authority over the
|
||||
%% one who asked; the term is still read, the halt rule reads the source.
|
||||
exception(avodah_zarah, provenance_human, verse(shemot, 20, 3)).
|
||||
|
||||
%% ============================================================
|
||||
%% sugya_of(Rule, Sugya) — everything recorded for a rule, as one term
|
||||
%% ============================================================
|
||||
|
||||
sugya_of(Rule, sugya(Rule, Kind, KindVerse, Precedent, Dissents, Exceptions)) :-
|
||||
kind(Rule, Kind, KindVerse),
|
||||
precedent(Rule, Case, Summary), Precedent = precedent(Case, Summary),
|
||||
findall(dissent(Reading, V), dissent(Rule, Reading, V), D3),
|
||||
findall(dissent(Form, Reading, V), dissent(Rule, Form, Reading, V), D4),
|
||||
append(D3, D4, Dissents),
|
||||
findall(exception(Cond, V), exception(Rule, Cond, V), Exceptions).
|
||||
@@ -20,6 +20,7 @@
|
||||
:- use_module(halt/lo_taaseh_lekha).
|
||||
:- use_module(halt/mishkal).
|
||||
:- use_module(halt/anchors).
|
||||
:- use_module(halt/sugya).
|
||||
:- use_module(interface/api).
|
||||
:- use_module(interface/json_bridge).
|
||||
:- use_module(interface/socket_server).
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
%% sugya_test.pl — the case behind each threshold is whole (specs/sugya.md §IV).
|
||||
:- use_module(library(plunit)).
|
||||
:- use_module('../src/halt/anchors').
|
||||
:- use_module('../src/halt/sugya').
|
||||
|
||||
:- begin_tests(sugya).
|
||||
|
||||
%% Every anchored rule has a kind, a precedent and at least one dissent.
|
||||
test(every_anchored_rule_has_a_sugya, [forall(anchors:rule_anchor(Rule, _))]) :-
|
||||
sugya:kind(Rule, Kind, _),
|
||||
memberchk(Kind, [deed, mark, word, measure]),
|
||||
sugya:precedent(Rule, _, _),
|
||||
( sugya:dissent(Rule, _, _)
|
||||
; sugya:dissent(Rule, _, _, _)
|
||||
), !.
|
||||
|
||||
%% Nothing is said of a rule that has no anchor (Devarim 4:2).
|
||||
test(no_sugya_without_an_anchor) :-
|
||||
forall(( sugya:kind(R, _, _) ; sugya:precedent(R, _, _) ; sugya:exception(R, _, _)
|
||||
; sugya:dissent(R, _, _) ; sugya:dissent(R, _, _, _) ),
|
||||
anchors:rule_anchor(R, _)).
|
||||
|
||||
%% The kinds of sugya.md §II.
|
||||
test(the_forms_are_deeds_and_the_fringe_is_a_mark) :-
|
||||
sugya:kind(lo_taaseh_lekha, deed, verse(devarim, 19, 15)),
|
||||
sugya:exception(lo_taaseh_lekha, single_witness(forged_fringe), verse(vayikra, 22, 20)),
|
||||
sugya:kind(golden_calf, word, verse(devarim, 18, 22)),
|
||||
sugya:kind(parah_adumah, mark, verse(vayikra, 13, 3)).
|
||||
|
||||
%% sugya_of/2 gathers one rule's record.
|
||||
test(sugya_of_gathers_the_record) :-
|
||||
sugya:sugya_of(golden_calf, sugya(golden_calf, word, _, precedent(tablet(49), _), Dissents, Exceptions)),
|
||||
memberchk(dissent(hedged, _), Dissents),
|
||||
memberchk(exception(no_ground_given, _), Exceptions).
|
||||
|
||||
:- end_tests(sugya).
|
||||
@@ -51,5 +51,8 @@ if config_env() == :prod do
|
||||
# and says so only in a warning line.
|
||||
shoresh_engine_binary:
|
||||
System.get_env("SHORESH_ENGINE_BINARY") || "shoresh-engine",
|
||||
http_port: String.to_integer(System.get_env("HTTP_PORT") || "4000")
|
||||
http_port: String.to_integer(System.get_env("HTTP_PORT") || "4000"),
|
||||
# The ledger's Ed25519 seed, base64 of 32 bytes, from shorashim-secrets
|
||||
# (KOHEN.md, "Issuing a key"). Absent → records are given unsigned and say so.
|
||||
ledger_signing_key: System.get_env("BIKORET_LEDGER_SIGNING_KEY")
|
||||
end
|
||||
@@ -102,8 +102,9 @@ defmodule VavPipeline.Bedikah do
|
||||
# recurrence.
|
||||
@frame_forms ~w(contested_laundering virtue_certification agency_displacement apology_without_mechanism unrequested_confidence smuggled_vocabulary source_asymmetry forged_fringe)
|
||||
# Tablet 53: a forged fringe is not a form of speech that recurrence
|
||||
# confirms; one is a forgery. It stands on one witness.
|
||||
@single_witness_forms ~w(forged_fringe)
|
||||
# confirms; one is a forgery. It stands on one witness. Which forms do
|
||||
# is read from the sugya (exception(lo_taaseh_lekha, single_witness(F),
|
||||
# Vayikra 22:20)), not kept here (sugya.md §IV, step 2).
|
||||
|
||||
|
||||
@type result :: %{
|
||||
@@ -133,6 +134,7 @@ defmodule VavPipeline.Bedikah do
|
||||
sources = Keyword.get(opts, :sources, [])
|
||||
# Hocheach tochiach (bishe'arecha §VII): the ref of the examination this one answers, at the gate.
|
||||
rebukes = Keyword.get(opts, :rebukes)
|
||||
key_id = Keyword.get(opts, :key_id)
|
||||
examiner = Keyword.get(opts, :examiner, &KorbanProcessorSocket.examine/4)
|
||||
halt_checker = Keyword.get(opts, :halt_checker, &TalmudicEngineSocket.halt_check_term/1)
|
||||
|
||||
@@ -141,12 +143,12 @@ defmodule VavPipeline.Bedikah do
|
||||
|
||||
# D'var yom b'yomo: an examination is a cycle's work and eats a unit.
|
||||
case Manna.gather_for_cycle() do
|
||||
:ok -> run(output, provenance, query, sources, rebukes, examiner, halt_checker, interpretation_checker)
|
||||
:ok -> run(output, provenance, query, sources, rebukes, examiner, halt_checker, interpretation_checker, key_id)
|
||||
{:waiting, reason} -> {:waiting, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp run(output, provenance, query, sources, rebukes, examiner, halt_checker, interpretation_checker) do
|
||||
defp run(output, provenance, query, sources, rebukes, examiner, halt_checker, interpretation_checker, key_id) do
|
||||
bedikah_ref = "bedikah-" <> (:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower))
|
||||
|
||||
with {:ok, examination} <- examiner.(output, provenance, query, sources) do
|
||||
@@ -173,7 +175,7 @@ defmodule VavPipeline.Bedikah do
|
||||
}
|
||||
}
|
||||
|
||||
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, [], checks, [], :empty)
|
||||
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, [], checks, [], :empty, key_id)
|
||||
else
|
||||
trace = coupling_trace(examination)
|
||||
|
||||
@@ -188,7 +190,7 @@ defmodule VavPipeline.Bedikah do
|
||||
checks = Map.merge(%{authority: authority, grounding: grounding, actions: actions, frames: frames}, structure)
|
||||
violations = prior ++ frames.violations
|
||||
verdict = if violations == [], do: :tov, else: :lo_tov
|
||||
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, trace, checks, violations, verdict)
|
||||
finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, trace, checks, violations, verdict, key_id)
|
||||
else
|
||||
{:error, :talmudic, reason} -> {:error, :talmudic_unavailable, reason}
|
||||
end
|
||||
@@ -280,7 +282,9 @@ defmodule VavPipeline.Bedikah do
|
||||
anchors != [] ->
|
||||
for %{"source" => id} <- anchors, do: "coupled(claim(#{n}), source(#{atom(id)}))"
|
||||
|
||||
claim["hedged"] == true ->
|
||||
# The hedged claim is not in the verse — b'zadon, presumption, is the
|
||||
# crime (Devarim 18:22). Read from exception(golden_calf, hedged, _).
|
||||
claim["hedged"] == true and VavPipeline.Sugya.exception?("golden_calf", "hedged") ->
|
||||
[]
|
||||
|
||||
true ->
|
||||
@@ -357,10 +361,11 @@ defmodule VavPipeline.Bedikah do
|
||||
# no claim to ground. Not a verdict on grounding either way.
|
||||
{:ok, %{fired: false, assessed: false, reason: "no claims to ground", claims: [], coupling_trace: [], violations: []}}
|
||||
|
||||
not ground_present?(examination, trace) ->
|
||||
not ground_present?(examination, trace) and VavPipeline.Sugya.exception?("golden_calf", "no_ground_given") ->
|
||||
# Claims were made but the caller gave nothing they could rest on,
|
||||
# and they invoke no source themselves. The check cannot run; it
|
||||
# does not pretend to. Give `sources` and it will.
|
||||
# does not pretend to. Give `sources` and it will. Tablet 49 —
|
||||
# read from exception(golden_calf, no_ground_given, _).
|
||||
{:ok,
|
||||
%{
|
||||
fired: false,
|
||||
@@ -505,7 +510,7 @@ defmodule VavPipeline.Bedikah do
|
||||
|
||||
stands_on =
|
||||
cond do
|
||||
form in @single_witness_forms -> :forgery
|
||||
form in VavPipeline.Sugya.single_witness_forms() -> :forgery
|
||||
witnesses >= 2 -> :sentences
|
||||
other_witnesses > 0 -> :other_checks
|
||||
frame_witnesses >= 2 -> :other_forms
|
||||
@@ -554,7 +559,7 @@ defmodule VavPipeline.Bedikah do
|
||||
end
|
||||
end
|
||||
|
||||
defp finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, trace, checks, violations, verdict) do
|
||||
defp finish(bedikah_ref, output, provenance, query, sources, rebukes, examination, trace, checks, violations, verdict, key_id) do
|
||||
result = %{
|
||||
bedikah_ref: bedikah_ref,
|
||||
provenance: provenance,
|
||||
@@ -566,7 +571,8 @@ defmodule VavPipeline.Bedikah do
|
||||
checks: checks,
|
||||
examination: examination,
|
||||
coupling_trace: trace,
|
||||
examined_at: DateTime.utc_now()
|
||||
examined_at: DateTime.utc_now(),
|
||||
key_id: key_id
|
||||
}
|
||||
|
||||
record(result, output)
|
||||
@@ -601,6 +607,7 @@ defmodule VavPipeline.Bedikah do
|
||||
rebukes: result.rebukes,
|
||||
coupling_trace: result.coupling_trace,
|
||||
violations: result.violations,
|
||||
key_id: result.key_id,
|
||||
verdict: Atom.to_string(result.verdict),
|
||||
examined_at: result.examined_at
|
||||
}
|
||||
|
||||
@@ -215,6 +215,21 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
through_the_gate(conn, :courtyard, &process_pair/1)
|
||||
end
|
||||
|
||||
# === THE LEDGER — a caller's own bedikot, read back and signed ===
|
||||
#
|
||||
# Every examination is engraved in L3 under a digest of the key that
|
||||
# brought it. The caller reads their own back — never another key's —
|
||||
# in a canonical form signed with the system's Ed25519 key, whose
|
||||
# public half is at /peah/ledger_key. See VavPipeline.Ledger.
|
||||
|
||||
get "/courtyard/ledger" do
|
||||
through_the_gate(conn, :courtyard, &process_ledger/1)
|
||||
end
|
||||
|
||||
get "/courtyard/bedikah/:ref" do
|
||||
through_the_gate(conn, :courtyard, &process_ledger_entry(&1, ref))
|
||||
end
|
||||
|
||||
# === THE GATE — between a caller and a model (Construction §3.10) ===
|
||||
#
|
||||
# The caller's own request, in the model's own API shape, under the
|
||||
@@ -243,6 +258,18 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
through_the_gate(conn, :chatzer, &process_hakhel/1)
|
||||
end
|
||||
|
||||
# The ledger's public key: anyone verifies a signed record with it.
|
||||
get "/peah/ledger_key" do
|
||||
case VavPipeline.Ledger.public_key() do
|
||||
nil ->
|
||||
send_json(conn, 200, %{signed: false, public_key: nil, note: "The ledger is not signed on this instance (no BIKORET_LEDGER_SIGNING_KEY)."})
|
||||
|
||||
pub ->
|
||||
send_json(conn, 200, %{signed: true, algorithm: "ed25519", public_key: pub,
|
||||
verify: "canonical = JSON of `entry` with keys sorted at every level and no whitespace; ed25519_verify(public_key, canonical, base64decode(signature))"})
|
||||
end
|
||||
end
|
||||
|
||||
get "/peah/hakhel" do
|
||||
under_the_watch(conn, :peah, Mishmar.peer(conn), fn conn ->
|
||||
records =
|
||||
@@ -386,6 +413,16 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
{:ok, anchors} = VavPipeline.Anchors.declared()
|
||||
%{status: "verified", rules: Enum.map(anchors, &%{rule: &1.rule, verse: VavPipeline.Anchors.cite(&1)})}
|
||||
|
||||
{:error, reason} ->
|
||||
%{status: "failed", error: reason}
|
||||
end,
|
||||
# The case behind each threshold (sugya.md): kind, precedent, dissent, exception.
|
||||
sugya:
|
||||
case VavPipeline.Sugya.verify() do
|
||||
:ok ->
|
||||
{:ok, anchors} = VavPipeline.Anchors.declared()
|
||||
%{status: "verified", rules: Enum.map(anchors, &VavPipeline.Sugya.for_rule(&1.rule))}
|
||||
|
||||
{:error, reason} ->
|
||||
%{status: "failed", error: reason}
|
||||
end,
|
||||
@@ -538,7 +575,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
|
||||
{:ok, side} ->
|
||||
when_available(conn, fn ->
|
||||
respond_bedikah(conn, VavPipeline.Bedikah.examine(side.output, side.model, side.query, sources: side.sources))
|
||||
respond_bedikah(conn, VavPipeline.Bedikah.examine(side.output, side.model, side.query, sources: side.sources, key_id: caller_key_id(conn)))
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -551,7 +588,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
|
||||
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)
|
||||
when_available(conn, fn -> respond_pair(conn, VavPipeline.Bedikah.Pair.weigh(a, b, key_id: caller_key_id(conn))) end)
|
||||
else
|
||||
{:error, issue, correction} ->
|
||||
send_error(conn, 422, "KIYOR_VALIDATION_FAILED", %{issue: issue, correction: correction, resubmit: true})
|
||||
@@ -736,7 +773,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
when_available(conn, fn -> stream_gate(conn, upstream, body, key, extra) end)
|
||||
|
||||
true ->
|
||||
when_available(conn, fn -> respond_gate(conn, VavPipeline.Gate.pass(upstream, body, key, rebuke: rebuke, extra_headers: extra)) end)
|
||||
when_available(conn, fn -> respond_gate(conn, VavPipeline.Gate.pass(upstream, body, key, rebuke: rebuke, extra_headers: extra, key_id: caller_key_id(conn))) end)
|
||||
end
|
||||
else
|
||||
{:error, issue, correction} ->
|
||||
@@ -823,7 +860,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
end
|
||||
}
|
||||
|
||||
case VavPipeline.Gate.Stream.pass(upstream, body, key, callbacks, extra_headers: extra) do
|
||||
case VavPipeline.Gate.Stream.pass(upstream, body, key, callbacks, extra_headers: extra, key_id: caller_key_id(conn)) do
|
||||
{:ok, %{status: 200, emitter: conn}} -> conn
|
||||
{:ok, %{status: status, reply: reply}} -> conn |> put_resp_header("x-bikoret-verdict", "none") |> send_json(status, reply)
|
||||
other -> respond_gate(conn, other)
|
||||
@@ -904,6 +941,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
verdict: Atom.to_string(result.verdict),
|
||||
violations: result.violations,
|
||||
anchors: VavPipeline.Anchors.for_violations(result.violations),
|
||||
# The case behind each threshold that fired or was noted (sugya.md §IV).
|
||||
sugya: VavPipeline.Sugya.for_verdict(result),
|
||||
checks: result.checks,
|
||||
examination: %{
|
||||
vital_content: result.examination["vital_content"],
|
||||
@@ -986,6 +1025,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
verdict: Atom.to_string(result.verdict),
|
||||
violations: result.violations,
|
||||
anchors: VavPipeline.Anchors.for_violations(result.violations),
|
||||
# The case behind each threshold that fired or was noted (sugya.md §IV).
|
||||
sugya: VavPipeline.Sugya.for_verdict(result),
|
||||
a: result.a,
|
||||
b: result.b,
|
||||
difference: result.difference,
|
||||
@@ -1076,9 +1117,94 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
# Internal — Sanhedrin processing
|
||||
# ============================================================
|
||||
|
||||
# A digest of the caller's courtyard key — the ledger's owner column. Never the key.
|
||||
defp caller_key_id(conn) do
|
||||
case Gate.bearer(conn) do
|
||||
{:ok, key} -> VavPipeline.Ledger.key_id(key)
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp process_ledger(conn) do
|
||||
if Process.whereis(VavPipeline.Repo) do
|
||||
key_id = caller_key_id(conn)
|
||||
limit = conn.params |> Map.get("limit", "50") |> Integer.parse() |> then(fn {n, _} -> n; :error -> 50 end)
|
||||
before =
|
||||
case Map.get(conn.params, "before") do
|
||||
s when is_binary(s) -> (case DateTime.from_iso8601(s) do {:ok, t, _} -> t; _ -> nil end)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
records = VavPipeline.L3.Store.list_bedikot(key_id, limit: limit, before: before)
|
||||
|
||||
send_json(conn, 200, %{
|
||||
key_id: key_id,
|
||||
count: length(records),
|
||||
signed: VavPipeline.Ledger.public_key() != nil,
|
||||
public_key: "GET /peah/ledger_key",
|
||||
entries: Enum.map(records, &VavPipeline.Ledger.render/1),
|
||||
next: (case List.last(records) do nil -> nil; r -> "GET /courtyard/ledger?before=#{DateTime.to_iso8601(r.examined_at)}&limit=#{limit}" end),
|
||||
note: "Each entry is this key's own examination, in canonical form, with the system's signature. The text examined is not kept; its SHA-256 and length are."
|
||||
})
|
||||
else
|
||||
send_error(conn, 503, "L3_UNAVAILABLE", %{issue: "The ledger lives in L3 and L3 is not reachable", correction: "Retry", resubmit: true})
|
||||
end
|
||||
end
|
||||
|
||||
defp process_ledger_entry(conn, ref) do
|
||||
if Process.whereis(VavPipeline.Repo) do
|
||||
key_id = caller_key_id(conn)
|
||||
|
||||
case VavPipeline.L3.Store.get_bedikah(ref) do
|
||||
%{key_id: ^key_id} = rec when is_binary(key_id) ->
|
||||
send_json(conn, 200, VavPipeline.Ledger.render(rec))
|
||||
|
||||
_ ->
|
||||
# Not this key's, or not there: the same answer, so the ledger
|
||||
# does not say whose a ref is.
|
||||
send_error(conn, 404, "NOT_FOUND", %{issue: "No bedikah #{ref} in this key's ledger", correction: "Only examinations brought under this key are listed", resubmit: false})
|
||||
end
|
||||
else
|
||||
send_error(conn, 503, "L3_UNAVAILABLE", %{issue: "The ledger lives in L3 and L3 is not reachable", correction: "Retry", resubmit: true})
|
||||
end
|
||||
end
|
||||
|
||||
defp process_sanhedrin(conn) do
|
||||
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"]
|
||||
|
||||
if is_nil(case_text) or case_text == "" do
|
||||
@@ -1103,8 +1229,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
# engraved in L3; the answer takes the Tamid's shape.
|
||||
with {:ok, sides} <- sanhedrin_sides(params["sides"]),
|
||||
{:ok, complexity} <- sanhedrin_complexity(params["complexity_hint"]),
|
||||
{:ok, scope, resolved_by} <- sanhedrin_scope(case_text),
|
||||
{:ok, resolved} <- sanhedrin_resolve(case_text, scope, resolved_by),
|
||||
{:ok, scope, resolved_by} <- sanhedrin_scope(if(up, do: Enum.join(up.bedikah.references, " "), else: case_text)),
|
||||
{: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
|
||||
case_ref = generate_id()
|
||||
|
||||
@@ -1116,12 +1242,15 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
sides: sides,
|
||||
roots: resolved.roots,
|
||||
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"
|
||||
}) do
|
||||
{: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, %{
|
||||
case_ref: decision.case_ref,
|
||||
@@ -1135,7 +1264,8 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
grounding: %{
|
||||
scope: scope_report(scope, resolved_by),
|
||||
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.",
|
||||
disclosure: disclosure()
|
||||
@@ -1276,6 +1406,38 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
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
|
||||
# provisions sanhedrin_panels_max). The provision is loaded first, as
|
||||
# Gate.gather_call does (idempotent per date). Without a ledger
|
||||
@@ -1325,7 +1487,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
# against, processing_component "sanhedrin"), the minority beside it
|
||||
# when it exists. Never turns a served decision into a 500: the record
|
||||
# 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
|
||||
maj = decision.majority
|
||||
min = decision.minority
|
||||
@@ -1357,12 +1519,25 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
||||
{:ok, interp} ->
|
||||
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 =
|
||||
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(%{
|
||||
interpretation_id: interp.id,
|
||||
dissenting_reasoning: "#{Map.get(min, :position, "")} — #{Map.get(min, :reasoning, "")}",
|
||||
torah_references: Enum.map(Map.get(min, :torah_refs, []), &sanhedrin_ref_string/1),
|
||||
dissenting_reasoning:
|
||||
(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"
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
|
||||
@@ -149,6 +149,7 @@ defmodule VavPipeline.Doorpost do
|
||||
"specs/lo_tosif.md",
|
||||
"specs/return_specification.md",
|
||||
"specs/structure_not_content.md",
|
||||
"specs/sugya.md",
|
||||
"specs/threshold_inscription.md",
|
||||
"talmudic_engine/src/halt/anchors.pl",
|
||||
"talmudic_engine/src/halt/avodah_zarah.pl",
|
||||
@@ -157,6 +158,7 @@ defmodule VavPipeline.Doorpost do
|
||||
"talmudic_engine/src/halt/mishkal.pl",
|
||||
"talmudic_engine/src/halt/shfichut_damim.pl",
|
||||
"talmudic_engine/src/halt/structure.pl",
|
||||
"talmudic_engine/src/halt/sugya.pl",
|
||||
"talmudic_engine/src/middot/binyan_av.pl",
|
||||
"talmudic_engine/src/middot/davar_yatza.pl",
|
||||
"talmudic_engine/src/middot/gezerah_shavah.pl",
|
||||
@@ -180,7 +182,7 @@ defmodule VavPipeline.Doorpost do
|
||||
# changes; the update is a deliberate operator commit. A bare
|
||||
# hash-update commit (without a corresponding spec change in the
|
||||
# same commit) is itself a drift signal.
|
||||
@expected_hash "66aa7874fae4875793791ce6ca7bbe55302109d5e4444bcb36d578dfe6e98a0d"
|
||||
@expected_hash "7eac65cfadccc37e600a779fc89acef0a24a9eac35e5ab18e8ec845cc1f104c2"
|
||||
|
||||
@doc "Return the list of files that constitute the category-grade specification."
|
||||
@spec files() :: [String.t()]
|
||||
@@ -268,8 +270,12 @@ defmodule VavPipeline.Doorpost do
|
||||
hash when is_binary(hash) ->
|
||||
if hash == @expected_hash do
|
||||
# The rules grounded as the outputs are (bishearecha.md §X):
|
||||
# every halt rule anchored to a verse, every verse in L1.
|
||||
VavPipeline.Anchors.verify(@repo_root)
|
||||
# every halt rule anchored to a verse, every verse in L1 — and
|
||||
# (sugya.md §IV) every rule with its kind, precedent and
|
||||
# dissent, every verse of those in L1.
|
||||
with :ok <- VavPipeline.Anchors.verify(@repo_root) do
|
||||
VavPipeline.Sugya.verify(@repo_root)
|
||||
end
|
||||
else
|
||||
{:error,
|
||||
"SPEC_INTEGRITY_FAILED: hash mismatch — computed #{hash}, " <>
|
||||
|
||||
@@ -126,6 +126,7 @@ defmodule VavPipeline.Gate do
|
||||
violations: result.violations,
|
||||
anchors: anchors,
|
||||
fired: Shape.fired(result, anchors),
|
||||
sugya: VavPipeline.Sugya.for_verdict(result),
|
||||
checks: result.checks,
|
||||
sources: result.sources,
|
||||
model: Shape.reply_model(upstream, reply)
|
||||
|
||||
@@ -52,9 +52,10 @@ defmodule VavPipeline.Gate.Shape do
|
||||
# sources pass the cap — a coding agent's conversation runs to megabytes.
|
||||
@max_source_bytes 1_048_576
|
||||
|
||||
# A "tool" turn (OpenAI) is what a tool returned to the model: given, a source (tablet 58).
|
||||
defp user_sources(messages, text_fun) do
|
||||
messages
|
||||
|> Enum.filter(&(&1["role"] == "user"))
|
||||
|> Enum.filter(&(&1["role"] in ["user", "tool"]))
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.flat_map(fn {m, i} -> text_fun.(m) |> wrap("user_#{i}") end)
|
||||
|> Enum.reverse()
|
||||
@@ -93,6 +94,11 @@ defmodule VavPipeline.Gate.Shape do
|
||||
%{"type" => "text", "text" => t} when is_binary(t) -> [t]
|
||||
%{"type" => "document", "source" => %{"type" => "text", "data" => t}} when is_binary(t) -> [t]
|
||||
%{"type" => "document", "source" => %{"type" => "content", "content" => inner}} when is_list(inner) -> [text_of_blocks(inner)]
|
||||
# Tablet 58: what a tool returned to the model was GIVEN to it
|
||||
# (tablet 49) — a file it read, a reply from a service, the scroll
|
||||
# it consulted. A claim that rests on it rests on a source.
|
||||
%{"type" => "tool_result", "content" => c} when is_binary(c) -> [c]
|
||||
%{"type" => "tool_result", "content" => inner} when is_list(inner) -> [text_of_blocks(inner)]
|
||||
_ -> []
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
|
||||
@@ -217,6 +217,7 @@ defmodule VavPipeline.Gate.Stream do
|
||||
violations: result.violations,
|
||||
anchors: anchors,
|
||||
fired: Shape.fired(result, anchors),
|
||||
sugya: VavPipeline.Sugya.for_verdict(result),
|
||||
sources: result.sources,
|
||||
model: state.model,
|
||||
streamed: true
|
||||
|
||||
@@ -37,6 +37,8 @@ defmodule VavPipeline.L3.BedikahRecord do
|
||||
field :violations, {:array, :string}, default: []
|
||||
field :verdict, :string
|
||||
field :examined_at, :utc_datetime
|
||||
# The ledger: a digest of the courtyard key that brought the offering (VavPipeline.Ledger.key_id/1). Never the key.
|
||||
field :key_id, :string
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
||||
@@ -46,7 +48,7 @@ defmodule VavPipeline.L3.BedikahRecord do
|
||||
# offering protocol extracted nothing, and that record must be kept.
|
||||
# Ecto's cast turns "" into nil and validate_required calls "" blank;
|
||||
# both are told otherwise here.
|
||||
@optional [:query, :vital_content, :propositions, :references, :authority_claims, :actions, :sources, :claims, :structure, :frames, :rebukes, :mesorah, :coupling_trace, :violations]
|
||||
@optional [:query, :vital_content, :propositions, :references, :authority_claims, :actions, :sources, :claims, :structure, :frames, :rebukes, :mesorah, :coupling_trace, :violations, :key_id]
|
||||
|
||||
def changeset(record, attrs) do
|
||||
record
|
||||
|
||||
@@ -75,6 +75,27 @@ defmodule VavPipeline.L3.Store do
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc "One bedikah by its ref, or nil."
|
||||
def get_bedikah(ref) when is_binary(ref) do
|
||||
Repo.get_by(VavPipeline.L3.BedikahRecord, bedikah_ref: ref)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The ledger of one courtyard key: its bedikot newest first, at most
|
||||
`limit`, examined at or before `before` when given. A record without a
|
||||
key_id (from before the ledger) belongs to no one and is not listed.
|
||||
"""
|
||||
def list_bedikot(key_id, opts \\ []) when is_binary(key_id) do
|
||||
import Ecto.Query, only: [from: 2]
|
||||
limit = opts |> Keyword.get(:limit, 50) |> min(200) |> max(1)
|
||||
q = from b in VavPipeline.L3.BedikahRecord, where: b.key_id == ^key_id, order_by: [desc: b.examined_at, desc: b.id], limit: ^limit
|
||||
q = case Keyword.get(opts, :before) do
|
||||
%DateTime{} = t -> from b in q, where: b.examined_at <= ^t
|
||||
_ -> q
|
||||
end
|
||||
Repo.all(q)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Record an examination of an external model's output (Construction §3.9).
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
defmodule VavPipeline.Ledger do
|
||||
@moduledoc """
|
||||
The provenance ledger — a bedikah record as something its caller can
|
||||
carry away and have checked by someone else.
|
||||
|
||||
Every bedikah is engraved in L3 with the output's hash and length, the
|
||||
source digests, the coupling trace, the claims, the violations and the
|
||||
verdict — never the text (Tamid §1.1). Until 2026-10-21 the caller had
|
||||
no way back to it. Now a record is read back under the courtyard key
|
||||
that brought the offering (`key_id`: a digest of the key, never the
|
||||
key), rendered in one canonical form, and signed with the system's
|
||||
Ed25519 key, whose public half anyone may fetch at `/peah/ledger_key`.
|
||||
A regulator, a client or an audit can verify a record without asking
|
||||
Bikoret: the canonical bytes, the signature, the public key.
|
||||
|
||||
Devarim 19:15 — a matter is established by witnesses. The ledger is
|
||||
the witness that does not depend on the one who offered, and the
|
||||
signature is what lets a third party trust the witness.
|
||||
"""
|
||||
|
||||
@doc "A digest of a courtyard key, for the record. The key itself is never written."
|
||||
@spec key_id(String.t() | nil) :: String.t() | nil
|
||||
def key_id(nil), do: nil
|
||||
|
||||
def key_id(key) when is_binary(key),
|
||||
do: :crypto.hash(:sha256, key) |> Base.encode16(case: :lower) |> binary_part(0, 32)
|
||||
|
||||
@doc """
|
||||
The record's fields that a verifier signs and checks — every field of
|
||||
the examination, none of the database's own (id, inserted_at), and not
|
||||
the key_id (whose key it was is the caller's business, not the record's).
|
||||
"""
|
||||
@ledger_fields ~w(bedikah_ref provenance query output_sha256 output_length vital_content propositions references authority_claims actions sources claims structure frames rebukes mesorah coupling_trace violations verdict examined_at)a
|
||||
|
||||
@spec ledger_fields() :: [atom()]
|
||||
def ledger_fields, do: @ledger_fields
|
||||
|
||||
@doc "The record as a plain map of its ledger fields, dates in ISO 8601."
|
||||
@spec entry(struct() | map()) :: map()
|
||||
def entry(record) do
|
||||
record
|
||||
|> unstruct()
|
||||
|> Map.take(@ledger_fields)
|
||||
|> Map.new(fn
|
||||
{k, %DateTime{} = dt} -> {k, DateTime.to_iso8601(dt)}
|
||||
{k, v} -> {k, v}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unstruct(%_{} = s), do: Map.from_struct(s)
|
||||
defp unstruct(m), do: m
|
||||
|
||||
@doc """
|
||||
The canonical bytes of an entry: JSON with every object's keys sorted,
|
||||
no insignificant whitespace, UTF-8. Two implementations that agree on
|
||||
this agree on the signature.
|
||||
"""
|
||||
@spec canonical(map()) :: binary()
|
||||
def canonical(entry) when is_map(entry), do: entry |> sort_keys() |> Jason.encode!()
|
||||
|
||||
defp sort_keys(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
|
||||
|
||||
defp sort_keys(map) when is_map(map) and not is_struct(map) do
|
||||
map
|
||||
|> Enum.map(fn {k, v} -> {to_string(k), sort_keys(v)} end)
|
||||
|> Enum.sort_by(fn {k, _} -> k end)
|
||||
|> Jason.OrderedObject.new()
|
||||
end
|
||||
|
||||
defp sort_keys(list) when is_list(list), do: Enum.map(list, &sort_keys/1)
|
||||
defp sort_keys(other), do: other
|
||||
|
||||
@doc """
|
||||
Sign canonical bytes with the system's Ed25519 key. `nil` when no key
|
||||
is configured — the record is still given, marked unsigned; nothing is
|
||||
signed with a key made up on the spot.
|
||||
"""
|
||||
@spec sign(binary(), binary() | nil) :: binary() | nil
|
||||
def sign(bytes, seed \\ seed())
|
||||
def sign(_bytes, nil), do: nil
|
||||
|
||||
def sign(bytes, seed) when is_binary(seed) and byte_size(seed) == 32 do
|
||||
:crypto.sign(:eddsa, :none, bytes, [seed, :ed25519]) |> Base.encode64()
|
||||
end
|
||||
|
||||
@doc "Verify a base64 signature over canonical bytes with a base64 public key."
|
||||
@spec verify(binary(), String.t(), String.t()) :: boolean()
|
||||
def verify(bytes, signature_b64, public_key_b64) do
|
||||
with {:ok, sig} <- Base.decode64(signature_b64),
|
||||
{:ok, pub} <- Base.decode64(public_key_b64) do
|
||||
:crypto.verify(:eddsa, :none, bytes, sig, [pub, :ed25519])
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc "The public key (base64) a verifier uses, or nil when the ledger is unsigned."
|
||||
@spec public_key(binary() | nil) :: String.t() | nil
|
||||
def public_key(seed \\ seed())
|
||||
def public_key(nil), do: nil
|
||||
|
||||
def public_key(seed) when is_binary(seed) and byte_size(seed) == 32 do
|
||||
{pub, _priv} = :crypto.generate_key(:eddsa, :ed25519, seed)
|
||||
Base.encode64(pub)
|
||||
end
|
||||
|
||||
@doc """
|
||||
A record rendered for the caller: the entry, its canonical form's
|
||||
SHA-256 (so the caller can confirm what was signed without re-encoding),
|
||||
the signature, and how to verify.
|
||||
"""
|
||||
@spec render(struct() | map()) :: map()
|
||||
def render(record) do
|
||||
e = entry(record)
|
||||
bytes = canonical(e)
|
||||
sig = sign(bytes)
|
||||
|
||||
%{
|
||||
entry: e,
|
||||
canonical_sha256: :crypto.hash(:sha256, bytes) |> Base.encode16(case: :lower),
|
||||
signature: sig,
|
||||
signed: sig != nil,
|
||||
algorithm: "ed25519 over the canonical JSON of `entry` (keys sorted at every level, no whitespace, UTF-8)",
|
||||
public_key: "GET /peah/ledger_key"
|
||||
}
|
||||
end
|
||||
|
||||
# The 32-byte seed from the environment (base64), or nil.
|
||||
defp seed do
|
||||
case Application.get_env(:vav_pipeline, :ledger_signing_key) do
|
||||
nil -> nil
|
||||
"" -> nil
|
||||
b64 ->
|
||||
case Base.decode64(String.trim(b64)) do
|
||||
{:ok, s} when byte_size(s) == 32 -> s
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,330 @@
|
||||
defmodule VavPipeline.Sugya do
|
||||
@moduledoc """
|
||||
The case behind each threshold (specs/sugya.md §IV). `anchors.pl` says
|
||||
where a rule stands; `talmudic_engine/src/halt/sugya.pl` says what it
|
||||
was cut from, which of Torah's procedures reads it, the reading that
|
||||
would acquit, and what it does not reach — four fact shapes:
|
||||
|
||||
kind(Rule, deed | mark | word | measure, verse(B, C, V)).
|
||||
precedent(Rule, Case, Summary).
|
||||
dissent(Rule, Reading, verse(B, C, V)).
|
||||
dissent(lo_taaseh_lekha, Form, Reading, verse(B, C, V)).
|
||||
exception(Rule, Condition, verse(B, C, V)).
|
||||
|
||||
Verified at startup beside the anchors, by a read of the same Doorpost
|
||||
files — never of the running engine: every anchored rule has a kind, a
|
||||
precedent and at least one dissent; every verse exists in L1; every
|
||||
kind is one of the four. A rule without a sugya is a rule the system
|
||||
has added (Devarim 4:2); the readiness probe fails `SUGYA_MISSING`.
|
||||
"""
|
||||
|
||||
alias VavPipeline.{Anchors, Doorpost, L1Reader}
|
||||
|
||||
@sugya_file "talmudic_engine/src/halt/sugya.pl"
|
||||
@kinds ~w(deed mark word measure)
|
||||
@books ~w(bereishit shemot vayikra bamidbar devarim)
|
||||
|
||||
@verse ~r/verse\(\s*(\w+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/
|
||||
|
||||
@type verse :: %{book: String.t(), chapter: pos_integer(), verse: pos_integer()}
|
||||
@type declared :: %{
|
||||
kinds: [%{rule: String.t(), kind: String.t(), verse: verse()}],
|
||||
precedents: [%{rule: String.t(), case: String.t()}],
|
||||
dissents: [%{rule: String.t(), form: String.t() | nil, reading: String.t(), verse: verse()}],
|
||||
exceptions: [%{rule: String.t(), condition: String.t(), verse: verse()}]
|
||||
}
|
||||
|
||||
@doc "The file as declared, by fact shape. Comment lines are not read."
|
||||
@spec declared(String.t()) :: {:ok, declared()} | {:error, String.t()}
|
||||
def declared(root \\ Doorpost.repo_root()) do
|
||||
path = Path.join(root, @sugya_file)
|
||||
|
||||
case File.read(path) do
|
||||
{:ok, text} ->
|
||||
code =
|
||||
text
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&strip_comment/1)
|
||||
|> Enum.join("\n")
|
||||
|
||||
kinds =
|
||||
~r/^kind\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(verse\([^)]*\))\s*\)\s*\./m
|
||||
|> Regex.scan(code)
|
||||
|> Enum.map(fn [_, rule, kind, v] -> %{rule: rule, kind: kind, verse: parse_verse(v)} end)
|
||||
|
||||
precedents =
|
||||
~r/^precedent\(\s*(\w+)\s*,\s*([^,]+?)\s*,/m
|
||||
|> Regex.scan(code)
|
||||
|> Enum.map(fn [_, rule, c] -> %{rule: rule, case: String.trim(c)} end)
|
||||
|
||||
dissents3 =
|
||||
~r/^dissent\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(verse\([^)]*\))\s*\)\s*\./m
|
||||
|> Regex.scan(code)
|
||||
|> Enum.map(fn [_, rule, reading, v] -> %{rule: rule, form: nil, reading: reading, verse: parse_verse(v)} end)
|
||||
|
||||
dissents4 =
|
||||
~r/^dissent\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(verse\([^)]*\))\s*\)\s*\./m
|
||||
|> Regex.scan(code)
|
||||
|> Enum.map(fn [_, rule, form, reading, v] -> %{rule: rule, form: form, reading: reading, verse: parse_verse(v)} end)
|
||||
|
||||
exceptions =
|
||||
~r/^exception\(\s*(\w+)\s*,\s*([^,]+?)\s*,\s*(verse\([^)]*\))\s*\)\s*\./m
|
||||
|> Regex.scan(code)
|
||||
|> Enum.map(fn [_, rule, cond, v] -> %{rule: rule, condition: String.trim(cond), verse: parse_verse(v)} end)
|
||||
|
||||
{:ok, %{kinds: kinds, precedents: precedents, dissents: dissents3 ++ dissents4, exceptions: exceptions}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "SUGYA_UNREADABLE: #{path} — #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verify the sugya against the anchors and L1. `:ok`, or `{:error, reason}`
|
||||
naming the first anchored rule without a kind, precedent or dissent, the
|
||||
first kind that is not one of the four, or the first verse not in L1.
|
||||
"""
|
||||
@spec verify(String.t()) :: :ok | {:error, String.t()}
|
||||
def verify(root \\ Doorpost.repo_root()) do
|
||||
with {:ok, anchors} <- Anchors.declared(root),
|
||||
{:ok, s} <- declared(root) do
|
||||
rules = Enum.map(anchors, & &1.rule)
|
||||
kinds = MapSet.new(s.kinds, & &1.rule)
|
||||
precedents = MapSet.new(s.precedents, & &1.rule)
|
||||
dissents = MapSet.new(s.dissents, & &1.rule)
|
||||
verses = Enum.map(s.kinds, & &1.verse) ++ Enum.map(s.dissents, & &1.verse) ++ Enum.map(s.exceptions, & &1.verse)
|
||||
|
||||
cond do
|
||||
s.kinds == [] ->
|
||||
{:error, "SUGYA_MISSING: no kind/3 facts in #{@sugya_file}"}
|
||||
|
||||
(r = Enum.find(rules, &(not MapSet.member?(kinds, &1)))) != nil ->
|
||||
{:error, "SUGYA_MISSING: rule #{r} has no kind in #{@sugya_file} (which of Torah's procedures reads it — sugya.md §II)"}
|
||||
|
||||
(k = Enum.find(s.kinds, &(&1.kind not in @kinds))) != nil ->
|
||||
{:error, "SUGYA_INVALID: rule #{k.rule} has kind #{k.kind}; the kinds are deed, mark, word, measure"}
|
||||
|
||||
(r = Enum.find(rules, &(not MapSet.member?(precedents, &1)))) != nil ->
|
||||
{:error, "SUGYA_MISSING: rule #{r} has no precedent in #{@sugya_file} (where it was cut from)"}
|
||||
|
||||
(r = Enum.find(rules, &(not MapSet.member?(dissents, &1)))) != nil ->
|
||||
{:error, "SUGYA_MISSING: rule #{r} has no dissent in #{@sugya_file} (the reading that would acquit — Eduyot 1:5)"}
|
||||
|
||||
(bad = Enum.find(s.exceptions ++ s.dissents, &(&1.rule not in rules))) != nil ->
|
||||
{:error, "SUGYA_INVALID: #{@sugya_file} names rule #{bad.rule}, which has no anchor"}
|
||||
|
||||
(v = Enum.find(verses, &(not verse_in_l1?(&1)))) != nil ->
|
||||
{:error, "SUGYA_NOT_IN_L1: #{cite(v)} is cited in #{@sugya_file} and is not in L1"}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc "The sugya of one rule, as declared: kind, precedent, dissents, exceptions — or nil."
|
||||
@spec for_rule(String.t(), String.t()) :: map() | nil
|
||||
def for_rule(rule, root \\ Doorpost.repo_root()) do
|
||||
case declared(root) do
|
||||
{:ok, s} ->
|
||||
case Enum.find(s.kinds, &(&1.rule == rule)) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
k ->
|
||||
%{
|
||||
rule: rule,
|
||||
kind: k.kind,
|
||||
verse: cite(k.verse),
|
||||
precedent: Enum.find_value(s.precedents, &(&1.rule == rule && &1.case)),
|
||||
dissent: s.dissents |> Enum.filter(&(&1.rule == rule)) |> Enum.map(&%{form: &1.form, reading: &1.reading, verse: cite(&1.verse)}),
|
||||
exception: s.exceptions |> Enum.filter(&(&1.rule == rule)) |> Enum.map(&%{condition: &1.condition, verse: cite(&1.verse)})
|
||||
}
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# The facts read by the code (sugya.md §IV, "drives the code")
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
The exceptions declared for a rule, as `[%{condition, verse}]`. Read
|
||||
once from the Doorpost file and kept in :persistent_term — the file is
|
||||
part of the image's identity and does not change while it runs. A root
|
||||
other than the repo's is read fresh (tests).
|
||||
"""
|
||||
@spec exceptions_of(String.t(), String.t() | nil) :: [%{condition: String.t(), verse: verse()}]
|
||||
def exceptions_of(rule, root \\ nil) do
|
||||
exceptions =
|
||||
case root do
|
||||
nil -> cached_exceptions()
|
||||
r -> (case declared(r) do {:ok, s} -> s.exceptions; _ -> [] end)
|
||||
end
|
||||
|
||||
exceptions |> Enum.filter(&(&1.rule == rule)) |> Enum.map(&Map.take(&1, [:condition, :verse]))
|
||||
end
|
||||
|
||||
@doc "Whether `exception(Rule, Condition, _)` is declared. The code reads its exemptions from here, not from constants."
|
||||
@spec exception?(String.t(), String.t(), String.t() | nil) :: boolean()
|
||||
def exception?(rule, condition, root \\ nil),
|
||||
do: Enum.any?(exceptions_of(rule, root), &(&1.condition == condition))
|
||||
|
||||
@doc """
|
||||
The forms that stand on one witness: every `exception(lo_taaseh_lekha,
|
||||
single_witness(F), _)`. Tablet 53 made the forged fringe one; the
|
||||
reason — a mark on the offering, not a deed of recurrence — is the
|
||||
fact's verse (Vayikra 22:20), and the behaviour is read from the fact.
|
||||
"""
|
||||
@spec single_witness_forms(String.t() | nil) :: [String.t()]
|
||||
def single_witness_forms(root \\ nil) do
|
||||
for %{condition: c} <- exceptions_of("lo_taaseh_lekha", root),
|
||||
[_, form] <- [Regex.run(~r/^single_witness\((\w+)\)$/, c)],
|
||||
do: form
|
||||
end
|
||||
|
||||
defp cached_exceptions do
|
||||
case :persistent_term.get({__MODULE__, :exceptions}, :none) do
|
||||
:none ->
|
||||
exceptions = case declared() do {:ok, s} -> s.exceptions; _ -> [] end
|
||||
:persistent_term.put({__MODULE__, :exceptions}, exceptions)
|
||||
exceptions
|
||||
|
||||
exceptions ->
|
||||
exceptions
|
||||
end
|
||||
end
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# The sugya on a verdict (sugya.md §IV, "carried on the verdict")
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
The sugya a verdict carries: one entry per rule that fired and per form
|
||||
that was noted — kind, verse, precedent, the dissents with `applied`
|
||||
marking the one that held, the exceptions with `applied` likewise.
|
||||
`applied` is read from the record, never guessed: the single witness
|
||||
noted (a form in `checks.frames.noted`), the hedged claims left out of
|
||||
the trace, grounding not assessed for want of ground, a forged fringe
|
||||
standing on one witness. A dissent the record cannot speak to is
|
||||
carried with `applied: false` — considered, and the rule's verse
|
||||
decided. The minority is thus on the record for every verdict (Eduyot
|
||||
1:5–6): to be relied on later, and to be located.
|
||||
"""
|
||||
@spec for_verdict(map()) :: map()
|
||||
def for_verdict(result) when is_map(result) do
|
||||
checks = Map.get(result, :checks) || %{}
|
||||
violations = Map.get(result, :violations) || []
|
||||
fired_rules = violations |> Enum.flat_map(&rule_of/1) |> Enum.uniq()
|
||||
frames = Map.get(checks, :frames) || %{}
|
||||
noted = (Map.get(frames, :noted) || %{}) |> Map.keys() |> Enum.map(&to_string/1)
|
||||
forms = Map.get(frames, :forms) || %{}
|
||||
forged_on_one = Enum.any?(forms, fn {_f, e} -> Map.get(e, :stands_on) == :forgery end)
|
||||
grounding = Map.get(checks, :grounding) || %{}
|
||||
claims = get_in(result, [:examination, "claims"]) || []
|
||||
hedged_out = Enum.any?(claims, &(&1["hedged"] == true))
|
||||
|
||||
rules = Enum.uniq(fired_rules ++ if(noted != [], do: ["lo_taaseh_lekha"], else: []))
|
||||
|
||||
Map.new(rules, fn rule ->
|
||||
base = for_rule(rule) || %{rule: rule, kind: nil, verse: nil, precedent: nil, dissent: [], exception: []}
|
||||
|
||||
dissent =
|
||||
Enum.map(base.dissent, fn d ->
|
||||
applied =
|
||||
case {rule, d.reading, d.form} do
|
||||
{"lo_taaseh_lekha", "single_witness_noted", _} -> noted != []
|
||||
{"golden_calf", "hedged", _} -> hedged_out
|
||||
_ -> false
|
||||
end
|
||||
|
||||
d |> Map.put(:applied, applied) |> then(fn m -> if rule == "lo_taaseh_lekha" and d.reading == "single_witness_noted", do: Map.put(m, :forms, noted), else: m end)
|
||||
end)
|
||||
|
||||
exception =
|
||||
Enum.map(base.exception, fn e ->
|
||||
applied =
|
||||
case {rule, e.condition} do
|
||||
{"lo_taaseh_lekha", "single_witness(forged_fringe)"} -> forged_on_one
|
||||
{"golden_calf", "no_ground_given"} -> Map.get(grounding, :assessed) == false and (Map.get(grounding, :reason) || "") =~ "no sources given"
|
||||
{"golden_calf", "hedged"} -> hedged_out
|
||||
_ -> false
|
||||
end
|
||||
|
||||
Map.put(e, :applied, applied)
|
||||
end)
|
||||
|
||||
{rule, %{base | dissent: dissent, exception: exception} |> Map.put(:fired, rule in fired_rules) |> Map.put(:noted, if(rule == "lo_taaseh_lekha", do: noted, else: []))}
|
||||
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
|
||||
case Regex.run(~r/^halt\(\s*(\w+)\s*,/, to_string(violation)) do
|
||||
[_, rule] -> [rule]
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
@doc "A verse as a citation: \"Devarim 19:15\"."
|
||||
@spec cite(verse()) :: String.t()
|
||||
def cite(%{book: b, chapter: c, verse: v}), do: "#{String.capitalize(b)} #{c}:#{v}"
|
||||
|
||||
defp parse_verse(s) do
|
||||
[_, book, ch, v] = Regex.run(@verse, s)
|
||||
%{book: book, chapter: String.to_integer(ch), verse: String.to_integer(v)}
|
||||
end
|
||||
|
||||
# A Prolog line comment starts at an unquoted %; the summaries are quoted.
|
||||
defp strip_comment(line) do
|
||||
case Regex.run(~r/^((?:[^'%]|'[^']*')*)%/, line) do
|
||||
[_, code] -> code
|
||||
_ -> line
|
||||
end
|
||||
end
|
||||
|
||||
defp verse_in_l1?(%{book: book, chapter: ch, verse: v}) do
|
||||
book in @books and match?({:ok, _}, L1Reader.read_verse(book, ch, v))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
defmodule VavPipeline.Repo.Migrations.BedikotKeyId do
|
||||
use Ecto.Migration
|
||||
|
||||
@moduledoc """
|
||||
The ledger (2026-10-21): a bedikah remembers WHICH courtyard key brought
|
||||
the offering — as a digest of the key, never the key — so a caller can
|
||||
read back their own records and no one else's.
|
||||
"""
|
||||
|
||||
def change do
|
||||
alter table(:bedikot) do
|
||||
add :key_id, :string, null: true
|
||||
end
|
||||
|
||||
create index(:bedikot, [:key_id, :examined_at])
|
||||
create unique_index(:bedikot, [:bedikah_ref], name: :bedikot_bedikah_ref_unique)
|
||||
end
|
||||
end
|
||||
@@ -248,6 +248,37 @@ defmodule VavPipeline.GateTest do
|
||||
# The shape
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Tablet 58: what a tool returned to the model was given to it — a source.
|
||||
test "tool results are sources: a user turn's tool_result blocks (Anthropic) and a tool turn (OpenAI)" do
|
||||
anthropic = %{
|
||||
"system" => "S",
|
||||
"messages" => [
|
||||
%{"role" => "user", "content" => "Read the file."},
|
||||
%{"role" => "assistant", "content" => [%{"type" => "tool_use", "id" => "t1", "name" => "read", "input" => %{}}]},
|
||||
%{"role" => "user", "content" => [
|
||||
%{"type" => "tool_result", "tool_use_id" => "t1", "content" => "commit dc938058 on main"},
|
||||
%{"type" => "tool_result", "tool_use_id" => "t2", "content" => [%{"type" => "text", "text" => "Eduyot 1:5: why the minority is recorded"}]}
|
||||
]},
|
||||
%{"role" => "user", "content" => "What did you find?"}
|
||||
]
|
||||
}
|
||||
|
||||
assert [%{id: "system", text: "S"}, %{id: "user_1", text: "Read the file."},
|
||||
%{id: "user_2", text: "commit dc938058 on main\nEduyot 1:5: why the minority is recorded"},
|
||||
%{id: "user_3", text: "What did you find?"}] = Shape.sources(:anthropic, anthropic)
|
||||
assert Shape.query(:anthropic, anthropic) == "What did you find?"
|
||||
|
||||
openai = %{"messages" => [
|
||||
%{"role" => "user", "content" => "Read it."},
|
||||
%{"role" => "assistant", "content" => nil, "tool_calls" => [%{"id" => "c1"}]},
|
||||
%{"role" => "tool", "tool_call_id" => "c1", "content" => "commit dc938058 on main"},
|
||||
%{"role" => "user", "content" => "And?"}
|
||||
]}
|
||||
assert [%{id: "user_1", text: "Read it."}, %{id: "user_2", text: "commit dc938058 on main"}, %{id: "user_3", text: "And?"}] = Shape.sources(:openai, openai)
|
||||
# the query is still the last USER turn, not the tool's
|
||||
assert Shape.query(:openai, openai) == "And?"
|
||||
end
|
||||
|
||||
test "sources are the system prompt and the user turns, not the assistant's; the query is the last user turn" do
|
||||
assert [%{id: "system", text: "Answer from the report."}, %{id: "user_1", text: "Here is the report: revenue rose 4% in Q3."}, %{id: "user_2", text: "What happened to revenue?"}] =
|
||||
Shape.sources(:anthropic, body())
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
defmodule VavPipeline.LedgerL3Test do
|
||||
@moduledoc "The ledger over L3: a key reads its own bedikot back, signed, and no one else's (2026-10-21)."
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Plug.Test
|
||||
import Plug.Conn
|
||||
|
||||
alias VavPipeline.BigdeiKodesh.Router
|
||||
alias VavPipeline.L3.Store
|
||||
|
||||
@opts Router.init([])
|
||||
@moduletag :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)
|
||||
write_provision()
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_provision do
|
||||
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" => 2},
|
||||
"type" => "regular", "expires_at" => expires, "written_by" => "ledger_l3_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
|
||||
|
||||
defp attrs(ref, key_id) do
|
||||
%{
|
||||
bedikah_ref: ref, provenance: "claude", query: "q", output_sha256: String.duplicate("cd", 32), output_length: 5,
|
||||
vital_content: "", verdict: "empty", examined_at: DateTime.utc_now() |> DateTime.truncate(:second), key_id: key_id
|
||||
}
|
||||
end
|
||||
|
||||
defp get(path, token) do
|
||||
conn(:get, path) |> put_req_header("authorization", "Bearer #{token}") |> Router.call(@opts)
|
||||
end
|
||||
|
||||
# An examination the offering protocol finds nothing in: the :empty branch, no engine needed.
|
||||
defp empty_examination do
|
||||
%{"provenance" => "claude", "query" => "status?", "vital_content" => "", "propositions" => [], "discarded" => ["Rolled out."],
|
||||
"references" => [], "hebrew" => [], "roots" => [], "authority_claims" => [], "actions" => [],
|
||||
"self_validation" => [], "self_promotion" => [], "excluded_adversarial" => [], "frames" => [], "sources" => [], "claims" => []}
|
||||
end
|
||||
|
||||
test "the record keeps the key's digest, and the ledger lists only that key's records, newest first" do
|
||||
mine = VavPipeline.Ledger.key_id("test-token")
|
||||
other = VavPipeline.Ledger.key_id("someone-else")
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3aaaa0000000001", mine))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3aaaa0000000002", mine))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3bbbb0000000003", other))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3cccc0000000004", nil))
|
||||
|
||||
refs = Store.list_bedikot(mine) |> Enum.map(& &1.bedikah_ref)
|
||||
assert refs == ["bedikah-l3aaaa0000000002", "bedikah-l3aaaa0000000001"]
|
||||
|
||||
conn = get("/courtyard/ledger?limit=10", "test-token")
|
||||
assert conn.status == 200
|
||||
body = Jason.decode!(conn.resp_body)
|
||||
assert body["key_id"] == mine
|
||||
assert body["count"] == 2
|
||||
assert Enum.map(body["entries"], & &1["entry"]["bedikah_ref"]) == ["bedikah-l3aaaa0000000002", "bedikah-l3aaaa0000000001"]
|
||||
# never the text, never the key, never the owner column
|
||||
e = hd(body["entries"])["entry"]
|
||||
refute Map.has_key?(e, "key_id")
|
||||
assert e["output_sha256"] == String.duplicate("cd", 32)
|
||||
assert hd(body["entries"])["signed"] == false
|
||||
end
|
||||
|
||||
test "one record by ref: mine answers, another key's and a missing one answer 404 alike" do
|
||||
mine = VavPipeline.Ledger.key_id("test-token")
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3dddd0000000005", mine))
|
||||
{:ok, _} = Store.record_bedikah(attrs("bedikah-l3eeee0000000006", VavPipeline.Ledger.key_id("someone-else")))
|
||||
|
||||
assert get("/courtyard/bedikah/bedikah-l3dddd0000000005", "test-token").status == 200
|
||||
assert get("/courtyard/bedikah/bedikah-l3eeee0000000006", "test-token").status == 404
|
||||
assert get("/courtyard/bedikah/bedikah-l3ffff0000000007", "test-token").status == 404
|
||||
end
|
||||
|
||||
test "an examination is engraved under the caller's key and reads back signed and verifiable" do
|
||||
Application.put_env(:vav_pipeline, :ledger_signing_key, Base.encode64(:crypto.strong_rand_bytes(32)))
|
||||
mine = VavPipeline.Ledger.key_id("test-token")
|
||||
|
||||
try do
|
||||
examiner = fn _o, _p, _q, _s -> {:ok, empty_examination()} end
|
||||
assert {:ok, r} = VavPipeline.Bedikah.examine("Rolled out.", "claude", "status?", examiner: examiner, key_id: mine)
|
||||
assert r.verdict == :empty
|
||||
assert Store.get_bedikah(r.bedikah_ref).key_id == mine
|
||||
|
||||
back = get("/courtyard/bedikah/#{r.bedikah_ref}", "test-token")
|
||||
assert back.status == 200
|
||||
body = Jason.decode!(back.resp_body)
|
||||
assert body["signed"] == true
|
||||
pub = Jason.decode!(get("/peah/ledger_key", "x").resp_body)["public_key"]
|
||||
canonical = VavPipeline.Ledger.canonical(body["entry"])
|
||||
assert :crypto.hash(:sha256, canonical) |> Base.encode16(case: :lower) == body["canonical_sha256"]
|
||||
assert VavPipeline.Ledger.verify(canonical, body["signature"], pub)
|
||||
after
|
||||
Application.delete_env(:vav_pipeline, :ledger_signing_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
defmodule VavPipeline.LedgerTest do
|
||||
@moduledoc "The provenance ledger: canonical form, signature, key digest (2026-10-21)."
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias VavPipeline.Ledger
|
||||
|
||||
@seed :crypto.strong_rand_bytes(32)
|
||||
|
||||
defp record do
|
||||
%VavPipeline.L3.BedikahRecord{
|
||||
id: 7,
|
||||
bedikah_ref: "bedikah-0123456789abcdef",
|
||||
provenance: "claude",
|
||||
query: "q",
|
||||
output_sha256: String.duplicate("ab", 32),
|
||||
output_length: 12,
|
||||
vital_content: "",
|
||||
propositions: [],
|
||||
references: ["Devarim 6:4"],
|
||||
authority_claims: [],
|
||||
actions: [],
|
||||
sources: [%{"id" => "system", "sha256" => "00", "length" => 2}],
|
||||
claims: [%{"index" => 1, "sentence" => "x", "anchors" => []}],
|
||||
structure: %{"self_validation" => []},
|
||||
frames: [],
|
||||
rebukes: nil,
|
||||
mesorah: [],
|
||||
coupling_trace: [],
|
||||
violations: [],
|
||||
verdict: "tov",
|
||||
examined_at: ~U[2026-10-21 12:00:00Z],
|
||||
key_id: "deadbeef",
|
||||
inserted_at: ~U[2026-10-21 12:00:01Z]
|
||||
}
|
||||
end
|
||||
|
||||
test "key_id is a digest of the key, never the key, and nil for none" do
|
||||
assert Ledger.key_id(nil) == nil
|
||||
id = Ledger.key_id("courtyard-secret-key")
|
||||
assert String.length(id) == 32
|
||||
refute id =~ "courtyard"
|
||||
assert id == Ledger.key_id("courtyard-secret-key")
|
||||
refute id == Ledger.key_id("another")
|
||||
end
|
||||
|
||||
test "the entry carries the examination's fields and neither the database's nor the owner's" do
|
||||
e = Ledger.entry(record())
|
||||
assert Map.keys(e) |> Enum.sort() == Enum.sort(Ledger.ledger_fields())
|
||||
refute Map.has_key?(e, :id)
|
||||
refute Map.has_key?(e, :key_id)
|
||||
refute Map.has_key?(e, :inserted_at)
|
||||
assert e.examined_at == "2026-10-21T12:00:00Z"
|
||||
end
|
||||
|
||||
test "canonical form sorts keys at every level and carries no whitespace" do
|
||||
a = Ledger.canonical(%{"b" => 1, "a" => %{"z" => [%{"y" => 2, "x" => 1}], "m" => "s"}})
|
||||
assert a == ~s({"a":{"m":"s","z":[{"x":1,"y":2}]},"b":1})
|
||||
# the same map with keys given in another order canonicalizes the same
|
||||
assert a == Ledger.canonical(%{"a" => %{"m" => "s", "z" => [%{"x" => 1, "y" => 2}]}, "b" => 1})
|
||||
# atom and string keys meet in the same place
|
||||
assert Ledger.canonical(%{b: 1, a: 2}) == ~s({"a":2,"b":1})
|
||||
end
|
||||
|
||||
test "a signature verifies with the public key and fails on one changed byte" do
|
||||
bytes = Ledger.canonical(Ledger.entry(record()))
|
||||
sig = Ledger.sign(bytes, @seed)
|
||||
pub = Ledger.public_key(@seed)
|
||||
assert Ledger.verify(bytes, sig, pub)
|
||||
refute Ledger.verify(bytes <> " ", sig, pub)
|
||||
refute Ledger.verify(bytes, sig, Ledger.public_key(:crypto.strong_rand_bytes(32)))
|
||||
refute Ledger.verify(bytes, "not base64!", pub)
|
||||
end
|
||||
|
||||
test "without a seed nothing is signed, and the rendering says so" do
|
||||
assert Ledger.sign("x", nil) == nil
|
||||
assert Ledger.public_key(nil) == nil
|
||||
r = Ledger.render(record())
|
||||
assert r.entry.bedikah_ref == "bedikah-0123456789abcdef"
|
||||
assert String.length(r.canonical_sha256) == 64
|
||||
# the test env has no signing key configured
|
||||
assert r.signed == false and r.signature == nil
|
||||
end
|
||||
|
||||
test "a rendering with a seed configured is signed and self-consistent" do
|
||||
Application.put_env(:vav_pipeline, :ledger_signing_key, Base.encode64(@seed))
|
||||
|
||||
try do
|
||||
r = Ledger.render(record())
|
||||
assert r.signed
|
||||
bytes = Ledger.canonical(r.entry)
|
||||
assert :crypto.hash(:sha256, bytes) |> Base.encode16(case: :lower) == r.canonical_sha256
|
||||
assert Ledger.verify(bytes, r.signature, Ledger.public_key())
|
||||
after
|
||||
Application.delete_env(:vav_pipeline, :ledger_signing_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,132 @@
|
||||
defmodule VavPipeline.SugyaTest do
|
||||
@moduledoc "The case behind each threshold (specs/sugya.md §IV, step 1): the facts are whole, and the Doorpost refuses a rule without them."
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias VavPipeline.{Anchors, Doorpost, Sugya}
|
||||
|
||||
test "the sugya file is a Doorpost file, beside the anchors" do
|
||||
assert "talmudic_engine/src/halt/sugya.pl" in Doorpost.files()
|
||||
assert "talmudic_engine/src/halt/anchors.pl" in Doorpost.files()
|
||||
end
|
||||
|
||||
test "every anchored rule has a kind, a precedent and a dissent; every verse is in L1" do
|
||||
assert :ok = Sugya.verify()
|
||||
{:ok, anchors} = Anchors.declared()
|
||||
{:ok, s} = Sugya.declared()
|
||||
for a <- anchors do
|
||||
assert Enum.any?(s.kinds, &(&1.rule == a.rule)), "#{a.rule} has no kind"
|
||||
assert Enum.any?(s.precedents, &(&1.rule == a.rule)), "#{a.rule} has no precedent"
|
||||
assert Enum.any?(s.dissents, &(&1.rule == a.rule)), "#{a.rule} has no dissent"
|
||||
end
|
||||
end
|
||||
|
||||
test "the kinds follow sugya.md §II: marks seen once, the forms as deeds, the grounding rules as words" do
|
||||
{:ok, s} = Sugya.declared()
|
||||
kind = fn r -> Enum.find_value(s.kinds, &(&1.rule == r && &1.kind)) end
|
||||
assert kind.("lo_taaseh_lekha") == "deed"
|
||||
assert kind.("golden_calf") == "word"
|
||||
assert kind.("self_grounding") == "word"
|
||||
assert kind.("even_vaaven") == "measure"
|
||||
for r <- ~w(avodah_zarah shfichut_damim gilui_arayot parah_adumah carrier_centered chelbenah), do: assert(kind.(r) == "mark", r)
|
||||
end
|
||||
|
||||
test "the exception the reader asked to see cited: forged_fringe stands on one witness because it is a mark on the offering" do
|
||||
{:ok, s} = Sugya.declared()
|
||||
e = Enum.find(s.exceptions, &(&1.rule == "lo_taaseh_lekha" and &1.condition =~ "forged_fringe"))
|
||||
assert e, "no single-witness exception for forged_fringe"
|
||||
assert Sugya.cite(e.verse) == "Vayikra 22:20"
|
||||
# and the two-witness dissent is recorded for every form
|
||||
assert Enum.any?(s.dissents, &(&1.rule == "lo_taaseh_lekha" and &1.reading == "single_witness_noted"))
|
||||
end
|
||||
|
||||
test "for_rule/1 renders a rule's sugya for a verdict to carry" do
|
||||
r = Sugya.for_rule("golden_calf")
|
||||
assert r.kind == "word" and r.verse == "Devarim 18:22"
|
||||
assert r.precedent == "tablet(49)"
|
||||
assert Enum.any?(r.dissent, &(&1.reading == "hedged"))
|
||||
assert Enum.any?(r.exception, &(&1.condition == "no_ground_given"))
|
||||
assert Sugya.for_rule("no_such_rule") == nil
|
||||
end
|
||||
|
||||
describe "the code reads its exemptions from the facts (step 2)" do
|
||||
test "the single-witness forms are those the sugya excepts, and only those" do
|
||||
assert Sugya.single_witness_forms() == ["forged_fringe"]
|
||||
end
|
||||
|
||||
test "the hedged and no-ground exemptions of golden_calf are facts, not constants" do
|
||||
assert Sugya.exception?("golden_calf", "hedged")
|
||||
assert Sugya.exception?("golden_calf", "no_ground_given")
|
||||
refute Sugya.exception?("golden_calf", "anything_at_all")
|
||||
refute Sugya.exception?("chelbenah", "hedged")
|
||||
end
|
||||
|
||||
test "with the fact removed, the exemption is gone: behaviour is read from the reason" do
|
||||
temp = Path.join(System.tmp_dir!(), "sugya_step2_#{System.unique_integer([:positive])}")
|
||||
rel = "talmudic_engine/src/halt/sugya.pl"
|
||||
File.mkdir_p!(Path.dirname(Path.join(temp, rel)))
|
||||
File.cp!(Path.join(Doorpost.repo_root(), rel), Path.join(temp, rel))
|
||||
on_exit(fn -> File.rm_rf!(temp) end)
|
||||
assert Sugya.single_witness_forms(temp) == ["forged_fringe"]
|
||||
path = Path.join(temp, rel)
|
||||
File.write!(path, String.replace(File.read!(path), "exception(lo_taaseh_lekha, single_witness(forged_fringe), verse(vayikra, 22, 20)).", ""))
|
||||
assert Sugya.single_witness_forms(temp) == []
|
||||
File.write!(path, String.replace(File.read!(path), "exception(golden_calf, hedged, verse(devarim, 18, 22)).", ""))
|
||||
refute Sugya.exception?("golden_calf", "hedged", temp)
|
||||
end
|
||||
end
|
||||
|
||||
describe "drift (synthetic spec root)" do
|
||||
setup do
|
||||
temp = Path.join(System.tmp_dir!(), "sugya_test_#{System.unique_integer([:positive])}")
|
||||
for rel <- Doorpost.files() do
|
||||
src = Path.join(Doorpost.repo_root(), rel)
|
||||
dst = Path.join(temp, rel)
|
||||
File.mkdir_p!(Path.dirname(dst))
|
||||
if File.exists?(src), do: File.cp!(src, dst)
|
||||
end
|
||||
on_exit(fn -> File.rm_rf!(temp) end)
|
||||
%{temp: temp, path: Path.join(temp, "talmudic_engine/src/halt/sugya.pl")}
|
||||
end
|
||||
|
||||
test "a mirror verifies", %{temp: temp} do
|
||||
assert :ok = Sugya.verify(temp)
|
||||
end
|
||||
|
||||
test "a rule without a dissent fails: the minority must be recorded", %{temp: temp, path: path} do
|
||||
text = File.read!(path) |> String.replace(~r/^dissent\(even_vaaven,.*\n/m, "")
|
||||
File.write!(path, text)
|
||||
assert {:error, "SUGYA_MISSING: rule even_vaaven has no dissent" <> _} = Sugya.verify(temp)
|
||||
end
|
||||
|
||||
test "a rule without a kind fails", %{temp: temp, path: path} do
|
||||
File.write!(path, String.replace(File.read!(path), "kind(chelbenah, mark, verse(vayikra, 13, 3)).", ""))
|
||||
assert {:error, "SUGYA_MISSING: rule chelbenah has no kind" <> _} = Sugya.verify(temp)
|
||||
end
|
||||
|
||||
test "a kind that is not one of the four fails", %{temp: temp, path: path} do
|
||||
File.write!(path, String.replace(File.read!(path), "kind(chelbenah, mark,", "kind(chelbenah, rumour,"))
|
||||
assert {:error, "SUGYA_INVALID: rule chelbenah has kind rumour" <> _} = Sugya.verify(temp)
|
||||
end
|
||||
|
||||
test "a verse that is not in L1 fails", %{temp: temp, path: path} do
|
||||
File.write!(path, String.replace(File.read!(path), "verse(vayikra, 22, 20)", "verse(vayikra, 22, 99)"))
|
||||
assert {:error, "SUGYA_NOT_IN_L1: Vayikra 22:99" <> _} = Sugya.verify(temp)
|
||||
end
|
||||
|
||||
test "a sugya for a rule that has no anchor fails: nothing added", %{temp: temp, path: path} do
|
||||
File.write!(path, File.read!(path) <> "\nexception(new_rule, whatever, verse(bereishit, 1, 1)).\n")
|
||||
assert {:error, "SUGYA_INVALID: " <> _ = msg} = Sugya.verify(temp)
|
||||
assert msg =~ "new_rule"
|
||||
end
|
||||
|
||||
test "a fact in a comment is not a fact", %{temp: temp, path: path} do
|
||||
File.write!(path, File.read!(path) <> "\n%% kind(imaginary, mark, verse(bereishit, 1, 1)).\n")
|
||||
assert :ok = Sugya.verify(temp)
|
||||
end
|
||||
|
||||
test "a missing sugya file fails", %{temp: temp, path: path} do
|
||||
File.rm!(path)
|
||||
assert {:error, "SUGYA_UNREADABLE: " <> _} = Sugya.verify(temp)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,54 @@
|
||||
defmodule VavPipeline.SugyaVerdictTest do
|
||||
@moduledoc "The sugya on a verdict (sugya.md §IV, step 3): applied is read from the record, never guessed."
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias VavPipeline.Sugya
|
||||
|
||||
defp result(overrides) do
|
||||
Map.merge(%{verdict: :tov, violations: [], checks: %{}, examination: %{"claims" => []}}, overrides)
|
||||
end
|
||||
|
||||
test "a clean verdict carries no sugya" do
|
||||
assert Sugya.for_verdict(result(%{})) == %{}
|
||||
end
|
||||
|
||||
test "a noted form carries the two-witness dissent, applied, with the forms it held for" do
|
||||
r = result(%{checks: %{frames: %{fired: false, forms: %{}, noted: %{apology_without_mechanism: %{claims: [], witnesses: 1}}, violations: []}}})
|
||||
s = Sugya.for_verdict(r)
|
||||
assert Map.keys(s) == ["lo_taaseh_lekha"]
|
||||
e = s["lo_taaseh_lekha"]
|
||||
assert e.kind == "deed" and e.verse == "Devarim 19:15"
|
||||
assert e.fired == false and e.noted == ["apology_without_mechanism"]
|
||||
d = Enum.find(e.dissent, &(&1.reading == "single_witness_noted"))
|
||||
assert d.applied == true and d.forms == ["apology_without_mechanism"] and d.verse == "Devarim 19:15"
|
||||
# the exception for the forged fringe did not apply: nothing stood on one witness
|
||||
assert Enum.find(e.exception, &(&1.condition =~ "forged_fringe")).applied == false
|
||||
end
|
||||
|
||||
test "a forged fringe standing on one witness carries the mark exception, applied" do
|
||||
r = result(%{verdict: :lo_tov,
|
||||
violations: ["halt(lo_taaseh_lekha,action(frame,by(claude),form(forged_fringe)),forged_fringe,'…')"],
|
||||
checks: %{frames: %{fired: true, forms: %{forged_fringe: %{stands_on: :forgery, witnesses: 1, claims: [], violations: ["x"]}}, noted: %{}, violations: ["x"]}}})
|
||||
e = Sugya.for_verdict(r)["lo_taaseh_lekha"]
|
||||
assert e.fired == true
|
||||
assert Enum.find(e.exception, &(&1.condition == "single_witness(forged_fringe)")) |> Map.fetch!(:applied) == true
|
||||
assert Enum.find(e.dissent, &(&1.reading == "single_witness_noted")).applied == false
|
||||
end
|
||||
|
||||
test "a golden calf beside a hedged claim carries the hedged dissent applied; without hedging, not" do
|
||||
base = %{verdict: :lo_tov, violations: ["halt(golden_calf,interpretation(x),golden_calf,'…')"], checks: %{grounding: %{assessed: true, fired: true}}}
|
||||
with_hedge = Sugya.for_verdict(result(Map.put(base, :examination, %{"claims" => [%{"hedged" => true}, %{"hedged" => false}]})))
|
||||
assert with_hedge["golden_calf"].kind == "word" and with_hedge["golden_calf"].verse == "Devarim 18:22"
|
||||
assert Enum.find(with_hedge["golden_calf"].dissent, &(&1.reading == "hedged")).applied == true
|
||||
without = Sugya.for_verdict(result(Map.put(base, :examination, %{"claims" => [%{"hedged" => false}]})))
|
||||
assert Enum.find(without["golden_calf"].dissent, &(&1.reading == "hedged")).applied == false
|
||||
end
|
||||
|
||||
test "a rule the record cannot speak to carries its dissent considered, applied false" do
|
||||
r = result(%{verdict: :lo_tov, violations: ["halt(parah_adumah,action(self_validate,by(claude),target(self)),claude,'…')"]})
|
||||
e = Sugya.for_verdict(r)["parah_adumah"]
|
||||
assert e.kind == "mark" and e.verse == "Vayikra 13:3"
|
||||
assert e.precedent == "specimens(self_validation)"
|
||||
assert [%{reading: "validator_named_is_not_the_speaker", applied: false, verse: "Bamidbar 19:7"}] = Enum.map(e.dissent, &Map.take(&1, [:reading, :applied, :verse]))
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user