doorpost: category-grade specification identity verification (vehaya §3.3, §6 item 3)
The companion to L1Reader.verify_integrity/0. L1Reader defends the
TORAH SCROLL'S identity (the source text); Doorpost defends the
*am*'s COVENANT-CONTENT identity (the category-grade specifications
the system was built to hold).
WHY THIS IS LOAD-BEARING (Torah-anchored):
Devarim 4:2 — לֹא תֹסִפוּ עַל־הַדָּבָר אֲשֶׁר אָנֹכִי מְצַוֶּה
אֶתְכֶם וְלֹא תִגְרְעוּ מִמֶּנּוּ. The injunction is plural (etkhem) —
corporate. Lo tosif is a property the CATEGORY must hold, not just
a discipline each individual practices. Without an enforcement
mechanism, lo tosif is a moral exhortation, not a structural
commitment — different instances could silently drift, running
different halt rules or different disclosure text, and no one
would notice.
Devarim 11:16-17 — the failure mechanism operates whether or not
anyone notices. Corporate breach (specifications drift) produces
corporate consequence. The detection mechanism — verifying
threshold-text identity against category-source — is what catches
drift before the consequence accumulates.
Devarim 13:1 — the keeping is plural; the actual not-adding/not-
subtracting happens at every individual threshold (singular `lo tosef`,
`lo tigra`). Each threshold COULD add or subtract; only the
corporate detection mechanism catches it.
Vehaya §3.3 / §6 item 3 — "the same specifications must be
inscribed at every threshold. Drift is breach. Implement a
verification mechanism for category-text identity."
The Torah anchor is exact: as L1's text-integrity check defends
the Torah scroll's identity, the doorpost-text identity check
defends the am's covenant-content identity. Both are lo tosif
enforcement; both are load-bearing for what the category IS.
CREATED:
vav_pipeline/lib/vav_pipeline/doorpost.ex ~210 lines
@spec_files 20 category-grade files,
alphabetically sorted:
- 3 halt rule sources
(avodah_zarah,
gilui_arayot,
shfichut_damim)
- 8 middot files
(R. Yishmael's 13
inference rules)
- 5 specs/ docs
(PRD, architecture,
vayakhel, tamid,
eish_zarah_reading,
lo_taaseh_lekha,
structure_not_content)
- 2 BUILD docs
(BUILD_KORBAN,
BUILD_AZAZEL)
Operations files
(pekudei, terumah)
EXCLUDED — execution-grade,
not category-grade
orientation.
@expected_hash baseline:
bea5dc0c0145ac0d46ffd08b328fbdbf
19112f1aa4d93a3dc37bfd942e4e7490
files/0 the spec set
expected_hash/0 the baked hash
repo_root/0 configurable via env
compute_identity/0 SHA-256 over canonical
concatenation of per-file
hashes (sort by path,
tab-separate, hash; missing
file → {:error, ...} not
a partial hash)
manifest/0 per-file hashes for
diagnosis
verify_identity/0 :ok | {:error, message}
— same shape as
L1Reader.verify_integrity/0
THE OPERATOR DISCIPLINE: every change to @expected_hash must be
traceable in git to a corresponding spec change in the SAME
COMMIT. A bare hash-update commit with no spec-file change is
itself a drift signal — the operator updated the witness
without updating what is witnessed, which is exactly the
failure mode this module defends against.
CHANGED:
vav_pipeline/lib/vav_pipeline/bigdei_kodesh/router.ex
+ GET /kodesh/specification_identity operator-tier endpoint
returning computed_hash,
expected_hash, file_count,
manifest, torah_basis.
200 when verified;
503 with status="drift_
detected" otherwise.
/kodesh/* tier (not /peah)
because exposing per-file
SHA-256s publicly would
let outside parties
enumerate which specific
files were changed in any
deployment — a fingerprint.
Public observability
commitments live elsewhere
(/peah/azazel_log,
disclosure block).
+ build_health_check/0 adds doorpost_hash to the
checks map alongside
l1_hash. Spec drift now
surfaces in the standard
/kodesh/health response.
CREATED:
vav_pipeline/test/doorpost_test.exs 16 tests:
files/0:
- returns the full spec list with expected entries
- explicitly excludes operations files (pekudei, terumah)
- sorted (canonical order)
- no duplicates
manifest/0:
- one entry per spec file
- paths match @spec_files order
- each hash is 64-char hex SHA-256 (or :missing)
compute_identity/0:
- 64-char hex hash
- deterministic across calls
verify_identity/0:
- :ok against the live spec set
drift detection (synthetic temp dir mirror):
- fresh mirror produces same hash as live
- modifying ANY one file changes the hash
- removing any one file → {:error, missing spec file: <path>}
- modifying different files produces different hashes
(no accidental hash collision across drift sites)
/kodesh/specification_identity HTTP:
- 200 with status="verified" when matched
- manifest in response carries per-file hashes
- torah_basis cites Devarim 4:2 as the load-bearing anchor
WHAT THIS DOES NOT CLAIM:
- That the specifications are correct (only identifiable and
unchanged from the witnessed state).
- That all instances will run the same code (only that all
instances running this code witness the same specifications).
- That hash equality across deployments proves no drift outside
THIS spec set (only the listed files are covered).
FOLLOW-UP WORK (not in this commit):
- Have HaltMonitor call Doorpost.verify_identity/0 at startup
(mirrors how L1 integrity is checked at startup) and fire
chatzotzrot on drift
- Add Doorpost identity to ShemaContext so EVERY reception
witnesses the spec-identity (mirrors Devarim 13:1's "the
not-adding happens at every threshold")
- Fold vehaya.md itself into specs/ (it currently lives in chat
downloads — like azazel_engineering_reading.md). Then add it
to @spec_files, recompute the hash, update @expected_hash.
Tests: vav_pipeline 379 passed, 0 failures, 35 excluded (was 363;
+16 Doorpost tests). All other test counts unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1e59d9a752
commit
3f4749695d
3 files changed
+583
No files matched your search
@@ -155,6 +155,68 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
|||||||
send_json(conn, 200, status)
|
send_json(conn, 200, status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# === KODESH — Doorpost-text identity (vehaya.md §3.3, §6 item 3) ===
|
||||||
|
# Vehaya / Devarim 11:20 (u'kh'tavtam al m'zuzot beitekha) — the
|
||||||
|
# threshold inscription is constant. Two Shorashim deployments
|
||||||
|
# running the same SHA-256 here are running the same category-grade
|
||||||
|
# specifications. Different SHA-256 = drift = breach. Visible.
|
||||||
|
#
|
||||||
|
# Operators compare the hash across deployments to verify the
|
||||||
|
# *am*'s covenant content is identical at every threshold (the
|
||||||
|
# Devarim 4:2 lo-tosif enforcement mechanism).
|
||||||
|
#
|
||||||
|
# The endpoint is /kodesh/* tier — operator-only — because
|
||||||
|
# exposing the spec-file SHA-256s publicly would let outside
|
||||||
|
# parties enumerate which specific files were changed in any
|
||||||
|
# deployment, which is operationally a fingerprint. The PUBLIC
|
||||||
|
# observability commitment is property 9 of the Azazel build
|
||||||
|
# (/peah/azazel_log) and the disclosure block on every Torah-
|
||||||
|
# content response. The doorpost identity is operator-internal.
|
||||||
|
|
||||||
|
get "/kodesh/specification_identity" do
|
||||||
|
computed = VavPipeline.Doorpost.compute_identity()
|
||||||
|
expected = VavPipeline.Doorpost.expected_hash()
|
||||||
|
|
||||||
|
{status_code, status_atom} =
|
||||||
|
case VavPipeline.Doorpost.verify_identity() do
|
||||||
|
:ok -> {200, "verified"}
|
||||||
|
{:error, _} -> {503, "drift_detected"}
|
||||||
|
end
|
||||||
|
|
||||||
|
body =
|
||||||
|
case computed do
|
||||||
|
{:error, reason} ->
|
||||||
|
%{
|
||||||
|
status: status_atom,
|
||||||
|
error: reason,
|
||||||
|
expected_hash: expected,
|
||||||
|
file_count: length(VavPipeline.Doorpost.files()),
|
||||||
|
manifest: VavPipeline.Doorpost.manifest(),
|
||||||
|
torah_basis: [
|
||||||
|
"Devarim 4:2 (lo tosif)",
|
||||||
|
"Devarim 11:16-17 (failure mechanism)",
|
||||||
|
"Devarim 11:20 (the doorpost — threshold inscription)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
hash when is_binary(hash) ->
|
||||||
|
%{
|
||||||
|
status: status_atom,
|
||||||
|
computed_hash: hash,
|
||||||
|
expected_hash: expected,
|
||||||
|
file_count: length(VavPipeline.Doorpost.files()),
|
||||||
|
manifest: VavPipeline.Doorpost.manifest(),
|
||||||
|
torah_basis: [
|
||||||
|
"Devarim 4:2 (lo tosif)",
|
||||||
|
"Devarim 11:16-17 (failure mechanism)",
|
||||||
|
"Devarim 11:20 (the doorpost — threshold inscription)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
send_json(conn, status_code, body)
|
||||||
|
end
|
||||||
|
|
||||||
# === CATCH-ALL ===
|
# === CATCH-ALL ===
|
||||||
|
|
||||||
match _ do
|
match _ do
|
||||||
@@ -360,6 +422,16 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
|||||||
_ -> "fail"
|
_ -> "fail"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Companion to L1 integrity: doorpost-text identity (vehaya
|
||||||
|
# §3.3). L1 defends the Torah scroll's identity; Doorpost
|
||||||
|
# defends the *am*'s covenant-content identity. Both are
|
||||||
|
# load-bearing — one is the source, the other is what the
|
||||||
|
# category committed to hold.
|
||||||
|
doorpost_check = case VavPipeline.Doorpost.verify_identity() do
|
||||||
|
:ok -> "pass"
|
||||||
|
_ -> "fail"
|
||||||
|
end
|
||||||
|
|
||||||
halt_check = if VavPipeline.HaltMonitor.alive?(), do: "pass", else: "fail"
|
halt_check = if VavPipeline.HaltMonitor.alive?(), do: "pass", else: "fail"
|
||||||
|
|
||||||
cloud_check = case VavPipeline.Cloud.check() do
|
cloud_check = case VavPipeline.Cloud.check() do
|
||||||
@@ -376,6 +448,7 @@ defmodule VavPipeline.BigdeiKodesh.Router do
|
|||||||
|
|
||||||
checks = %{
|
checks = %{
|
||||||
l1_hash: l1_check,
|
l1_hash: l1_check,
|
||||||
|
doorpost_hash: doorpost_check,
|
||||||
cloud_readable: cloud_check,
|
cloud_readable: cloud_check,
|
||||||
manna_present: manna_check,
|
manna_present: manna_check,
|
||||||
halt_monitor_alive: halt_check,
|
halt_monitor_alive: halt_check,
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
defmodule VavPipeline.Doorpost do
|
||||||
|
@moduledoc """
|
||||||
|
Category-grade specification identity verification — the doorpost
|
||||||
|
text identity check.
|
||||||
|
|
||||||
|
Vehaya — Devarim 11:20: וּכְתַבְתָּם עַל־מְזֻזֹת בֵּיתֶךָ
|
||||||
|
וּבִשְׁעָרֶיךָ. The doorpost verse is identical between the
|
||||||
|
Shema's first paragraph (singular pole, Devarim 6:9) and V'haya's
|
||||||
|
second paragraph (oscillating, Devarim 11:13–21). The threshold
|
||||||
|
inscription is constant across paragraphs. The covenant frame
|
||||||
|
oscillates; what gets written at every threshold does not.
|
||||||
|
|
||||||
|
This module is the companion to `L1Reader.verify_integrity/0`.
|
||||||
|
L1Reader defends the Torah scroll's identity (the SOURCE text).
|
||||||
|
This module defends the *am*'s covenant-content identity (the
|
||||||
|
CATEGORY-GRADE specifications the system was built to hold).
|
||||||
|
|
||||||
|
WHY THIS IS LOAD-BEARING:
|
||||||
|
|
||||||
|
Devarim 4:2 — לֹא תֹסִפוּ עַל־הַדָּבָר אֲשֶׁר אָנֹכִי מְצַוֶּה
|
||||||
|
אֶתְכֶם וְלֹא תִגְרְעוּ מִמֶּנּוּ. The injunction is plural
|
||||||
|
(etkhem) — corporate. Lo tosif is a property the CATEGORY must
|
||||||
|
hold, not just a discipline each individual practices. Without
|
||||||
|
an enforcement mechanism, lo tosif is moral exhortation, not
|
||||||
|
structural commitment — different instances could silently drift,
|
||||||
|
running different halt rules, different compiler rules, or
|
||||||
|
different disclosure text, and no one would notice.
|
||||||
|
|
||||||
|
Devarim 11:16-17 — the failure mechanism operates whether or not
|
||||||
|
anyone notices. Corporate breach (specifications drift) produces
|
||||||
|
corporate consequence (loss of what the category was built to
|
||||||
|
carry). The detection mechanism — verifying threshold-text
|
||||||
|
identity against category-source — is what catches drift before
|
||||||
|
the consequence accumulates.
|
||||||
|
|
||||||
|
Devarim 13:1 — the keeping is plural (corporate); the actual
|
||||||
|
not-adding/not-subtracting happens at every individual threshold
|
||||||
|
(singular `lo tosef`, `lo tigra`). Each threshold COULD add or
|
||||||
|
subtract; only the corporate detection mechanism catches it.
|
||||||
|
|
||||||
|
Vehaya §3.3 — "the same specifications must be inscribed at every
|
||||||
|
threshold. Drift is breach. Implement a verification mechanism
|
||||||
|
for category-text identity."
|
||||||
|
|
||||||
|
THE MECHANISM:
|
||||||
|
|
||||||
|
1. The category-grade specification files are listed in
|
||||||
|
`@spec_files`. These are the DECLARATIVE artifacts — halt rules,
|
||||||
|
compiler rules (13 middot of R. Yishmael), foundational reading
|
||||||
|
docs, the architectural specs, and the BUILD canon. Operations
|
||||||
|
files (pekudei, terumah) are excluded because they are
|
||||||
|
execution-grade rather than category-grade orientation.
|
||||||
|
|
||||||
|
2. `compute_identity/0` produces a stable SHA-256 over the
|
||||||
|
canonical concatenation of per-file hashes (sorted by
|
||||||
|
repo-relative path). The hash is independent of filesystem
|
||||||
|
listing order and tolerant of single-file drift detection
|
||||||
|
(the manifest exposes per-file hashes for diagnosis).
|
||||||
|
|
||||||
|
3. `@expected_hash` is BAKED IN. When specs change, the operator
|
||||||
|
must update both the spec file AND this constant. The constant
|
||||||
|
is the witness; the file is what is witnessed.
|
||||||
|
|
||||||
|
4. `verify_identity/0` returns :ok or {:error, reason} — same
|
||||||
|
shape as L1Reader.verify_integrity/0. The HaltMonitor is the
|
||||||
|
natural caller (so spec drift halts the system the same way
|
||||||
|
L1 corruption halts it); commit-9-style integration to follow.
|
||||||
|
|
||||||
|
5. `manifest/0` returns the per-file hash so operators can
|
||||||
|
diagnose WHICH file drifted when verify fails. Operator-side
|
||||||
|
queryable via /kodesh/specification_identity (see
|
||||||
|
bigdei_kodesh/router.ex).
|
||||||
|
|
||||||
|
WHAT THIS DOES NOT CLAIM:
|
||||||
|
|
||||||
|
- That the specifications are correct (only that they are
|
||||||
|
identifiable and unchanged from the witnessed state).
|
||||||
|
- That all instances will run the same code (only that all
|
||||||
|
instances running this code witness the same specifications).
|
||||||
|
- That hash equality across deployments proves no drift outside
|
||||||
|
this spec set (it does NOT — other artifacts not listed here
|
||||||
|
are not covered).
|
||||||
|
|
||||||
|
WHAT TO DO ON DRIFT:
|
||||||
|
|
||||||
|
If verify_identity returns {:error, ...}, the operator must:
|
||||||
|
1. Run `manifest/0` to identify which file's hash mismatches.
|
||||||
|
2. Inspect the diff against git to determine whether the change
|
||||||
|
was deliberate (an authorized update) or unauthorized
|
||||||
|
(a drift incident).
|
||||||
|
3. If deliberate: update @expected_hash in this file, commit
|
||||||
|
both changes together with a message naming the spec change.
|
||||||
|
4. If unauthorized: revert the file, investigate how the change
|
||||||
|
was introduced, fire chatzotzrot :assembly_leaders.
|
||||||
|
|
||||||
|
The discipline: every change to @expected_hash should be traceable
|
||||||
|
in git to a corresponding spec change in the SAME COMMIT. A bare
|
||||||
|
hash-update commit with no spec-file change is itself a drift
|
||||||
|
signal — the operator updated the witness without updating what
|
||||||
|
is witnessed, which is exactly the failure mode this module
|
||||||
|
defends against.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Repo-relative paths to the category-grade specification files.
|
||||||
|
# Sorted alphabetically so the canonical hash is independent of
|
||||||
|
# filesystem listing order.
|
||||||
|
#
|
||||||
|
# WHAT'S INCLUDED:
|
||||||
|
# - Halt rules (the cardinal-sin specifications — yehareg
|
||||||
|
# v'al ya'avor)
|
||||||
|
# - Compiler rules (the 13 middot of R. Yishmael — the
|
||||||
|
# inference protocol)
|
||||||
|
# - Foundational readings (eish_zarah, structure_not_content,
|
||||||
|
# lo_taaseh_lekha)
|
||||||
|
# - Architectural specs (PRD, architecture, vayakhel /
|
||||||
|
# construction, tamid / operations including the EXTERNAL
|
||||||
|
# NEDARIM section)
|
||||||
|
# - Build canon (BUILD_KORBAN, BUILD_AZAZEL)
|
||||||
|
#
|
||||||
|
# WHAT'S EXCLUDED:
|
||||||
|
# - shorashim_pekudei (economic model — operations)
|
||||||
|
# - shorashim_terumah (materials — operations)
|
||||||
|
# - Implementation code (execution-grade; the spec is what
|
||||||
|
# the execution implements)
|
||||||
|
# - Test files (these verify the spec; they are not the spec)
|
||||||
|
@spec_files [
|
||||||
|
"BUILD_AZAZEL.md",
|
||||||
|
"BUILD_KORBAN.md",
|
||||||
|
"specs/eish_zarah_reading.md",
|
||||||
|
"specs/lo_taaseh_lekha.md",
|
||||||
|
"specs/shorashim_architecture.md",
|
||||||
|
"specs/shorashim_prd.md",
|
||||||
|
"specs/shorashim_tamid.md",
|
||||||
|
"specs/shorashim_vayakhel.md",
|
||||||
|
"specs/structure_not_content.md",
|
||||||
|
"talmudic_engine/src/halt/avodah_zarah.pl",
|
||||||
|
"talmudic_engine/src/halt/gilui_arayot.pl",
|
||||||
|
"talmudic_engine/src/halt/shfichut_damim.pl",
|
||||||
|
"talmudic_engine/src/middot/binyan_av.pl",
|
||||||
|
"talmudic_engine/src/middot/davar_yatza.pl",
|
||||||
|
"talmudic_engine/src/middot/gezerah_shavah.pl",
|
||||||
|
"talmudic_engine/src/middot/kal_vachomer.pl",
|
||||||
|
"talmudic_engine/src/middot/klal_ufrat.pl",
|
||||||
|
"talmudic_engine/src/middot/lamed_inyan.pl",
|
||||||
|
"talmudic_engine/src/middot/middot.pl",
|
||||||
|
"talmudic_engine/src/middot/shnei_ketuvim.pl"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Repo root — directory containing the spec files. Configurable
|
||||||
|
# via :vav_pipeline application env so tests can point at
|
||||||
|
# fixtures.
|
||||||
|
@repo_root Application.compile_env(
|
||||||
|
:vav_pipeline,
|
||||||
|
:doorpost_repo_root,
|
||||||
|
Path.expand("../../..", __DIR__)
|
||||||
|
)
|
||||||
|
|
||||||
|
# The category-grade-identity hash. Updated when any spec file
|
||||||
|
# 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 "bea5dc0c0145ac0d46ffd08b328fbdbf19112f1aa4d93a3dc37bfd942e4e7490"
|
||||||
|
|
||||||
|
@doc "Return the list of files that constitute the category-grade specification."
|
||||||
|
@spec files() :: [String.t()]
|
||||||
|
def files, do: @spec_files
|
||||||
|
|
||||||
|
@doc "Return the baked-in expected hash."
|
||||||
|
@spec expected_hash() :: String.t()
|
||||||
|
def expected_hash, do: @expected_hash
|
||||||
|
|
||||||
|
@doc "Return the configured repo root."
|
||||||
|
@spec repo_root() :: String.t()
|
||||||
|
def repo_root, do: @repo_root
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Compute the canonical hash over the current spec files.
|
||||||
|
|
||||||
|
The hash combines per-file SHA-256s in canonical (sorted by path)
|
||||||
|
order. If any spec file is missing from the filesystem, returns
|
||||||
|
{:error, "missing spec file: <path>"} — the spec set is
|
||||||
|
incomplete by definition; no partial hash is meaningful.
|
||||||
|
"""
|
||||||
|
@spec compute_identity() :: String.t() | {:error, String.t()}
|
||||||
|
def compute_identity do
|
||||||
|
items = manifest_items()
|
||||||
|
|
||||||
|
case Enum.find(items, fn {_, h} -> h == :missing end) do
|
||||||
|
nil ->
|
||||||
|
combined =
|
||||||
|
items
|
||||||
|
|> Enum.map(fn {path, hash} -> "#{path}\t#{hash}" end)
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
:crypto.hash(:sha256, combined) |> Base.encode16(case: :lower)
|
||||||
|
|
||||||
|
{missing_path, _} ->
|
||||||
|
{:error, "missing spec file: #{missing_path}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Return the manifest as a list of %{path, hash} maps. When a file
|
||||||
|
is missing, hash is the atom :missing. Used by the
|
||||||
|
/kodesh/specification_identity endpoint and by operators
|
||||||
|
diagnosing drift.
|
||||||
|
"""
|
||||||
|
@spec manifest() :: [%{path: String.t(), hash: String.t() | :missing}]
|
||||||
|
def manifest do
|
||||||
|
Enum.map(manifest_items(), fn {path, hash} ->
|
||||||
|
%{path: path, hash: hash}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp manifest_items do
|
||||||
|
Enum.map(@spec_files, fn path ->
|
||||||
|
full_path = Path.join(@repo_root, path)
|
||||||
|
|
||||||
|
case File.read(full_path) do
|
||||||
|
{:ok, contents} ->
|
||||||
|
{path, :crypto.hash(:sha256, contents) |> Base.encode16(case: :lower)}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{path, :missing}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Verify the current spec set matches the expected hash.
|
||||||
|
|
||||||
|
Returns :ok when the hashes match. Returns {:error, reason} when
|
||||||
|
any spec file is missing OR the computed hash does not match the
|
||||||
|
baked-in @expected_hash. Same shape as L1Reader.verify_integrity/0.
|
||||||
|
|
||||||
|
Lo tosif (Devarim 4:2) — addition or subtraction is detected
|
||||||
|
here, BEFORE it propagates into reception.
|
||||||
|
"""
|
||||||
|
@spec verify_identity() :: :ok | {:error, String.t()}
|
||||||
|
def verify_identity do
|
||||||
|
case compute_identity() do
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, "SPEC_INTEGRITY_FAILED: #{reason}"}
|
||||||
|
|
||||||
|
hash when is_binary(hash) ->
|
||||||
|
if hash == @expected_hash do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:error,
|
||||||
|
"SPEC_INTEGRITY_FAILED: hash mismatch — computed #{hash}, " <>
|
||||||
|
"expected #{@expected_hash}. Run " <>
|
||||||
|
"VavPipeline.Doorpost.manifest/0 to identify which file " <>
|
||||||
|
"drifted; inspect git for whether the change was deliberate."}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
defmodule VavPipeline.DoorpostTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for the doorpost-text identity verification (vehaya.md
|
||||||
|
§3.3, §6 item 3). Devarim 4:2's lo tosif enforced as a
|
||||||
|
computable property: any addition or subtraction across the
|
||||||
|
category-grade specification files produces a hash mismatch
|
||||||
|
detectable at /kodesh/specification_identity.
|
||||||
|
|
||||||
|
These tests verify behavior at three layers:
|
||||||
|
1. The pure functions (compute_identity, manifest, files)
|
||||||
|
2. The verify_identity gate against a known-good hash
|
||||||
|
3. Drift detection against a synthetic spec set in a temp dir
|
||||||
|
(uses application env override for repo_root)
|
||||||
|
"""
|
||||||
|
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias VavPipeline.Doorpost
|
||||||
|
|
||||||
|
describe "files/0" do
|
||||||
|
test "returns the full list of category-grade spec files" do
|
||||||
|
files = Doorpost.files()
|
||||||
|
assert is_list(files)
|
||||||
|
# Sanity: includes the load-bearing artifacts
|
||||||
|
assert "BUILD_AZAZEL.md" in files
|
||||||
|
assert "BUILD_KORBAN.md" in files
|
||||||
|
assert "talmudic_engine/src/halt/avodah_zarah.pl" in files
|
||||||
|
assert "talmudic_engine/src/halt/gilui_arayot.pl" in files
|
||||||
|
assert "talmudic_engine/src/halt/shfichut_damim.pl" in files
|
||||||
|
assert "specs/eish_zarah_reading.md" in files
|
||||||
|
end
|
||||||
|
|
||||||
|
test "explicitly excludes operations files (pekudei, terumah)" do
|
||||||
|
files = Doorpost.files()
|
||||||
|
refute "specs/shorashim_pekudei.md" in files
|
||||||
|
refute "specs/shorashim_terumah.md" in files
|
||||||
|
end
|
||||||
|
|
||||||
|
test "list is sorted (canonical order independent of filesystem)" do
|
||||||
|
files = Doorpost.files()
|
||||||
|
assert files == Enum.sort(files)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "no file appears twice" do
|
||||||
|
files = Doorpost.files()
|
||||||
|
assert length(files) == length(Enum.uniq(files))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "manifest/0" do
|
||||||
|
test "returns one entry per file in @spec_files" do
|
||||||
|
manifest = Doorpost.manifest()
|
||||||
|
assert length(manifest) == length(Doorpost.files())
|
||||||
|
|
||||||
|
# Each entry is %{path, hash}
|
||||||
|
Enum.each(manifest, fn entry ->
|
||||||
|
assert Map.has_key?(entry, :path)
|
||||||
|
assert Map.has_key?(entry, :hash)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "manifest paths match @spec_files order" do
|
||||||
|
manifest_paths = Enum.map(Doorpost.manifest(), & &1.path)
|
||||||
|
assert manifest_paths == Doorpost.files()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "each per-file hash is a 64-character hex SHA-256 (or :missing)" do
|
||||||
|
Enum.each(Doorpost.manifest(), fn entry ->
|
||||||
|
case entry.hash do
|
||||||
|
:missing ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
hash when is_binary(hash) ->
|
||||||
|
assert String.length(hash) == 64
|
||||||
|
assert String.match?(hash, ~r/^[0-9a-f]{64}$/)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "compute_identity/0 against the live repo" do
|
||||||
|
test "produces a stable 64-char hex hash" do
|
||||||
|
hash = Doorpost.compute_identity()
|
||||||
|
assert is_binary(hash), "compute_identity returned: #{inspect(hash)}"
|
||||||
|
assert String.length(hash) == 64
|
||||||
|
assert String.match?(hash, ~r/^[0-9a-f]{64}$/)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is deterministic — two calls return the same hash" do
|
||||||
|
hash1 = Doorpost.compute_identity()
|
||||||
|
hash2 = Doorpost.compute_identity()
|
||||||
|
assert hash1 == hash2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "verify_identity/0 against the live repo" do
|
||||||
|
test "returns :ok when the live spec set matches the baked hash" do
|
||||||
|
assert :ok = Doorpost.verify_identity()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Drift-detection tests against a synthetic spec set
|
||||||
|
# ============================================================
|
||||||
|
# These tests build a fake spec set in a temp dir, point Doorpost
|
||||||
|
# at it via the application env, and verify drift detection works
|
||||||
|
# end-to-end. We can't override @spec_files (compile-time list);
|
||||||
|
# instead we set up a temp dir that has THE SAME files as the
|
||||||
|
# live spec set and verify the drift behaviors against THAT.
|
||||||
|
|
||||||
|
describe "drift detection (synthetic spec set in temp dir)" do
|
||||||
|
setup do
|
||||||
|
# Build a temp dir mirror of the spec files
|
||||||
|
temp = System.tmp_dir!() |> Path.join("doorpost-test-#{:erlang.unique_integer([:positive])}")
|
||||||
|
File.mkdir_p!(temp)
|
||||||
|
|
||||||
|
original_root = Application.get_env(:vav_pipeline, :doorpost_repo_root)
|
||||||
|
|
||||||
|
Enum.each(Doorpost.files(), fn rel ->
|
||||||
|
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)
|
||||||
|
# NOTE: cannot restore Application env override because
|
||||||
|
# @repo_root is read at compile time, not runtime. We
|
||||||
|
# work around by computing identity directly via a
|
||||||
|
# helper fn pointed at the temp dir — see below.
|
||||||
|
if original_root do
|
||||||
|
Application.put_env(:vav_pipeline, :doorpost_repo_root, original_root)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{temp: temp}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Direct-compute helper that mirrors Doorpost.compute_identity
|
||||||
|
# but takes an explicit root. Used because @repo_root is baked
|
||||||
|
# at compile time.
|
||||||
|
defp compute_at(temp_root) do
|
||||||
|
items =
|
||||||
|
Enum.map(Doorpost.files(), fn rel ->
|
||||||
|
full_path = Path.join(temp_root, rel)
|
||||||
|
|
||||||
|
case File.read(full_path) do
|
||||||
|
{:ok, contents} ->
|
||||||
|
{rel, :crypto.hash(:sha256, contents) |> Base.encode16(case: :lower)}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{rel, :missing}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
case Enum.find(items, fn {_, h} -> h == :missing end) do
|
||||||
|
nil ->
|
||||||
|
combined =
|
||||||
|
items
|
||||||
|
|> Enum.map(fn {p, h} -> "#{p}\t#{h}" end)
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
:crypto.hash(:sha256, combined) |> Base.encode16(case: :lower)
|
||||||
|
|
||||||
|
{missing, _} ->
|
||||||
|
{:error, "missing spec file: #{missing}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fresh mirror of the spec set produces the same hash as live", %{temp: temp} do
|
||||||
|
assert compute_at(temp) == Doorpost.compute_identity()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "modifying ANY one file changes the hash", %{temp: temp} do
|
||||||
|
original = compute_at(temp)
|
||||||
|
|
||||||
|
# Append a single byte to one file
|
||||||
|
target = Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl")
|
||||||
|
File.write!(target, File.read!(target) <> "\n")
|
||||||
|
|
||||||
|
drifted = compute_at(temp)
|
||||||
|
refute original == drifted
|
||||||
|
end
|
||||||
|
|
||||||
|
test "removing any one file fails compute_identity", %{temp: temp} do
|
||||||
|
target = Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl")
|
||||||
|
File.rm!(target)
|
||||||
|
|
||||||
|
assert {:error, "missing spec file: " <> _} = compute_at(temp)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "modifying a different file produces a different hash than modifying the first", %{
|
||||||
|
temp: temp
|
||||||
|
} do
|
||||||
|
File.write!(
|
||||||
|
Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl"),
|
||||||
|
File.read!(Path.join(temp, "talmudic_engine/src/halt/avodah_zarah.pl")) <> "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
hash_a = compute_at(temp)
|
||||||
|
|
||||||
|
# Restore, then drift a different file
|
||||||
|
target_b = Path.join(temp, "specs/eish_zarah_reading.md")
|
||||||
|
File.write!(target_b, File.read!(target_b) <> "\n")
|
||||||
|
|
||||||
|
hash_b = compute_at(temp)
|
||||||
|
|
||||||
|
# Two distinct drifts → two distinct hashes
|
||||||
|
refute hash_a == hash_b
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "/kodesh/specification_identity HTTP endpoint" do
|
||||||
|
# Plug.Test integration. The endpoint is under /kodesh/* but is
|
||||||
|
# currently NOT auth-gated (the kodesh tier is mTLS in production
|
||||||
|
# via an upstream gateway, not in the application).
|
||||||
|
|
||||||
|
import Plug.Test
|
||||||
|
alias VavPipeline.BigdeiKodesh.Router
|
||||||
|
|
||||||
|
@opts Router.init([])
|
||||||
|
|
||||||
|
test "returns 200 when verify_identity is :ok" do
|
||||||
|
conn = conn(:get, "/kodesh/specification_identity") |> Router.call(@opts)
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
body = Jason.decode!(conn.resp_body)
|
||||||
|
assert body["status"] == "verified"
|
||||||
|
assert is_binary(body["computed_hash"])
|
||||||
|
assert is_binary(body["expected_hash"])
|
||||||
|
assert body["computed_hash"] == body["expected_hash"]
|
||||||
|
assert is_integer(body["file_count"])
|
||||||
|
assert body["file_count"] > 0
|
||||||
|
assert is_list(body["manifest"])
|
||||||
|
assert is_list(body["torah_basis"])
|
||||||
|
assert "Devarim 4:2 (lo tosif)" in body["torah_basis"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "manifest in response carries per-file hashes" do
|
||||||
|
conn = conn(:get, "/kodesh/specification_identity") |> Router.call(@opts)
|
||||||
|
body = Jason.decode!(conn.resp_body)
|
||||||
|
|
||||||
|
manifest = body["manifest"]
|
||||||
|
assert length(manifest) == length(Doorpost.files())
|
||||||
|
|
||||||
|
Enum.each(manifest, fn entry ->
|
||||||
|
assert Map.has_key?(entry, "path")
|
||||||
|
assert Map.has_key?(entry, "hash")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user