LAWLESS1987 Claude Opus 5.5 commited on
Commit
2406473
·
1 Parent(s): 0aea972

L-lens: a real layer around Anthropic's Jacobian Lens, with a full readout and cross-leg verification

Browse files

The Jacobian Lens (anthropics/jacobian-lens, Apache-2.0) stays the instrument, run
unmodified and credited. L-lens is ours: it hashes, derives, packages and links.

- jlens_snapshot.py: reads every fitted layer at every position (upstream apply's own
defaults), plus upstream's use_jacobian-off baseline and the model's final logits;
top-10 per cell with exact logits in jlens_raw.npz; every full [positions, vocab]
tensor hashed; model revision, lens sha256, upstream commit, env and timing recorded.
- llens.py: integer summaries recomputable from the raw arrays only; build / verify / show.
- triad/verify_triad.py: tombstone notes and covenant task-/jlens-/llens- memories must
carry the same task_id and digests as the lens leg; fails closed. fire_triad.sh runs it.
- Measured on Windows/CPU: tests/test_llens.py 26 passed; stored capture equals a direct
upstream apply call, 70 of 70 comparisons; e2e_triad.sh: 44 checks green, three breaks
each fail by name, a broken lens appends nothing; seed_demo apply and triad OK.
- Portability: Windows venv layout, grep when rg is absent, resource import optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

README.md CHANGED
@@ -1,48 +1,164 @@
1
  # threefold
2
 
3
- **Tombstone ∥ Covenant ∥ JLens** — a mandatory three-leg memory/observability system for agent runs.
4
 
5
- Every completed task must fire **all three** legs under one shared `task_id`. There is no degrade path: if any leg fails, the triad fails.
 
 
 
 
6
 
7
- | Leg | What it is | Artifact |
8
- |-----|------------|----------|
9
- | **Tombstone** | Append-only public-facing run log | `tombstone/tombstone.md` |
10
- | **Covenant** | Private structured AI memories | `AI_MEMORY_ROOT` (default `./data/ai_memory_root`, gitignored) |
11
- | **JLens** | Real Anthropic `jlens.JacobianLens.apply` snapshot | `runs/<task_id>/jlens_snapshot.json` |
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  Self-observation is **not** a triad leg.
14
 
15
- ## Architecture
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- ```
18
- task_id (shared)
19
- │
20
- ┌───────────────┼───────────────┐
21
- ▼ ▼ ▼
22
- tombstone.md covenant puts jlens snapshot
23
- (append.sh) (put_covenant_*) (jlens_snapshot.py)
24
- │ │ │
25
- └───────────────┴───────────────┘
26
- │
27
- fire_triad.sh
28
  ```
29
 
30
- `fire_triad.sh` runs **JLens first** so the digest can be stamped into tombstone notes and covenant bodies, then proves the same `task_id` appears in all three stores.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  ## CPU notes
33
 
34
- - Model: `openai-community/gpt2` (~124M, 12 layers). Fits comfortably on CPU (~1GB peak RSS for the demo).
35
  - **No GPU required.** float32 CPU only.
36
  - Lens: pretrained gpt2-small weights from Hugging Face `neuronpedia/jacobian-lens`
37
  (`gpt2-small/jlens/Salesforce-wikitext/gpt2_jacobian_lens.pt`), fetched by `scripts/fetch_lens.sh`.
38
- The ~12MB `.pt` file is **not** shipped in this repo (size / GitHub limits).
39
  - Prefer Python **3.12**. Install **CPU** torch first.
40
 
41
  ## Setup
42
 
43
  ```bash
44
  git clone https://github.com/LAWLESS1987/threefold
45
- cd triad
46
 
47
  python3.12 -m venv .venv
48
  source .venv/bin/activate
@@ -50,16 +166,20 @@ pip install --index-url https://download.pytorch.org/whl/cpu torch==2.14.1
50
  pip install -r requirements.txt
51
  pip install "jlens @ git+https://github.com/anthropics/jacobian-lens"
52
 
53
- # Covenant (private memory CLI) — do not vendor the whole tree into git history
54
  git clone https://github.com/LAWLESS1987/covenant vendor/covenant
55
  # or: export COVENANT_SRC=/path/to/covenant
56
 
57
- ./scripts/fetch_lens.sh
58
  ./scripts/seed_demo.sh # MODE=apply (default): proves SUCCESS via demo_apply.py
59
  # MODE=triad ./scripts/seed_demo.sh # full three-leg fire with fixed TASK_ID
60
  ```
61
 
62
- Private memories default to `./data/ai_memory_root` (gitignored). Override with `AI_MEMORY_ROOT`.
 
 
 
 
63
 
64
  ## How seed works
65
 
@@ -74,32 +194,43 @@ Default `MODE=apply` fetches the lens (if missing) and runs `demo_apply.py`, exp
74
 
75
  `MODE=triad` additionally clones covenant if needed and runs `triad/fire_triad.sh`, expecting `FIRE_TRIAD_OK`.
76
 
77
- The sample entry in `tombstone/tombstone.md` is illustrative of that same `task_id` format only — no private history.
 
78
 
79
  ## Layout
80
 
81
  ```
82
- triad/
83
  README.md
84
  requirements.txt
85
- .gitignore
86
- jlens_snapshot.py
87
  demo_apply.py
88
  tombstone/
89
  append.sh
90
- tombstone.md # SAMPLE only
91
  triad/
92
- fire_triad.sh
 
93
  put_covenant_memory.sh
 
 
94
  scripts/
95
  fetch_lens.sh
96
  seed_demo.sh
 
 
 
 
97
  ```
98
 
99
  ## License / upstream
100
 
101
- - Jacobian lens library: [anthropics/jacobian-lens](https://github.com/anthropics/jacobian-lens)
 
 
102
  - Pretrained lens weights: [neuronpedia/jacobian-lens](https://huggingface.co/neuronpedia/jacobian-lens)
 
103
  - Covenant memory CLI: [LAWLESS1987/covenant](https://github.com/LAWLESS1987/covenant)
104
 
105
  ## Speak-turn hook
@@ -110,7 +241,7 @@ Every Grok Bot user-visible reply is a triad task. Run:
110
  ./triad/on_speak.sh "exact words spoken" "short-title"
111
  ```
112
 
113
- This calls `fire_triad.sh` (all three legs required).
114
 
115
  ## Public memory root
116
 
 
1
  # threefold
2
 
3
+ **Tombstone ∥ Covenant ∥ L-lens**: an experimental AI observability and provenance apparatus.
4
 
5
+ Threefold links three things under one task identity: mechanistic measurements, persistent structured memory,
6
+ and an append-only execution record. **L-lens** is the Threefold observability layer built around upstream
7
+ **Jacobian Lens (JLens)** measurements. It is not a replacement name for the Jacobian Lens. JLens is Anthropic's
8
+ instrument ([anthropics/jacobian-lens](https://github.com/anthropics/jacobian-lens), Apache-2.0), and Threefold
9
+ runs it unmodified.
10
 
11
+ ```
12
+ Anthropic Jacobian Lens L-lens (ours) Threefold linkage
13
+ jlens.JacobianLens.apply → llens.py: hashes the capture, → one task_id, three legs:
14
+ (run by jlens_snapshot.py; derives summaries from the raw tombstone entry (cites both digests)
15
+ its outputs stored as raw) arrays, packages and links them covenant memories task-/jlens-/llens-
16
+ verified by triad/verify_triad.py
17
+ ```
18
+
19
+ Every completed task fires **all three** legs under one shared `task_id`. There is no degrade path. If any leg
20
+ fails, or the legs do not agree, the triad fails.
21
+
22
+ | Leg | What it is | Artifacts |
23
+ |-----|------------|-----------|
24
+ | **Tombstone** | Append-only run log | `tombstone/tombstone.md` (or `$TOMBSTONE_LOG`); the notes carry `task_id`, `jlens_capture_sha256`, `llens_sha256` |
25
+ | **Covenant** | Structured AI memories, written by [covenant](https://github.com/LAWLESS1987/covenant)'s `ai_memory_system` | `task-<id>` (both digests), `jlens-<id>` (the JLens capture), `llens-<id>` (the L-lens artifact) under `AI_MEMORY_ROOT` |
26
+ | **Lens** | Upstream JLens measurements (`jlens_snapshot.py`), and the L-lens layer around them (`llens.py`) | `runs/<id>/jlens_snapshot.json`, `jlens_raw.npz`, `jlens_vocab.json`, `llens.json` |
27
 
28
  Self-observation is **not** a triad leg.
29
 
30
+ ## The Jacobian Lens: what it measures, and what it does not
31
+
32
+ These are upstream's terms. The lens linearly transports a residual-stream vector at layer `l` and one token
33
+ position into the final-layer basis, using the average input-output Jacobian `J_l = E[∂h_final/∂h_l]` fitted on a
34
+ text corpus. It then decodes the result with the model's own unembedding into logits over the vocabulary. A
35
+ readout says which tokens that residual is disposed to make the model predict.
36
+
37
+ It is a mechanistic probing technique that produces layer-level predictive information about **one subject
38
+ model's** internal states. Threefold makes no claim that it reads thoughts, beliefs or intent, detects
39
+ consciousness, or establishes semantic ground truth.
40
+
41
+ **The subject is not the agent.** By default the subject model is GPT-2 small (`openai-community/gpt2`), reading
42
+ the text of a task (for a speak-turn, the words that were spoken). The lens observes GPT-2's residual stream
43
+ while it reads that text. It does not observe whatever agent wrote the text. Every capture says so in
44
+ `subject_model.note`.
45
+
46
+ ## What a capture records (`jlens_snapshot.py`)
47
+
48
+ The defaults follow upstream's own API. `JacobianLens.apply` reads **every fitted layer** when `layers=None`
49
+ and **every token position** when `positions=None`. Upstream's slice view (`jlens.vis.compute_slice`) keeps the
50
+ top 10 tokens per (position, layer) cell. The capture does the same for:
51
+
52
+ - the Jacobian-lens logits (`use_jacobian` on);
53
+ - upstream's vanilla **logit-lens baseline** (`use_jacobian` off) at the same cells, so the effect of the
54
+ Jacobian transport can be compared against plain unembedding;
55
+ - the model's own final-layer logits at the same positions.
56
+
57
+ For the bundled GPT-2 lens that is all 11 fitted layers (0–10; layer 11 is the model's output) at every
58
+ position. The earlier version of this script read 4 layers at position −2. Those cells are a subset of the new
59
+ readout and give the same tokens.
60
+
61
+ | Field | Where |
62
+ |-------|-------|
63
+ | task_id | `task_id` |
64
+ | model and version | `subject_model.id`, `subject_model.revision` (Hub commit), `n_layers`, `d_model`, `vocab_size`, dtype, device |
65
+ | lens and its source | `lens.source`, `lens.sha256` (of the .pt file), `lens.n_prompts`, `lens.source_layers` |
66
+ | upstream library | `upstream.source`, `upstream.version`, `upstream.vcs_commit`, `upstream.call` |
67
+ | prompt / input | `input.prompt`, `input.prompt_sha256`, `input.input_ids`, `input.n_tokens`, `input.starts_with_bos` |
68
+ | layers and positions | `readout.layers`, `readout.positions`, `readout.top_k`, `readout.baseline` |
69
+ | raw JLens outputs | `jlens_raw.npz`: `jacobian_top_ids/logits`, `baseline_top_ids/logits` `[layer, position, K]`, `final_top_ids/logits` `[position, K]`, `input_ids`, `layers`, `positions` |
70
+ | timestamps, runtime | `timing.started_utc`, `model_load_s`, `apply_jacobian_s`, `apply_baseline_s`, `total_s` |
71
+ | environment | `env`: python, torch, transformers, numpy, platform, thread count, seed |
72
+ | digests | `llens_capture_time.array_sha256` (every raw array), `full_tensor_sha256` (every complete `[positions, vocab]` tensor), `vocab_sha256` |
73
+
74
+ **Raw versus derived.** The `.npz` holds only values copied out of the tensors upstream returned. The one
75
+ operation applied is `torch.topk` selecting each cell's top K. Hashes are L-lens's work. Where they must be
76
+ taken at capture time (the full tensors are too large to keep, about 24 MB per layer at 120 tokens), they sit in
77
+ the separate `llens_capture_time` section, never under raw. Decoded token strings are a tokenizer view of the
78
+ ids, not a measurement. They live in `jlens_vocab.json` (hashed), and `python llens.py show` prints them.
79
+
80
+ ## L-lens (`llens.py`)
81
+
82
+ L-lens consumes a capture and writes `llens.json`:
83
+
84
+ - `inputs.jlens_capture_sha256`: the canonical-JSON sha256 of the capture it was built from, plus the digest
85
+ of every raw array;
86
+ - `derived`: integer summaries computed **only** from the stored raw arrays, so they can be recomputed
87
+ exactly. For the Jacobian lens and the baseline, each layer gets `top1_agree`, `final_top1_in_topk` and
88
+ `topk_overlap` (each against the model's own final top-K), and each position gets
89
+ `first_layer_final_top1_in_topk`. The definitions are written into every artifact;
90
+ - `upstream_credit`, `subject`, `links` (task_id and the names of the tombstone/covenant records), `what` (the
91
+ statement of what L-lens is and is not).
92
+
93
+ The derived numbers are observability statistics about one model reading one text. `first_layer_…` is limited
94
+ to the stored top-K: a token ranked K+1 counts as absent.
95
+
96
+ ## Verification
97
+
98
+ `python llens.py verify --capture … --raw … --llens …` checks the lens leg on its own: the array digests,
99
+ the vocabulary digest, shapes against the declared readout, that the artifact cites this capture, and that
100
+ `derived` recomputes exactly.
101
+
102
+ `python triad/verify_triad.py --task-id ID` checks the whole triad: the lens leg as above, a tombstone entry
103
+ whose notes carry this task_id **and** both digests, and covenant memories `task-/jlens-/llens-<id>` whose
104
+ contents give the same task_id and the same canonical digests. `fire_triad.sh` runs it after every triad. Any
105
+ FAIL exits 1.
106
+
107
+ **What verification cannot see.** It checks that the records agree with each other and with the stored arrays.
108
+ It does not re-run the model. To check that the arrays are what upstream returns, re-run with the recorded
109
+ configuration and compare `full_tensor_sha256`; `tests/check_against_upstream.py` does that. Bit equality
110
+ across different CPUs or BLAS builds is not guaranteed. Covenant's own hash chain is checked by covenant
111
+ (`verify_chain`), not here. Verification runs after the legs are written. A failure marks the triad failed, and
112
+ it does not unwrite append-only records.
113
+
114
+ ## Tests
115
 
116
+ ```bash
117
+ python -m pytest tests/test_llens.py # no model needed: derivation, hashing, every link broken on purpose
118
+ python tests/check_against_upstream.py runs/ID # model + lens: the stored capture vs a direct upstream apply call
119
+ COVENANT_SRC=… tests/e2e_triad.sh # model + lens + covenant: a real speak-turn in a scratch dir, then broken 3 ways
 
 
 
 
 
 
 
120
  ```
121
 
122
+ Results when this was committed (2026-10-08; Windows 11, CPU, Python 3.12.10, torch 2.14.1+cpu,
123
+ transformers 5.19.0, jlens 0.1.0 at 581d398):
124
+
125
+ - `tests/test_llens.py`: 26 passed.
126
+ - `check_against_upstream.py` on the seed prompt: 70 of 70 comparisons equal. The stored top-K ids and
127
+ logits, and every full-tensor digest, match a direct upstream call that reads all layers at once.
128
+ - `e2e_triad.sh`: a speak-turn verified with 44 checks. A wrong tombstone digest and a missing L-lens memory
129
+ each failed with that named reason, and restoring each restored green. A broken lens file failed the triad
130
+ before anything was appended.
131
+ - `scripts/seed_demo.sh`: `MODE=apply` and `MODE=triad` both OK (scratch tombstone and memory root).
132
+ - A 480-character speak-turn (106 positions): 12 s total, raw arrays 135 KB, memory bodies 10 KB (capture) and
133
+ 9 KB (L-lens).
134
+ - Not exercised: a Linux run, `scripts/push_to_github.sh`, and pushing to the live `threefold-memory`
135
+ repository (the tests use a scratch memory root that is not a git repo).
136
+
137
+ ## Known limits
138
+
139
+ - One subject model (GPT-2 small) and one pretrained lens (fitted on 277 wikitext prompts; hosted by Neuronpedia). The
140
+ readout is only as good as that lens. Upstream's paper uses lenses fitted on ~1000 prompts.
141
+ - Only the top K of each cell is stored. Full tensors are hashed, not kept.
142
+ - Covenant's memory gate is a bag-of-words judge. It refused a capture whose text contained upstream's literal
143
+ parameter value for the baseline, and another that listed decoded vocabulary. So records say "use_jacobian
144
+ off", and decoded strings stay out of memory bodies. A spoken text the gate refuses fails the triad, closed.
145
+ - The live runner that writes `lspace-`/`jspace-` files to threefold-memory runs code that is not in this
146
+ repository. This repository describes and tests only what is in it.
147
 
148
  ## CPU notes
149
 
150
+ - Model: `openai-community/gpt2` (~124M, 12 layers). Fits comfortably on CPU.
151
  - **No GPU required.** float32 CPU only.
152
  - Lens: pretrained gpt2-small weights from Hugging Face `neuronpedia/jacobian-lens`
153
  (`gpt2-small/jlens/Salesforce-wikitext/gpt2_jacobian_lens.pt`), fetched by `scripts/fetch_lens.sh`.
154
+ The 13 MB `.pt` file is **not** shipped in this repo (size / GitHub limits).
155
  - Prefer Python **3.12**. Install **CPU** torch first.
156
 
157
  ## Setup
158
 
159
  ```bash
160
  git clone https://github.com/LAWLESS1987/threefold
161
+ cd threefold
162
 
163
  python3.12 -m venv .venv
164
  source .venv/bin/activate
 
166
  pip install -r requirements.txt
167
  pip install "jlens @ git+https://github.com/anthropics/jacobian-lens"
168
 
169
+ # Covenant (memory CLI) — do not vendor the whole tree into git history
170
  git clone https://github.com/LAWLESS1987/covenant vendor/covenant
171
  # or: export COVENANT_SRC=/path/to/covenant
172
 
173
+ ./scripts/fetch_lens.sh # PYTHON=… to choose the interpreter (default python3)
174
  ./scripts/seed_demo.sh # MODE=apply (default): proves SUCCESS via demo_apply.py
175
  # MODE=triad ./scripts/seed_demo.sh # full three-leg fire with fixed TASK_ID
176
  ```
177
 
178
+ On Windows (Git Bash), the scripts also find a venv's `Scripts/python.exe`. Paths written inside heredocs are
179
+ not translated for Windows Python, so give `fetch_lens.sh` a Windows-style `OUT=C:/…` there.
180
+
181
+ Memories default to `./data/ai_memory_root` (gitignored). Override with `AI_MEMORY_ROOT`, the tombstone log
182
+ with `TOMBSTONE_LOG`, and the run directory with `RUN_DIR`.
183
 
184
  ## How seed works
185
 
 
194
 
195
  `MODE=triad` additionally clones covenant if needed and runs `triad/fire_triad.sh`, expecting `FIRE_TRIAD_OK`.
196
 
197
+ The sample entry in `tombstone/tombstone.md` is illustrative of that same `task_id` format only. It predates
198
+ L-lens, so it carries the older `jlens_digest` note and no `llens_sha256`.
199
 
200
  ## Layout
201
 
202
  ```
203
+ threefold/
204
  README.md
205
  requirements.txt
206
+ jlens_snapshot.py # JLens capture: runs upstream apply, stores what it returned
207
+ llens.py # L-lens: hashing, derived summaries, packaging, verify, show
208
  demo_apply.py
209
  tombstone/
210
  append.sh
211
+ tombstone.md # SAMPLE only
212
  triad/
213
+ fire_triad.sh # the three legs, then verify_triad.py
214
+ on_speak.sh
215
  put_covenant_memory.sh
216
+ put_memory_file.py # covenant MemoryStore.put with the body read from a file
217
+ verify_triad.py # cross-leg verification, fails closed
218
  scripts/
219
  fetch_lens.sh
220
  seed_demo.sh
221
+ tests/
222
+ test_llens.py
223
+ check_against_upstream.py
224
+ e2e_triad.sh
225
  ```
226
 
227
  ## License / upstream
228
 
229
+ - **Jacobian Lens** (the instrument): [anthropics/jacobian-lens](https://github.com/anthropics/jacobian-lens),
230
+ © Anthropic PBC, Apache-2.0. It is installed as a dependency, not copied into this repository. Paper:
231
+ [Verbalizable Representations Form a Global Workspace in Language Models](https://transformer-circuits.pub/2026/workspace/index.html).
232
  - Pretrained lens weights: [neuronpedia/jacobian-lens](https://huggingface.co/neuronpedia/jacobian-lens)
233
+ - Subject model: [openai-community/gpt2](https://huggingface.co/openai-community/gpt2)
234
  - Covenant memory CLI: [LAWLESS1987/covenant](https://github.com/LAWLESS1987/covenant)
235
 
236
  ## Speak-turn hook
 
241
  ./triad/on_speak.sh "exact words spoken" "short-title"
242
  ```
243
 
244
+ This calls `fire_triad.sh` (all three legs required, then verified).
245
 
246
  ## Public memory root
247
 
demo_apply.py CHANGED
@@ -7,7 +7,10 @@ Lens: pretrained gpt2-small lens (fetch via scripts/fetch_lens.sh)
7
  from __future__ import annotations
8
 
9
  import os
10
- import resource
 
 
 
11
  import time
12
  import traceback
13
 
@@ -30,6 +33,8 @@ TOP_K = 5
30
 
31
 
32
  def rss_mb() -> float:
 
 
33
  return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0
34
 
35
 
 
7
  from __future__ import annotations
8
 
9
  import os
10
+ try:
11
+ import resource
12
+ except ImportError: # Windows has no resource module; RSS is then reported as nan
13
+ resource = None
14
  import time
15
  import traceback
16
 
 
33
 
34
 
35
  def rss_mb() -> float:
36
+ if resource is None:
37
+ return float("nan")
38
  return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0
39
 
40
 
jlens_snapshot.py CHANGED
@@ -1,119 +1,291 @@
1
  #!/usr/bin/env python3
2
- """Run real Anthropic jlens.JacobianLens.apply and write a triad snapshot.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  Usage:
5
- jlens_snapshot.py --task-id TID --prompt "..." --out SNAPSHOT_PATH [--digest DIGEST_PATH]
 
 
6
 
7
- Exits nonzero if apply fails or top-k is empty.
 
8
  """
9
  from __future__ import annotations
10
 
11
  import argparse
12
  import hashlib
 
13
  import json
14
  import os
 
 
15
  import sys
16
  import time
17
 
18
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
19
 
 
20
  import torch
21
  import transformers
22
  import jlens
23
 
 
 
24
  MODEL_ID = "openai-community/gpt2"
25
  # Default: lens next to this script (after scripts/fetch_lens.sh)
26
  DEFAULT_LENS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gpt2_jacobian_lens.pt")
27
- TOP_K = 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
 
30
- def topk_tokens(logits: torch.Tensor, tok, k: int = TOP_K) -> list[str]:
31
- ids = logits.topk(k).indices.tolist()
32
- return [tok.decode([t]) for t in ids]
33
 
34
 
35
- def main() -> int:
 
 
 
 
36
  ap = argparse.ArgumentParser()
37
  ap.add_argument("--task-id", required=True)
38
  ap.add_argument("--prompt", required=True)
39
- ap.add_argument("--out", required=True, help="JSON snapshot path")
 
40
  ap.add_argument("--digest", default="", help="optional short digest text path")
41
  ap.add_argument("--lens", default=DEFAULT_LENS)
 
42
  ap.add_argument("--model", default=MODEL_ID)
43
- args = ap.parse_args()
 
 
 
 
 
 
 
44
 
 
45
  t0 = time.perf_counter()
46
- hf = transformers.AutoModelForCausalLM.from_pretrained(args.model)
 
47
  hf.eval()
48
- tok = transformers.AutoTokenizer.from_pretrained(args.model)
49
  model = jlens.from_hf(hf, tok)
 
50
 
 
51
  lens = jlens.JacobianLens.load(args.lens)
 
 
 
 
 
 
 
 
 
 
52
 
53
- layers = [
54
- model.n_layers // 4,
55
- model.n_layers // 2,
56
- (model.n_layers * 3) // 4,
57
- model.n_layers - 2,
58
- ]
59
- layers = [L for L in layers if L in lens.jacobians]
60
- if not layers:
61
- layers = list(lens.source_layers)[:4]
62
-
63
- lens_logits, model_logits, _ = lens.apply(
64
- model, args.prompt, layers=layers, positions=[-2]
65
- )
66
-
67
- per_layer = {}
68
- lines = []
69
  for layer in layers:
70
- toks = topk_tokens(lens_logits[layer][0], tok)
71
- if not toks:
72
- print(f"ERROR: empty top-k at layer {layer}", file=sys.stderr)
73
- return 1
74
- per_layer[str(layer)] = toks
75
- lines.append(f"L{layer}: {toks}")
76
-
77
- model_top = topk_tokens(model_logits[0], tok)
78
- body = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  "type": "jlens",
80
  "task_id": args.task_id,
81
- "model_id": args.model,
82
- "lens_path": args.lens,
83
- "prompt": args.prompt,
84
- "layers": layers,
85
- "positions": [-2],
86
- "top_k": TOP_K,
87
- "jlens_top_k": per_layer,
88
- "model_final_top_k": model_top,
89
- "apply_s": round(time.perf_counter() - t0, 3),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  "mechanism": "anthropics/jacobian-lens JacobianLens.apply (real per-layer logits)",
91
  }
92
-
93
  os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
94
- with open(args.out, "w", encoding="utf-8") as f:
95
- json.dump(body, f, indent=2)
96
  f.write("\n")
97
 
98
- digest = (
99
- f"jlens task_id={args.task_id} model={args.model} "
100
- f"layers={layers} topk={{{'; '.join(lines)}}}"
101
- )
102
- digest_hash = hashlib.sha256(
103
- json.dumps(body, sort_keys=True).encode()
104
- ).hexdigest()[:16]
105
- digest_line = f"{digest} digest={digest_hash}"
106
-
107
  if args.digest:
108
- with open(args.digest, "w", encoding="utf-8") as f:
109
  f.write(digest_line + "\n")
110
- for line in lines:
111
- f.write(line + "\n")
112
-
113
  print(digest_line)
114
- for line in lines:
115
- print(line)
116
- print(f"SNAPSHOT_OK path={args.out}")
117
  return 0
118
 
119
 
 
1
  #!/usr/bin/env python3
2
+ """JLens capture: run Anthropic's Jacobian Lens and record what it returned.
3
+
4
+ This is the measurement step of Threefold's lens leg. The instrument is upstream `jlens.JacobianLens.apply` from
5
+ https://github.com/anthropics/jacobian-lens (Anthropic, Apache-2.0), called unmodified. Every array written to the
6
+ RAW .npz file holds values copied out of the tensors that call returned. The only operation applied to them is
7
+ selecting the top-K entries of each [vocab] row (torch.topk). Hashing and anything computed from the measurements
8
+ belong to L-lens (llens.py). Where L-lens has to work at capture time, because the full tensors are not kept, its
9
+ results go in the capture's separate "llens_capture_time" section, never in "raw".
10
+
11
+ Upstream's description: the Jacobian lens linearly transports a residual-stream vector at a layer and position
12
+ into the final-layer basis with the average input-output Jacobian J_l = E[dh_final/dh_l], fitted on a text corpus.
13
+ It then decodes the result with the model's own unembedding into logits over the vocabulary. A readout shows which
14
+ tokens that residual is disposed to make the model predict. It is a probe of one model's internal states. It is not
15
+ a reading of thoughts, beliefs or intent, and it measures the subject model named in the capture (GPT-2 small
16
+ reading the prompt text, by default), not the agent that wrote the text.
17
+
18
+ Readout. By default the capture reads every fitted layer of the lens (upstream apply's own default, layers=None) at
19
+ every token position (positions=None, truncated by upstream at --max-seq-len). It records:
20
+ - the Jacobian-lens logits (use_jacobian=True);
21
+ - upstream's vanilla logit-lens baseline (use_jacobian=False) at the same cells;
22
+ - the model's actual final-layer logits, which apply returns at the same positions.
23
+ For each (layer, position) cell, the top-K token ids and their exact float32 logits are stored. The full
24
+ [positions, vocab] tensors are too large to store per run (about 24 MB per layer at 120 tokens). L-lens hashes each
25
+ one instead, so a re-run can be compared bit for bit. apply is called once per layer, so peak memory holds one
26
+ layer's logits. Each call is a complete upstream apply with layers=[l]. The model's final logits must come out
27
+ identical across all calls, or the capture fails.
28
 
29
  Usage:
30
+ jlens_snapshot.py --task-id TID --prompt "..." --out CAPTURE_JSON [--raw RAW_NPZ] [--digest DIGEST_PATH]
31
+ [--lens PATH] [--lens-source TEXT] [--model ID] [--model-revision REV] [--top-k K] [--max-seq-len N]
32
+ [--layers 0,1,...] [--positions -2,...] [--no-baseline]
33
 
34
+ Exits nonzero if apply fails, a layer is not fitted, a logit is not finite, a top-K list is empty, or the final
35
+ logits differ between calls.
36
  """
37
  from __future__ import annotations
38
 
39
  import argparse
40
  import hashlib
41
+ import importlib.metadata as md
42
  import json
43
  import os
44
+ import platform
45
+ import subprocess
46
  import sys
47
  import time
48
 
49
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
50
 
51
+ import numpy as np
52
  import torch
53
  import transformers
54
  import jlens
55
 
56
+ import llens
57
+
58
  MODEL_ID = "openai-community/gpt2"
59
  # Default: lens next to this script (after scripts/fetch_lens.sh)
60
  DEFAULT_LENS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gpt2_jacobian_lens.pt")
61
+ DEFAULT_LENS_SOURCE = ("huggingface:neuronpedia/jacobian-lens "
62
+ "gpt2-small/jlens/Salesforce-wikitext/gpt2_jacobian_lens.pt")
63
+ TOP_K = 10 # upstream jlens.vis.compute_slice keeps top_n=10 per cell by default
64
+ MAX_SEQ_LEN = 512 # upstream apply's default
65
+ # Written "on/off", not as the Python literals: covenant's memory gate is a bag-of-words judge, and it refused a
66
+ # capture record on 2026-10-08 for the word in upstream's own parameter value ("evidence: false(194), false(194)").
67
+ UPSTREAM_CALL = ("jlens.JacobianLens.apply(model, prompt, layers=[l], positions=<positions>, "
68
+ "max_seq_len=<max_seq_len>, use_jacobian=<on for the Jacobian lens, off for the baseline>)")
69
+
70
+
71
+ def file_sha256(path: str) -> str:
72
+ h = hashlib.sha256()
73
+ with open(path, "rb") as f:
74
+ for chunk in iter(lambda: f.read(1 << 20), b""):
75
+ h.update(chunk)
76
+ return h.hexdigest()
77
+
78
+
79
+ def jlens_provenance() -> dict:
80
+ """Which copy of upstream ran: its distribution version and, when pip recorded one, the source commit."""
81
+ out = {"package": "jlens", "module_file": jlens.__file__}
82
+ try:
83
+ out["version"] = md.version("jlens")
84
+ direct = md.distribution("jlens").read_text("direct_url.json")
85
+ if direct:
86
+ d = json.loads(direct)
87
+ out["installed_from"] = d.get("url")
88
+ out["vcs_commit"] = (d.get("vcs_info") or {}).get("commit_id")
89
+ url = d.get("url") or ""
90
+ if not out["vcs_commit"] and url.startswith("file:"):
91
+ src = os.path.dirname(os.path.dirname(os.path.abspath(jlens.__file__)))
92
+ r = subprocess.run(["git", "-C", src, "rev-parse", "HEAD"], capture_output=True, text=True)
93
+ if r.returncode == 0:
94
+ out["vcs_commit"] = r.stdout.strip()
95
+ out["vcs_commit_from"] = "git rev-parse HEAD in the local source checkout"
96
+ except Exception as e: # noqa: BLE001 -- provenance is best effort, and says so
97
+ out["provenance_error"] = "%s: %s" % (type(e).__name__, e)
98
+ return out
99
+
100
+
101
+ def model_revision(model_id: str, requested):
102
+ """(commit, how it was found). transformers does not always set config._commit_hash; for a Hub model the
103
+ cached config.json sits in snapshots/<commit>/, so the directory name is the commit that was loaded."""
104
+ try:
105
+ from huggingface_hub import try_to_load_from_cache
106
+ p = try_to_load_from_cache(model_id, "config.json", revision=requested)
107
+ if isinstance(p, str):
108
+ return os.path.basename(os.path.dirname(p)), "huggingface_hub cache: snapshots/<commit>/config.json"
109
+ except Exception as e: # noqa: BLE001
110
+ return None, "not found (%s: %s)" % (type(e).__name__, e)
111
+ return None, "not found (not loaded from the Hugging Face Hub cache)"
112
 
113
 
114
+ def topk(logits: torch.Tensor, k: int):
115
+ v, i = logits.topk(k, dim=-1)
116
+ return i.cpu().numpy().astype(np.int64), v.cpu().numpy().astype(np.float32)
117
 
118
 
119
+ def parse_ints(s: str):
120
+ return [int(x) for x in s.split(",") if x.strip()] if s else None
121
+
122
+
123
+ def main(argv=None) -> int:
124
  ap = argparse.ArgumentParser()
125
  ap.add_argument("--task-id", required=True)
126
  ap.add_argument("--prompt", required=True)
127
+ ap.add_argument("--out", required=True, help="capture JSON path")
128
+ ap.add_argument("--raw", default="", help="raw arrays .npz path (default: next to --out)")
129
  ap.add_argument("--digest", default="", help="optional short digest text path")
130
  ap.add_argument("--lens", default=DEFAULT_LENS)
131
+ ap.add_argument("--lens-source", default=DEFAULT_LENS_SOURCE)
132
  ap.add_argument("--model", default=MODEL_ID)
133
+ ap.add_argument("--model-revision", default=None)
134
+ ap.add_argument("--top-k", type=int, default=TOP_K)
135
+ ap.add_argument("--max-seq-len", type=int, default=MAX_SEQ_LEN)
136
+ ap.add_argument("--layers", default="", help="comma list; default every fitted layer")
137
+ ap.add_argument("--positions", default="", help="comma list; default every position")
138
+ ap.add_argument("--no-baseline", action="store_true", help="skip the use_jacobian=False baseline")
139
+ args = ap.parse_args(argv)
140
+ raw_path = args.raw or os.path.join(os.path.dirname(os.path.abspath(args.out)), "jlens_raw.npz")
141
 
142
+ started = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
143
  t0 = time.perf_counter()
144
+ torch.manual_seed(0)
145
+ hf = transformers.AutoModelForCausalLM.from_pretrained(args.model, revision=args.model_revision)
146
  hf.eval()
147
+ tok = transformers.AutoTokenizer.from_pretrained(args.model, revision=args.model_revision)
148
  model = jlens.from_hf(hf, tok)
149
+ t_model = time.perf_counter() - t0
150
 
151
+ t1 = time.perf_counter()
152
  lens = jlens.JacobianLens.load(args.lens)
153
+ t_lens = time.perf_counter() - t1
154
+
155
+ layers = parse_ints(args.layers) or list(lens.source_layers)
156
+ missing = [l for l in layers if l not in lens.jacobians]
157
+ if missing:
158
+ print("ERROR: layers %s are not fitted in this lens (fitted: %s)" % (missing, lens.source_layers),
159
+ file=sys.stderr)
160
+ return 1
161
+ positions_arg = parse_ints(args.positions)
162
+ families = [("jacobian", True)] + ([] if args.no_baseline else [("baseline", False)])
163
 
164
+ ids_by, logits_by, full_sha = {f: [] for f, _ in families}, {f: [] for f, _ in families}, {f: {} for f, _ in families}
165
+ final_ids = final_logits = final_ref = input_ids = None
166
+ n_calls, t_apply = 0, {f: 0.0 for f, _ in families}
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  for layer in layers:
168
+ for fam, use_j in families:
169
+ ta = time.perf_counter()
170
+ lens_logits, model_logits, ids = lens.apply(model, args.prompt, layers=[layer], positions=positions_arg,
171
+ max_seq_len=args.max_seq_len, use_jacobian=use_j)
172
+ t_apply[fam] += time.perf_counter() - ta
173
+ n_calls += 1
174
+ x = lens_logits[layer]
175
+ if not torch.isfinite(x).all() or not torch.isfinite(model_logits).all():
176
+ print("ERROR: non-finite logits at layer %d (%s)" % (layer, fam), file=sys.stderr)
177
+ return 1
178
+ if final_ref is None:
179
+ final_ref, input_ids = model_logits, ids
180
+ final_ids, final_logits = topk(model_logits, args.top_k)
181
+ elif not torch.equal(final_ref, model_logits) or not torch.equal(input_ids, ids):
182
+ print("ERROR: final logits or input ids differ between apply calls (layer %d, %s)" % (layer, fam),
183
+ file=sys.stderr)
184
+ return 1
185
+ i, v = topk(x, args.top_k)
186
+ if i.size == 0:
187
+ print("ERROR: empty top-k at layer %d" % layer, file=sys.stderr)
188
+ return 1
189
+ ids_by[fam].append(i)
190
+ logits_by[fam].append(v)
191
+ full_sha[fam][str(layer)] = llens.array_sha256(x.numpy().astype(np.float32))
192
+ del lens_logits, x
193
+
194
+ n_tokens = int(input_ids.shape[1])
195
+ positions = ([p if p >= 0 else n_tokens + p for p in positions_arg] if positions_arg
196
+ else list(range(n_tokens)))
197
+ raw = {"input_ids": input_ids[0].cpu().numpy().astype(np.int64),
198
+ "layers": np.array(layers, dtype=np.int64),
199
+ "positions": np.array(positions, dtype=np.int64),
200
+ "final_top_ids": final_ids, "final_top_logits": final_logits}
201
+ for fam, _ in families:
202
+ raw[fam + "_top_ids"] = np.stack(ids_by[fam])
203
+ raw[fam + "_top_logits"] = np.stack(logits_by[fam])
204
+ os.makedirs(os.path.dirname(os.path.abspath(raw_path)) or ".", exist_ok=True)
205
+ np.savez_compressed(raw_path, **raw)
206
+
207
+ id_set = set(raw["input_ids"].tolist()) | set(int(t) for k in raw if k.endswith("_top_ids") for t in raw[k].ravel())
208
+ vocab = {str(t): tok.decode([t], clean_up_tokenization_spaces=False) for t in sorted(id_set)}
209
+ vocab_path = os.path.join(os.path.dirname(os.path.abspath(raw_path)), "jlens_vocab.json")
210
+ with open(vocab_path, "w", encoding="utf-8", newline="\n") as f:
211
+ json.dump(vocab, f, indent=0, sort_keys=True, ensure_ascii=False)
212
+ f.write("\n")
213
+ bos = getattr(tok, "bos_token_id", None)
214
+ rev = model_revision(args.model, args.model_revision)
215
+ capture = {
216
+ "schema": llens.CAPTURE_SCHEMA,
217
  "type": "jlens",
218
  "task_id": args.task_id,
219
+ "upstream": {"instrument": "Jacobian Lens (JLens)", "by": "Anthropic",
220
+ "source": "https://github.com/anthropics/jacobian-lens", "license": "Apache-2.0",
221
+ "call": UPSTREAM_CALL, "calls_made": n_calls, **jlens_provenance()},
222
+ "subject_model": {"id": args.model, "requested_revision": args.model_revision,
223
+ "revision": getattr(hf.config, "_commit_hash", None) or rev[0],
224
+ "revision_from": "config._commit_hash" if getattr(hf.config, "_commit_hash", None)
225
+ else rev[1],
226
+ "n_layers": model.n_layers, "d_model": model.d_model,
227
+ "vocab_size": int(final_ref.shape[-1]), "dtype": "float32", "device": "cpu",
228
+ "adapter": "jlens.from_hf(hf, tokenizer) with upstream defaults (force_bos=True)",
229
+ "note": "The lens reads this model's residual stream while it reads the prompt text. "
230
+ "It does not observe whichever agent wrote the text."},
231
+ "lens": {"path": os.path.abspath(args.lens), "sha256": file_sha256(args.lens), "source": args.lens_source,
232
+ "n_prompts": lens.n_prompts, "d_model": lens.d_model, "source_layers": list(lens.source_layers)},
233
+ "input": {"prompt": args.prompt, "prompt_sha256": hashlib.sha256(args.prompt.encode("utf-8")).hexdigest(),
234
+ "max_seq_len": args.max_seq_len, "n_tokens": n_tokens,
235
+ "starts_with_bos": bool(bos is not None and n_tokens and int(raw["input_ids"][0]) == bos),
236
+ "input_ids": raw["input_ids"].tolist()},
237
+ "readout": {"layers": layers, "positions": positions, "top_k": args.top_k, "baseline": not args.no_baseline,
238
+ "layers_rule": "explicit --layers" if args.layers else "every fitted layer (lens.source_layers)",
239
+ "positions_rule": "explicit --positions" if positions_arg else "every token position"},
240
+ "raw": {"file": os.path.basename(raw_path),
241
+ "meaning": {
242
+ "input_ids": "token ids upstream apply returned (model.encode of the prompt)",
243
+ "layers": "the layer of each row in the *_top_* arrays",
244
+ "positions": "absolute token position of each column",
245
+ "jacobian_top_ids/logits": "[layer, position, K] top-K of apply logits, use_jacobian on",
246
+ "baseline_top_ids/logits": "[layer, position, K] top-K of apply logits, use_jacobian off",
247
+ "final_top_ids/logits": "[position, K] top-K of the model's own final logits from apply",
248
+ "order": "torch.topk order, largest logit first; ties are ordered by torch, not by us"}},
249
+ "vocab": {"file": os.path.basename(vocab_path),
250
+ "what": "tokenizer.decode of every token id in the raw arrays. It is a view of the ids, not a "
251
+ "measurement. It is kept out of this record, and out of covenant memory bodies, because "
252
+ "covenant's memory gate reads a list of vocabulary words as if it were statements "
253
+ "(it refused one on 2026-10-08)."},
254
+ "llens_capture_time": {
255
+ "by": "llens.py (Threefold), not upstream",
256
+ "array_sha256": {k: llens.array_sha256(v) for k, v in raw.items()},
257
+ "vocab_sha256": llens.digest(vocab),
258
+ "full_tensor_sha256": {**full_sha, "final": llens.array_sha256(final_ref.numpy().astype(np.float32))},
259
+ "full_tensor_sha256_note": "sha256 of each complete [positions, vocab] float32 tensor apply returned "
260
+ "(not stored). Equal on a re-run means bit-identical logits. Bit equality "
261
+ "across different CPUs or BLAS builds is not guaranteed.",
262
+ "checks": {"all_logits_finite": True, "final_logits_identical_across_calls": True}},
263
+ "env": {"python": sys.version.split()[0], "platform": platform.platform(), "torch": torch.__version__,
264
+ "transformers": transformers.__version__, "numpy": np.__version__,
265
+ "torch_num_threads": torch.get_num_threads(), "seed": 0},
266
+ "timing": {"started_utc": started, "model_load_s": round(t_model, 3), "lens_load_s": round(t_lens, 3),
267
+ **{"apply_%s_s" % f: round(s, 3) for f, s in t_apply.items()},
268
+ "total_s": round(time.perf_counter() - t0, 3)},
269
+ # continuity with the pre-2026-10-08 snapshot fields
270
+ "model_id": args.model, "lens_path": args.lens, "prompt": args.prompt, "layers": layers,
271
+ "positions": positions, "top_k": args.top_k,
272
  "mechanism": "anthropics/jacobian-lens JacobianLens.apply (real per-layer logits)",
273
  }
 
274
  os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
275
+ with open(args.out, "w", encoding="utf-8", newline="\n") as f:
276
+ json.dump(capture, f, indent=1, sort_keys=True, ensure_ascii=False)
277
  f.write("\n")
278
 
279
+ cap_sha = llens.digest(capture)
280
+ digest_line = ("jlens task_id=%s model=%s layers=%d positions=%d top_k=%d baseline=%s capture_sha256=%s"
281
+ % (args.task_id, args.model, len(layers), len(positions), args.top_k,
282
+ "yes" if not args.no_baseline else "no", cap_sha))
 
 
 
 
 
283
  if args.digest:
284
+ with open(args.digest, "w", encoding="utf-8", newline="\n") as f:
285
  f.write(digest_line + "\n")
 
 
 
286
  print(digest_line)
287
+ print("JLENS_CAPTURE_SHA256=%s" % cap_sha)
288
+ print(f"SNAPSHOT_OK path={args.out} raw={raw_path} vocab={vocab_path}")
 
289
  return 0
290
 
291
 
llens.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """L-lens: Threefold's observability and provenance layer around upstream Jacobian Lens measurements.
3
+
4
+ L-lens is not the Jacobian Lens, and it is not a new name for it. The Jacobian Lens (JLens) is Anthropic's
5
+ mechanistic-interpretability instrument (https://github.com/anthropics/jacobian-lens, Apache-2.0). It linearly
6
+ transports a residual-stream vector at one layer and position into the final-layer basis, then decodes it with the
7
+ model's own unembedding into logits over the vocabulary. `jlens_snapshot.py` runs it and stores what it returned
8
+ ("raw").
9
+
10
+ L-lens is the part Threefold adds around those measurements. It does four things, all of which are ours:
11
+
12
+ 1. Hashing. It hashes every stored raw array, and the full [positions, vocab] tensors that are too large to keep
13
+ (at capture time). It also hashes the capture record in canonical JSON, so a later reader can tell whether
14
+ anything changed.
15
+ 2. Derivation. It derives a small set of integer summaries from the stored raw arrays (DEFINITIONS below). Every
16
+ one can be recomputed exactly from those arrays, and `verify_llens` does recompute them.
17
+ 3. Packaging. It puts the summaries and the digests into one artifact under the triad's task_id. The artifact
18
+ holds token ids, never decoded vocabulary strings. `llens.py show` decodes them for a reader from the
19
+ capture's hashed vocabulary file.
20
+ 4. Linking. The tombstone entry and the covenant task memory for that task_id cite the artifact's digest, and
21
+ `triad/verify_triad.py` fails if any of the links is absent or disagrees.
22
+
23
+ What none of it is: a reading of thoughts, beliefs, intent or truth. A JLens readout says which tokens a layer's
24
+ residual is disposed to make the model predict, under a lens fitted on a text corpus. The subject is the model named
25
+ in the capture (by default GPT-2 small reading the prompt text), not the agent that wrote the text. The derived
26
+ summaries describe how each layer's top-K list relates to the model's own final top-K at the same position. That is
27
+ an observability statistic about one model reading one text.
28
+
29
+ Usage:
30
+ llens.py build --capture CAPTURE_JSON --raw RAW_NPZ --out LLENS_JSON (prints LLENS_SHA256=<hex>)
31
+ llens.py verify --capture CAPTURE_JSON --raw RAW_NPZ [--llens LLENS_JSON]
32
+ llens.py show --capture CAPTURE_JSON --raw RAW_NPZ [--position P] [--n N]
33
+ The vocabulary file is read from the capture's directory (capture["vocab"]["file"]).
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import hashlib
39
+ import json
40
+ import sys
41
+ import time
42
+
43
+ import numpy as np
44
+
45
+ SCHEMA = "threefold.llens/1"
46
+ CAPTURE_SCHEMA = "threefold.jlens-capture/2"
47
+
48
+ STATEMENT = (
49
+ "L-lens is the Threefold observability and provenance layer built around upstream Jacobian Lens measurements "
50
+ "(anthropics/jacobian-lens, Apache-2.0). It is not a replacement name for the Jacobian Lens and adds no "
51
+ "measurement of its own. Everything under 'derived' is computed by L-lens from the stored raw arrays of the "
52
+ "capture this artifact cites. Nothing here is a reading of thoughts, beliefs, intent or ground truth."
53
+ )
54
+
55
+ UPSTREAM_CREDIT = {
56
+ "instrument": "Jacobian Lens (JLens)",
57
+ "by": "Anthropic",
58
+ "source": "https://github.com/anthropics/jacobian-lens",
59
+ "license": "Apache-2.0",
60
+ "paper": "https://transformer-circuits.pub/2026/workspace/index.html",
61
+ }
62
+
63
+ DEFINITIONS = {
64
+ "family": "jacobian = upstream JacobianLens.apply with use_jacobian on; baseline = the same call with "
65
+ "use_jacobian off (upstream's vanilla logit-lens baseline); final = the model's own final-layer "
66
+ "logits at the same positions, returned by the same calls.",
67
+ "top1_agree": "for one layer: the number of positions whose lens top-1 token id equals the final top-1 id.",
68
+ "final_top1_in_topk": "for one layer: the number of positions whose final top-1 id appears anywhere in the "
69
+ "lens top-K list.",
70
+ "topk_overlap": "for one layer: the sum over positions of |lens top-K ids AND final top-K ids|, out of "
71
+ "n_positions * K.",
72
+ "first_layer_final_top1_in_topk": "for one position: the first layer, in readout order, whose lens top-K list "
73
+ "contains the final top-1 id; null if none does. Limited to the stored top-K: "
74
+ "a token ranked K+1 counts as absent.",
75
+ }
76
+
77
+
78
+ # ----------------------------------------------------------------------------- hashing (L-lens)
79
+
80
+ def canonical_json(obj) -> bytes:
81
+ """One byte string per JSON value: sorted keys, no whitespace, UTF-8. Whitespace or key order in a file or a
82
+ memory body therefore cannot change a digest."""
83
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
84
+
85
+
86
+ def digest(obj) -> str:
87
+ return hashlib.sha256(canonical_json(obj)).hexdigest()
88
+
89
+
90
+ def array_sha256(a: np.ndarray) -> str:
91
+ """sha256 over dtype, shape and little-endian C-order bytes, so a reshaped or re-typed copy does not match."""
92
+ a = np.ascontiguousarray(a)
93
+ a = a.astype(a.dtype.newbyteorder("<"), copy=False)
94
+ h = hashlib.sha256()
95
+ h.update(("%s|%s|" % (a.dtype.str, ",".join(str(d) for d in a.shape))).encode("ascii"))
96
+ h.update(a.tobytes(order="C"))
97
+ return h.hexdigest()
98
+
99
+
100
+ def load_raw(path: str) -> dict:
101
+ with np.load(path, allow_pickle=False) as z:
102
+ return {k: np.array(z[k]) for k in z.files}
103
+
104
+
105
+ def load_vocab(capture_path: str, capture: dict):
106
+ """The capture's vocabulary file ({id: decoded string}), or None when the capture names none."""
107
+ name = (capture.get("vocab") or {}).get("file")
108
+ if not name:
109
+ return None
110
+ import os
111
+ with open(os.path.join(os.path.dirname(os.path.abspath(capture_path)), name), encoding="utf-8") as f:
112
+ return json.load(f)
113
+
114
+
115
+ # ----------------------------------------------------------------------------- derivation (L-lens)
116
+
117
+ def _family_stats(ids: np.ndarray, final: np.ndarray, layers: list) -> dict:
118
+ """ids: [L, n, K] lens top-K ids; final: [n, K] the model's own top-K ids at the same positions."""
119
+ n, k = final.shape
120
+ per_layer, first = {}, [None] * n
121
+ for li, layer in enumerate(layers):
122
+ lens_ids = ids[li]
123
+ agree = int(np.sum(lens_ids[:, 0] == final[:, 0]))
124
+ in_topk = [bool(final[p, 0] in lens_ids[p]) for p in range(n)]
125
+ overlap = int(sum(len(set(lens_ids[p].tolist()) & set(final[p].tolist())) for p in range(n)))
126
+ per_layer[str(layer)] = {"top1_agree": agree, "final_top1_in_topk": int(sum(in_topk)),
127
+ "topk_overlap": overlap, "n_positions": n, "k": k}
128
+ for p in range(n):
129
+ if first[p] is None and in_topk[p]:
130
+ first[p] = int(layer)
131
+ return {"per_layer": per_layer, "first_layer_final_top1_in_topk": first}
132
+
133
+
134
+ def derive(raw: dict) -> dict:
135
+ """The derived section: a pure function of the stored raw arrays, so verify can recompute it exactly."""
136
+ layers = [int(x) for x in raw["layers"].tolist()]
137
+ out = {"definitions": DEFINITIONS, "layers": layers, "positions": [int(x) for x in raw["positions"].tolist()],
138
+ "jacobian": _family_stats(raw["jacobian_top_ids"], raw["final_top_ids"], layers)}
139
+ if "baseline_top_ids" in raw:
140
+ out["baseline"] = _family_stats(raw["baseline_top_ids"], raw["final_top_ids"], layers)
141
+ return out
142
+
143
+
144
+ def readable(raw: dict, vocab: dict, position: int = -1, n_tokens: int = 5) -> dict:
145
+ """A human view of one position, decoded with the capture's vocabulary file (the subject model's own tokenizer
146
+ output at capture time). Presentation only: it is not stored in the artifact, and nothing is computed here."""
147
+ word = lambda i: (vocab or {}).get(str(int(i)), "<id %d>" % int(i)) # noqa: E731
148
+ p = position % int(raw["final_top_ids"].shape[0])
149
+ layers = [int(x) for x in raw["layers"].tolist()]
150
+ out = {"position": int(raw["positions"][p]),
151
+ "jacobian_top_tokens_by_layer": {str(l): [word(i) for i in raw["jacobian_top_ids"][li, p, :n_tokens]]
152
+ for li, l in enumerate(layers)},
153
+ "final_top_tokens": [word(i) for i in raw["final_top_ids"][p, :n_tokens]]}
154
+ if "baseline_top_ids" in raw:
155
+ out["baseline_top_tokens_by_layer"] = {str(l): [word(i) for i in raw["baseline_top_ids"][li, p, :n_tokens]]
156
+ for li, l in enumerate(layers)}
157
+ return out
158
+
159
+
160
+ def build(capture: dict, raw: dict, vocab=None) -> dict:
161
+ problems = [why for ok, _, why in verify_capture(capture, raw, vocab) if not ok]
162
+ if problems:
163
+ raise ValueError("capture does not verify; L-lens will not package it: " + "; ".join(problems))
164
+ task_id = capture["task_id"]
165
+ return {
166
+ "schema": SCHEMA,
167
+ "type": "llens",
168
+ "task_id": task_id,
169
+ "what": STATEMENT,
170
+ "upstream_credit": UPSTREAM_CREDIT,
171
+ "subject": {"model_id": capture["subject_model"]["id"],
172
+ "revision": capture["subject_model"].get("revision"),
173
+ "note": capture["subject_model"].get("note")},
174
+ "inputs": {"jlens_capture_sha256": digest(capture), "jlens_capture_schema": capture.get("schema"),
175
+ "raw_array_sha256": capture["llens_capture_time"]["array_sha256"]},
176
+ "derived": derive(raw),
177
+ "links": {"task_id": task_id,
178
+ "tombstone": "the tombstone entry whose notes carry task_id=%s and llens_sha256" % task_id,
179
+ "covenant": ["task-%s" % task_id, "jlens-%s" % task_id, "llens-%s" % task_id]},
180
+ "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
181
+ }
182
+
183
+
184
+ # ----------------------------------------------------------------------------- verification
185
+
186
+ def verify_capture(capture: dict, raw: dict, vocab=None) -> list:
187
+ """[(ok, check, why)] -- the capture's own consistency: its arrays match the digests it recorded, their shapes
188
+ match the readout it declares, and (when given) the vocabulary file is the one it recorded."""
189
+ out = []
190
+
191
+ def check(ok, name, why=""):
192
+ out.append((bool(ok), name, why))
193
+
194
+ if vocab is not None:
195
+ check(digest(vocab) == (capture.get("llens_capture_time") or {}).get("vocab_sha256"), "vocab digest",
196
+ "the vocabulary file is not the one recorded at capture time")
197
+
198
+ check(capture.get("schema") == CAPTURE_SCHEMA, "capture schema", "schema is %r" % capture.get("schema"))
199
+ check(bool(capture.get("task_id")), "capture task_id", "no task_id")
200
+ recorded = (capture.get("llens_capture_time") or {}).get("array_sha256") or {}
201
+ check(set(recorded) == set(raw), "raw array set",
202
+ "recorded %s, file has %s" % (sorted(recorded), sorted(raw)))
203
+ for name in sorted(set(recorded) & set(raw)):
204
+ check(array_sha256(raw[name]) == recorded[name], "raw %s digest" % name, "%s changed since capture" % name)
205
+ ro = capture.get("readout") or {}
206
+ try:
207
+ L, n, K = len(ro["layers"]), len(ro["positions"]), int(ro["top_k"])
208
+ shapes = {"jacobian_top_ids": (L, n, K), "jacobian_top_logits": (L, n, K),
209
+ "final_top_ids": (n, K), "final_top_logits": (n, K), "layers": (L,), "positions": (n,)}
210
+ if ro.get("baseline"):
211
+ shapes.update(baseline_top_ids=(L, n, K), baseline_top_logits=(L, n, K))
212
+ for name, shape in shapes.items():
213
+ check(name in raw and tuple(raw[name].shape) == shape, "raw %s shape" % name,
214
+ "%s is %s, readout says %s" % (name, tuple(raw[name].shape) if name in raw else None, shape))
215
+ check("layers" in raw and [int(x) for x in raw["layers"]] == list(ro["layers"]), "raw layers agree",
216
+ "the layers array is not the declared readout")
217
+ check("positions" in raw and [int(x) for x in raw["positions"]] == list(ro["positions"]),
218
+ "raw positions agree", "the positions array is not the declared readout")
219
+ except (KeyError, TypeError, ValueError) as e:
220
+ check(False, "readout declared", "%s: %s" % (type(e).__name__, e))
221
+ return out
222
+
223
+
224
+ def verify_llens(llens: dict, capture: dict, raw: dict, vocab=None) -> list:
225
+ out = verify_capture(capture, raw, vocab)
226
+
227
+ def check(ok, name, why=""):
228
+ out.append((bool(ok), name, why))
229
+
230
+ check(llens.get("schema") == SCHEMA, "llens schema", "schema is %r" % llens.get("schema"))
231
+ check(llens.get("task_id") == capture.get("task_id"), "llens task_id agrees",
232
+ "llens %r, capture %r" % (llens.get("task_id"), capture.get("task_id")))
233
+ check((llens.get("inputs") or {}).get("jlens_capture_sha256") == digest(capture), "llens cites this capture",
234
+ "the capture's digest is not the one L-lens recorded")
235
+ check(llens.get("derived") == json.loads(json.dumps(derive(raw))), "derived recomputes from raw",
236
+ "the stored derived section is not what the raw arrays give")
237
+ return out
238
+
239
+
240
+ def _report(results) -> int:
241
+ bad = [r for r in results if not r[0]]
242
+ for ok, name, why in results:
243
+ print(" %s %s%s" % ("PASS" if ok else "FAIL", name, "" if ok else " -- " + why))
244
+ return 1 if bad else 0
245
+
246
+
247
+ def main(argv=None) -> int:
248
+ ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
249
+ sub = ap.add_subparsers(dest="cmd", required=True)
250
+ b = sub.add_parser("build")
251
+ b.add_argument("--capture", required=True)
252
+ b.add_argument("--raw", required=True)
253
+ b.add_argument("--out", required=True)
254
+ v = sub.add_parser("verify")
255
+ v.add_argument("--capture", required=True)
256
+ v.add_argument("--raw", required=True)
257
+ v.add_argument("--llens", default="")
258
+ s = sub.add_parser("show")
259
+ s.add_argument("--capture", required=True)
260
+ s.add_argument("--raw", required=True)
261
+ s.add_argument("--position", type=int, default=-1, help="index into the readout's positions (default last)")
262
+ s.add_argument("--n", type=int, default=5)
263
+ a = ap.parse_args(argv)
264
+
265
+ with open(a.capture, encoding="utf-8") as f:
266
+ capture = json.load(f)
267
+ raw = load_raw(a.raw)
268
+ try:
269
+ vocab = load_vocab(a.capture, capture)
270
+ except OSError as e:
271
+ print("LLENS_FAIL the capture names a vocabulary file that cannot be read: %s" % e, file=sys.stderr)
272
+ return 1
273
+ if a.cmd == "show":
274
+ print(json.dumps(readable(raw, vocab, a.position, a.n), indent=1, ensure_ascii=False))
275
+ return 0
276
+ if a.cmd == "build":
277
+ try:
278
+ art = build(capture, raw, vocab)
279
+ except ValueError as e:
280
+ print("LLENS_FAIL %s" % e, file=sys.stderr)
281
+ return 1
282
+ with open(a.out, "w", encoding="utf-8", newline="\n") as f:
283
+ json.dump(art, f, indent=1, sort_keys=True, ensure_ascii=False)
284
+ f.write("\n")
285
+ print("LLENS_SHA256=%s" % digest(art))
286
+ print("LLENS_OK path=%s" % a.out)
287
+ return 0
288
+ if a.llens:
289
+ with open(a.llens, encoding="utf-8") as f:
290
+ rc = _report(verify_llens(json.load(f), capture, raw, vocab))
291
+ else:
292
+ rc = _report(verify_capture(capture, raw, vocab))
293
+ print("LLENS_VERIFY_OK" if rc == 0 else "LLENS_VERIFY_FAIL")
294
+ return rc
295
+
296
+
297
+ if __name__ == "__main__":
298
+ raise SystemExit(main())
requirements.txt CHANGED
@@ -8,5 +8,7 @@
8
  torch==2.14.1
9
  transformers==5.19.0
10
  huggingface_hub>=0.20.0
 
 
11
  # jlens: install from git (not on PyPI as a stable pin here)
12
  # pip install "jlens @ git+https://github.com/anthropics/jacobian-lens"
 
8
  torch==2.14.1
9
  transformers==5.19.0
10
  huggingface_hub>=0.20.0
11
+ numpy
12
+ # tests: pip install pytest
13
  # jlens: install from git (not on PyPI as a stable pin here)
14
  # pip install "jlens @ git+https://github.com/anthropics/jacobian-lens"
scripts/fetch_lens.sh CHANGED
@@ -8,6 +8,7 @@
8
  # Usage:
9
  # ./scripts/fetch_lens.sh
10
  # OUT=/path/to/gpt2_jacobian_lens.pt ./scripts/fetch_lens.sh
 
11
  set -euo pipefail
12
 
13
  REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@@ -28,8 +29,8 @@ if command -v huggingface-cli >/dev/null 2>&1; then
28
  huggingface-cli download "$REPO_ID" "$HF_PATH" --local-dir "$TMPDIR" --local-dir-use-symlinks False
29
  mv "$TMPDIR/$HF_PATH" "$OUT"
30
  rm -rf "$TMPDIR"
31
- elif python3 -c "import huggingface_hub" 2>/dev/null; then
32
- python3 - <<PY
33
  from huggingface_hub import hf_hub_download
34
  import shutil, os
35
  path = hf_hub_download(repo_id="${REPO_ID}", filename="${HF_PATH}")
 
8
  # Usage:
9
  # ./scripts/fetch_lens.sh
10
  # OUT=/path/to/gpt2_jacobian_lens.pt ./scripts/fetch_lens.sh
11
+ # PYTHON=/path/to/venv/python ./scripts/fetch_lens.sh (default python3)
12
  set -euo pipefail
13
 
14
  REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
 
29
  huggingface-cli download "$REPO_ID" "$HF_PATH" --local-dir "$TMPDIR" --local-dir-use-symlinks False
30
  mv "$TMPDIR/$HF_PATH" "$OUT"
31
  rm -rf "$TMPDIR"
32
+ elif "${PYTHON:-python3}" -c "import huggingface_hub" 2>/dev/null; then
33
+ "${PYTHON:-python3}" - <<PY
34
  from huggingface_hub import hf_hub_download
35
  import shutil, os
36
  path = hf_hub_download(repo_id="${REPO_ID}", filename="${HF_PATH}")
scripts/seed_demo.sh CHANGED
@@ -43,6 +43,8 @@ export JLENS_LENS="${JLENS_LENS:-$REPO_ROOT/gpt2_jacobian_lens.pt}"
43
  # Resolve python
44
  if [[ -x "$JLENS_VENV/bin/python" ]]; then
45
  PYTHON="$JLENS_VENV/bin/python"
 
 
46
  else
47
  PYTHON="$(command -v python3)"
48
  fi
@@ -51,7 +53,7 @@ echo "PYTHON=$PYTHON"
51
  if [[ "$MODE" == "apply" ]]; then
52
  echo "--- demo_apply.py ---"
53
  TRIAD_PROMPT="$PROMPT" JLENS_LENS="$JLENS_LENS" "$PYTHON" "$REPO_ROOT/demo_apply.py" | tee /tmp/triad_seed_demo_apply.log
54
- rg -n "SUCCESS: lens.apply returned real top-k tokens" /tmp/triad_seed_demo_apply.log
55
  echo "SEED_DEMO_OK mode=apply"
56
  exit 0
57
  fi
@@ -79,7 +81,7 @@ if [[ "$MODE" == "triad" ]]; then
79
  "none" \
80
  "Third leg must be real jlens.apply not self-observation" \
81
  "seed-demo" | tee /tmp/triad_seed_demo_triad.log
82
- rg -n "FIRE_TRIAD_OK" /tmp/triad_seed_demo_triad.log
83
  echo "SEED_DEMO_OK mode=triad task_id=$TASK_ID"
84
  exit 0
85
  fi
 
43
  # Resolve python
44
  if [[ -x "$JLENS_VENV/bin/python" ]]; then
45
  PYTHON="$JLENS_VENV/bin/python"
46
+ elif [[ -x "$JLENS_VENV/Scripts/python.exe" ]]; then
47
+ PYTHON="$JLENS_VENV/Scripts/python.exe" # a Windows venv
48
  else
49
  PYTHON="$(command -v python3)"
50
  fi
 
53
  if [[ "$MODE" == "apply" ]]; then
54
  echo "--- demo_apply.py ---"
55
  TRIAD_PROMPT="$PROMPT" JLENS_LENS="$JLENS_LENS" "$PYTHON" "$REPO_ROOT/demo_apply.py" | tee /tmp/triad_seed_demo_apply.log
56
+ grep -n -F "SUCCESS: lens.apply returned real top-k tokens" /tmp/triad_seed_demo_apply.log
57
  echo "SEED_DEMO_OK mode=apply"
58
  exit 0
59
  fi
 
81
  "none" \
82
  "Third leg must be real jlens.apply not self-observation" \
83
  "seed-demo" | tee /tmp/triad_seed_demo_triad.log
84
+ grep -n -F "FIRE_TRIAD_OK" /tmp/triad_seed_demo_triad.log
85
  echo "SEED_DEMO_OK mode=triad task_id=$TASK_ID"
86
  exit 0
87
  fi
tests/check_against_upstream.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Does a stored capture hold what upstream returns? An independent re-run, compared cell by cell.
3
+
4
+ For a capture made by jlens_snapshot.py, this loads the same model and lens, calls upstream
5
+ jlens.JacobianLens.apply the way the upstream README does (all requested layers in ONE call, which
6
+ jlens_snapshot.py does not do), and checks:
7
+ - the stored top-K ids and logits equal the direct call's, exactly, for every layer and position;
8
+ - the stored final-layer top-K equals the direct call's model_logits top-K;
9
+ - each full [positions, vocab] tensor hashes to the full_tensor_sha256 L-lens recorded at capture time;
10
+ - the use_jacobian=False baseline, likewise.
11
+ Exit 0 only if everything matches. Needs torch, transformers, jlens, the model and the lens file.
12
+
13
+ Usage: check_against_upstream.py RUN_DIR [--lens PATH]
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+
22
+ import numpy as np
23
+ import torch
24
+ import transformers
25
+ import jlens
26
+
27
+ HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
+ sys.path.insert(0, HERE)
29
+ import llens # noqa: E402
30
+
31
+
32
+ def main(argv=None) -> int:
33
+ ap = argparse.ArgumentParser()
34
+ ap.add_argument("run_dir")
35
+ ap.add_argument("--lens", default="")
36
+ a = ap.parse_args(argv)
37
+ cap = json.load(open(os.path.join(a.run_dir, "jlens_snapshot.json"), encoding="utf-8"))
38
+ raw = llens.load_raw(os.path.join(a.run_dir, cap["raw"]["file"]))
39
+ ro = cap["readout"]
40
+ lens_path = a.lens or cap["lens"]["path"]
41
+ if llens_sha(lens_path) != cap["lens"]["sha256"]:
42
+ print("FAIL lens file differs from the one the capture recorded")
43
+ return 1
44
+ hf = transformers.AutoModelForCausalLM.from_pretrained(cap["subject_model"]["id"],
45
+ revision=cap["subject_model"].get("revision"))
46
+ hf.eval()
47
+ tok = transformers.AutoTokenizer.from_pretrained(cap["subject_model"]["id"],
48
+ revision=cap["subject_model"].get("revision"))
49
+ model = jlens.from_hf(hf, tok)
50
+ lens = jlens.JacobianLens.load(lens_path)
51
+ k, bad, n = ro["top_k"], 0, 0
52
+ fams = [("jacobian", True)] + ([("baseline", False)] if ro["baseline"] else [])
53
+ for fam, use_j in fams:
54
+ lens_logits, model_logits, ids = lens.apply(model, cap["input"]["prompt"], layers=ro["layers"],
55
+ positions=ro["positions"], max_seq_len=cap["input"]["max_seq_len"],
56
+ use_jacobian=use_j)
57
+ n += 1
58
+ if ids[0].tolist() != cap["input"]["input_ids"]:
59
+ print("FAIL %s: input ids differ" % fam)
60
+ bad += 1
61
+ for li, layer in enumerate(ro["layers"]):
62
+ v, i = lens_logits[layer].topk(k, dim=-1)
63
+ ok_ids = np.array_equal(i.numpy(), raw[fam + "_top_ids"][li])
64
+ ok_val = np.array_equal(v.numpy().astype(np.float32), raw[fam + "_top_logits"][li])
65
+ ok_full = (llens.array_sha256(lens_logits[layer].numpy().astype(np.float32))
66
+ == cap["llens_capture_time"]["full_tensor_sha256"][fam][str(layer)])
67
+ n += 3
68
+ if not (ok_ids and ok_val and ok_full):
69
+ bad += 1
70
+ print("FAIL %s layer %d: ids %s, logits %s, full-tensor digest %s" % (fam, layer, ok_ids, ok_val, ok_full))
71
+ v, i = model_logits.topk(k, dim=-1)
72
+ ok = (np.array_equal(i.numpy(), raw["final_top_ids"]) and np.array_equal(v.numpy(), raw["final_top_logits"])
73
+ and llens.array_sha256(model_logits.numpy().astype(np.float32))
74
+ == cap["llens_capture_time"]["full_tensor_sha256"]["final"])
75
+ n += 1
76
+ if not ok:
77
+ bad += 1
78
+ print("FAIL %s: final-layer top-K or digest differs" % fam)
79
+ print("UPSTREAM_MATCH %d of %d comparisons" % (n - bad, n) if not bad else "UPSTREAM_MISMATCH %d" % bad)
80
+ return 1 if bad else 0
81
+
82
+
83
+ def llens_sha(path):
84
+ import hashlib
85
+ h = hashlib.sha256()
86
+ with open(path, "rb") as f:
87
+ for chunk in iter(lambda: f.read(1 << 20), b""):
88
+ h.update(chunk)
89
+ return h.hexdigest()
90
+
91
+
92
+ if __name__ == "__main__":
93
+ raise SystemExit(main())
tests/e2e_triad.sh ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # e2e_triad.sh — the whole triad on a real model, in a scratch directory, driven both ways.
3
+ #
4
+ # Runs triad/on_speak.sh with the tombstone log, the memory root and the run
5
+ # directory all inside a fresh temp dir. It never touches tombstone/tombstone.md,
6
+ # and never pushes: the scratch memory root is not a git repo, so the push step
7
+ # is skipped. Then it breaks the triad on purpose, and each break must fail:
8
+ # 1. a tombstone entry citing another L-lens artifact -> verify_triad exits 1
9
+ # 2. the L-lens memory removed from covenant -> verify_triad exits 1
10
+ # 3. a lens file that is not a lens -> fire_triad exits
11
+ # nonzero BEFORE the tombstone is written (fail closed, nothing appended)
12
+ #
13
+ # Needs: the venv (JLENS_VENV, default <repo>/.venv), the lens file, the model,
14
+ # rg, and covenant's ai_memory_system (COVENANT_SRC).
15
+ # Prints E2E_TRIAD_OK when every step behaved as stated.
16
+ set -euo pipefail
17
+
18
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
19
+ SCRATCH="$(mktemp -d)"
20
+ trap 'rm -rf "$SCRATCH"' EXIT
21
+ export JLENS_VENV="${JLENS_VENV:-$REPO_ROOT/.venv}"
22
+ export JLENS_LENS="${JLENS_LENS:-$REPO_ROOT/gpt2_jacobian_lens.pt}"
23
+ export COVENANT_SRC="${COVENANT_SRC:?set COVENANT_SRC to a covenant checkout}"
24
+ export TOMBSTONE_LOG="$SCRATCH/tombstone.md"
25
+ export AI_MEMORY_ROOT="$SCRATCH/memory"
26
+ export RUN_DIR="$SCRATCH/runs"
27
+ if [[ -x "$JLENS_VENV/bin/python" ]]; then PY="$JLENS_VENV/bin/python"; else PY="$JLENS_VENV/Scripts/python.exe"; fi
28
+ printf '# scratch tombstone log\n' > "$TOMBSTONE_LOG"
29
+ mkdir -p "$AI_MEMORY_ROOT"
30
+ SAMPLE_BEFORE="$(sha256sum "$REPO_ROOT/tombstone/tombstone.md" | cut -c1-64)"
31
+
32
+ fail() { echo "E2E FAIL: $*" >&2; exit 1; }
33
+
34
+ echo "=== 1. a real speak-turn fires all three legs and verifies ==="
35
+ export TASK_ID="e2e-$(date -u +%Y%m%d-%H%M%S)-$$"
36
+ OUT="$("$REPO_ROOT/triad/on_speak.sh" "The covenant keeps its record append-only, and every claim cites the check that backs it." "e2e")" \
37
+ || { printf '%s\n' "$OUT"; fail "on_speak.sh exited nonzero on a clean run"; }
38
+ printf '%s\n' "$OUT" | grep -E "SNAPSHOT_OK|LLENS_OK|LLENS_VERIFY_OK|VERIFY_TRIAD_OK|FIRE_TRIAD_OK|Wrote covenant"
39
+ printf '%s\n' "$OUT" | grep -q "^VERIFY_TRIAD_OK task_id=$TASK_ID" || fail "no VERIFY_TRIAD_OK"
40
+ printf '%s\n' "$OUT" | grep -q "^FIRE_TRIAD_OK task_id=$TASK_ID" || fail "no FIRE_TRIAD_OK"
41
+ for m in task jlens llens; do [[ -f "$AI_MEMORY_ROOT/$m-$TASK_ID.md" ]] || fail "covenant memory $m-$TASK_ID absent"; done
42
+ V=("$PY" "$REPO_ROOT/triad/verify_triad.py" --task-id "$TASK_ID" --run-dir "$RUN_DIR/$TASK_ID"
43
+ --tombstone-log "$TOMBSTONE_LOG" --memory-root "$AI_MEMORY_ROOT")
44
+ "${V[@]}" >/dev/null || fail "verify_triad fails on the clean triad"
45
+
46
+ echo "=== 2. break: the tombstone cites another L-lens artifact ==="
47
+ cp "$TOMBSTONE_LOG" "$SCRATCH/tombstone.bak"
48
+ sed -i -E "s/llens_sha256=[0-9a-f]{64}/llens_sha256=$(printf '0%.0s' {1..64})/" "$TOMBSTONE_LOG"
49
+ if "${V[@]}" > "$SCRATCH/v2.txt"; then fail "verify_triad passed with a wrong tombstone digest"; fi
50
+ grep "FAIL tombstone: llens_sha256 agrees" "$SCRATCH/v2.txt" || fail "the wrong digest was not the named failure"
51
+ cp "$SCRATCH/tombstone.bak" "$TOMBSTONE_LOG"
52
+ "${V[@]}" >/dev/null || fail "restoring the tombstone did not restore green"
53
+
54
+ echo "=== 3. break: the L-lens memory is gone from covenant ==="
55
+ mv "$AI_MEMORY_ROOT/llens-$TASK_ID.md" "$SCRATCH/llens.bak"
56
+ if "${V[@]}" > "$SCRATCH/v3.txt"; then fail "verify_triad passed without the L-lens memory"; fi
57
+ grep "FAIL covenant: llens-$TASK_ID present" "$SCRATCH/v3.txt" || fail "the missing memory was not the named failure"
58
+ mv "$SCRATCH/llens.bak" "$AI_MEMORY_ROOT/llens-$TASK_ID.md"
59
+ "${V[@]}" >/dev/null || fail "restoring the memory did not restore green"
60
+
61
+ echo "=== 4. break: the lens leg fails; nothing may be appended ==="
62
+ LINES_BEFORE="$(wc -l < "$TOMBSTONE_LOG")"
63
+ printf 'not a lens\n' > "$SCRATCH/bad_lens.pt"
64
+ if JLENS_LENS="$SCRATCH/bad_lens.pt" TASK_ID="e2e-bad-$$" "$REPO_ROOT/triad/on_speak.sh" "x" "e2e-bad" > "$SCRATCH/bad.txt" 2>&1; then
65
+ fail "fire_triad succeeded with a broken lens"
66
+ fi
67
+ [[ "$(wc -l < "$TOMBSTONE_LOG")" == "$LINES_BEFORE" ]] || fail "the tombstone was written although the lens leg failed"
68
+ [[ ! -e "$AI_MEMORY_ROOT/task-e2e-bad-$$.md" ]] || fail "a covenant memory was written although the lens leg failed"
69
+ echo "lens leg failed as it should; tombstone and covenant untouched"
70
+
71
+ [[ "$(sha256sum "$REPO_ROOT/tombstone/tombstone.md" | cut -c1-64)" == "$SAMPLE_BEFORE" ]] || fail "the repo's tombstone sample changed"
72
+ echo "E2E_TRIAD_OK task_id=$TASK_ID"
tests/test_llens.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """L-lens and triad verification, without a model: synthetic captures, then each link broken on purpose.
2
+
3
+ Every check is driven both ways. A clean artifact passes, and one specific tamper must fail with a named reason. A
4
+ verifier that has only been seen passing has not been observed.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ import numpy as np
14
+ import pytest
15
+
16
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ sys.path.insert(0, ROOT)
18
+ sys.path.insert(0, os.path.join(ROOT, "triad"))
19
+ import llens # noqa: E402
20
+ import verify_triad # noqa: E402
21
+
22
+ TASK = "20261008-000000-1"
23
+
24
+
25
+ def synthetic(task_id=TASK, baseline=True):
26
+ """2 layers x 3 positions x K=2, with known answers:
27
+ final top ids per position: [5,6], [7,8], [9,1]
28
+ jacobian layer 0: [5,0], [0,7], [2,3] -> top1 agree 1 (pos 0); final top1 in top-K: pos0, pos1 -> 2
29
+ jacobian layer 1: [5,6], [7,8], [1,9] -> top1 agree 2; in top-K 3; overlap 2+2+2 = 6
30
+ first layer the final top1 is in top-K: pos0 -> 0, pos1 -> 0, pos2 -> 1
31
+ """
32
+ raw = {"input_ids": np.array([50256, 11, 12], dtype=np.int64),
33
+ "layers": np.array([0, 1], dtype=np.int64), "positions": np.array([0, 1, 2], dtype=np.int64),
34
+ "final_top_ids": np.array([[5, 6], [7, 8], [9, 1]], dtype=np.int64),
35
+ "final_top_logits": np.array([[3, 2], [3, 2], [3, 2]], dtype=np.float32),
36
+ "jacobian_top_ids": np.array([[[5, 0], [0, 7], [2, 3]], [[5, 6], [7, 8], [1, 9]]], dtype=np.int64),
37
+ "jacobian_top_logits": np.ones((2, 3, 2), dtype=np.float32)}
38
+ if baseline:
39
+ raw["baseline_top_ids"] = np.array([[[0, 1], [2, 3], [4, 5]], [[5, 0], [7, 0], [9, 0]]], dtype=np.int64)
40
+ raw["baseline_top_logits"] = np.ones((2, 3, 2), dtype=np.float32)
41
+ cap = {"schema": llens.CAPTURE_SCHEMA, "type": "jlens", "task_id": task_id,
42
+ "subject_model": {"id": "synthetic", "revision": None, "note": "test"},
43
+ "input": {"prompt": "a b", "input_ids": [50256, 11, 12]},
44
+ "readout": {"layers": [0, 1], "positions": [0, 1, 2], "top_k": 2, "baseline": baseline},
45
+ "vocab": {"file": "jlens_vocab.json"},
46
+ "llens_capture_time": {"array_sha256": {k: llens.array_sha256(v) for k, v in raw.items()},
47
+ "vocab_sha256": llens.digest(VOCAB)}}
48
+ return cap, raw
49
+
50
+
51
+ VOCAB = {str(i): "t%d" % i for i in range(20)} | {"50256": "<|endoftext|>"}
52
+
53
+
54
+ def roundtrip(obj):
55
+ return json.loads(json.dumps(obj))
56
+
57
+
58
+ # --------------------------------------------------------------------------- derivation
59
+
60
+ def test_derived_values_are_the_hand_computed_ones():
61
+ cap, raw = synthetic()
62
+ d = llens.derive(raw)
63
+ j = d["jacobian"]["per_layer"]
64
+ assert j["0"] == {"top1_agree": 1, "final_top1_in_topk": 2, "topk_overlap": 2, "n_positions": 3, "k": 2}
65
+ assert j["1"] == {"top1_agree": 2, "final_top1_in_topk": 3, "topk_overlap": 6, "n_positions": 3, "k": 2}
66
+ assert d["jacobian"]["first_layer_final_top1_in_topk"] == [0, 0, 1]
67
+ assert d["baseline"]["per_layer"]["0"]["top1_agree"] == 0
68
+ assert d["baseline"]["first_layer_final_top1_in_topk"] == [1, 1, 1]
69
+
70
+
71
+ def test_no_baseline_means_no_baseline_section():
72
+ cap, raw = synthetic(baseline=False)
73
+ assert "baseline" not in llens.derive(raw)
74
+ assert all(ok for ok, _, _ in llens.verify_capture(cap, raw))
75
+
76
+
77
+ def test_artifact_says_what_it_is_and_credits_upstream():
78
+ cap, raw = synthetic()
79
+ art = llens.build(cap, raw)
80
+ assert art["type"] == "llens" and art["task_id"] == TASK
81
+ assert "not a replacement name for the Jacobian Lens" in art["what"]
82
+ assert art["upstream_credit"]["source"] == "https://github.com/anthropics/jacobian-lens"
83
+ assert art["upstream_credit"]["license"] == "Apache-2.0"
84
+ assert art["inputs"]["jlens_capture_sha256"] == llens.digest(cap)
85
+
86
+
87
+ # --------------------------------------------------------------------------- hashing
88
+
89
+ def test_canonical_digest_ignores_whitespace_and_key_order_but_not_content():
90
+ a = {"b": 1, "a": [1, 2]}
91
+ assert llens.digest(a) == llens.digest(json.loads(json.dumps(a, indent=4)))
92
+ assert llens.digest(a) != llens.digest({"b": 1, "a": [2, 1]})
93
+
94
+
95
+ def test_array_digest_sees_dtype_and_shape():
96
+ x = np.arange(6, dtype=np.int64)
97
+ assert llens.array_sha256(x) != llens.array_sha256(x.reshape(2, 3))
98
+ assert llens.array_sha256(x) != llens.array_sha256(x.astype(np.int32))
99
+ assert llens.array_sha256(x) == llens.array_sha256(x.copy())
100
+
101
+
102
+ # --------------------------------------------------------------------------- verify, both ways
103
+
104
+ def test_clean_capture_and_artifact_verify():
105
+ cap, raw = synthetic()
106
+ art = roundtrip(llens.build(cap, raw))
107
+ assert all(ok for ok, _, _ in llens.verify_llens(art, cap, raw))
108
+
109
+
110
+ def failing(results):
111
+ return [name for ok, name, _ in results if not ok]
112
+
113
+
114
+ def test_a_changed_raw_value_fails_its_digest():
115
+ cap, raw = synthetic()
116
+ art = roundtrip(llens.build(cap, raw))
117
+ raw["jacobian_top_ids"][1, 2, 0] = 4
118
+ assert "raw jacobian_top_ids digest" in failing(llens.verify_llens(art, cap, raw))
119
+
120
+
121
+ def test_a_changed_derived_section_fails_recompute():
122
+ cap, raw = synthetic()
123
+ art = roundtrip(llens.build(cap, raw))
124
+ art["derived"]["jacobian"]["per_layer"]["1"]["top1_agree"] = 3
125
+ assert failing(llens.verify_llens(art, cap, raw)) == ["derived recomputes from raw"]
126
+
127
+
128
+ def test_an_edited_capture_is_no_longer_the_one_cited():
129
+ cap, raw = synthetic()
130
+ art = roundtrip(llens.build(cap, raw))
131
+ cap["input"]["prompt"] = "a c"
132
+ assert failing(llens.verify_llens(art, cap, raw)) == ["llens cites this capture"]
133
+
134
+
135
+ def test_a_different_task_id_fails():
136
+ cap, raw = synthetic()
137
+ art = roundtrip(llens.build(cap, raw))
138
+ art["task_id"] = "other"
139
+ assert "llens task_id agrees" in failing(llens.verify_llens(art, cap, raw))
140
+
141
+
142
+ def test_a_shape_that_disagrees_with_the_readout_fails():
143
+ cap, raw = synthetic()
144
+ cap["readout"]["top_k"] = 3
145
+ assert "raw jacobian_top_ids shape" in failing(llens.verify_capture(cap, raw))
146
+
147
+
148
+ def test_build_refuses_a_capture_that_does_not_verify():
149
+ cap, raw = synthetic()
150
+ raw["final_top_ids"][0, 0] = 1
151
+ with pytest.raises(ValueError, match="does not verify"):
152
+ llens.build(cap, raw)
153
+
154
+
155
+ # --------------------------------------------------------------------------- the triad, end to end on disk
156
+
157
+ def write_memory(root, name, body):
158
+ with open(os.path.join(root, name + ".md"), "w", encoding="utf-8", newline="\n") as f:
159
+ f.write("---\nname: %s\ndescription: test\nmetadata:\n type: project\n---\n\n%s\n" % (name, body))
160
+
161
+
162
+ def triad(tmp_path, task_id=TASK):
163
+ cap, raw = synthetic(task_id)
164
+ art = llens.build(cap, raw)
165
+ run = tmp_path / "runs" / task_id
166
+ run.mkdir(parents=True)
167
+ json.dump(cap, open(run / "jlens_snapshot.json", "w", encoding="utf-8"), indent=1)
168
+ np.savez_compressed(run / "jlens_raw.npz", **raw)
169
+ json.dump(art, open(run / "llens.json", "w", encoding="utf-8"), indent=1)
170
+ json.dump(VOCAB, open(run / "jlens_vocab.json", "w", encoding="utf-8"), indent=0)
171
+ cs, ls = llens.digest(cap), llens.digest(art)
172
+ log = tmp_path / "tombstone.md"
173
+ log.write_text("# log\n\n### 2026-10-08T00:00:00Z | t\n- id: 1\n- task: t\n- outcome: success\n"
174
+ "- notes: task_id=%s; jlens_capture_sha256=%s; llens_sha256=%s; jlens_digest=jlens x=1\n"
175
+ % (task_id, cs, ls), encoding="utf-8")
176
+ mem = tmp_path / "mem"
177
+ mem.mkdir()
178
+ write_memory(mem, "task-" + task_id, "task_id=%s\ntitle=t\njlens_capture_sha256=%s\nllens_sha256=%s" % (task_id, cs, ls))
179
+ write_memory(mem, "jlens-" + task_id, json.dumps(cap, indent=2))
180
+ write_memory(mem, "llens-" + task_id, json.dumps(art, indent=2))
181
+ return {"run": str(run), "log": str(log), "mem": str(mem), "cap_sha": cs, "ll_sha": ls}
182
+
183
+
184
+ def run_verify(t, task_id=TASK):
185
+ return verify_triad.verify(task_id, t["run"], t["log"], t["mem"])
186
+
187
+
188
+ def test_a_complete_triad_verifies(tmp_path):
189
+ res = run_verify(triad(tmp_path))
190
+ assert failing(res) == [] and len(res) > 20
191
+
192
+
193
+ def test_verifier_main_exits_1_on_failure_and_0_when_clean(tmp_path):
194
+ t = triad(tmp_path)
195
+ args = ["--task-id", TASK, "--run-dir", t["run"], "--tombstone-log", t["log"], "--memory-root", t["mem"]]
196
+ assert verify_triad.main(args) == 0
197
+ os.remove(os.path.join(t["mem"], "llens-%s.md" % TASK))
198
+ assert verify_triad.main(args) == 1
199
+
200
+
201
+ def test_no_tombstone_entry_for_the_task_fails(tmp_path):
202
+ t = triad(tmp_path)
203
+ open(t["log"], "w", encoding="utf-8").write("# log\n")
204
+ assert "tombstone: an entry for this task_id" in failing(run_verify(t))
205
+
206
+
207
+ def test_a_tombstone_entry_citing_another_artifact_fails(tmp_path):
208
+ t = triad(tmp_path)
209
+ text = open(t["log"], encoding="utf-8").read().replace(t["ll_sha"], "0" * 64)
210
+ open(t["log"], "w", encoding="utf-8").write(text)
211
+ assert failing(run_verify(t)) == ["tombstone: llens_sha256 agrees"]
212
+
213
+
214
+ def test_a_missing_covenant_memory_fails(tmp_path):
215
+ t = triad(tmp_path)
216
+ os.remove(os.path.join(t["mem"], "jlens-%s.md" % TASK))
217
+ assert failing(run_verify(t)) == ["covenant: jlens-%s present" % TASK]
218
+
219
+
220
+ def test_a_covenant_task_memory_under_another_id_fails(tmp_path):
221
+ t = triad(tmp_path)
222
+ p = os.path.join(t["mem"], "task-%s.md" % TASK)
223
+ text = open(p, encoding="utf-8").read()
224
+ open(p, "w", encoding="utf-8").write(text.replace("task_id=%s" % TASK, "task_id=x"))
225
+ assert failing(run_verify(t)) == ["covenant: task memory task_id"]
226
+
227
+
228
+ def test_an_altered_covenant_llens_body_fails(tmp_path):
229
+ t = triad(tmp_path)
230
+ art = json.load(open(os.path.join(t["run"], "llens.json"), encoding="utf-8"))
231
+ art["what"] = art["what"] + " (edited)"
232
+ write_memory(t["mem"], "llens-" + TASK, json.dumps(art))
233
+ assert failing(run_verify(t)) == ["covenant: llens-%s body digest agrees" % TASK]
234
+
235
+
236
+ def test_a_missing_lens_artifact_fails_closed(tmp_path):
237
+ t = triad(tmp_path)
238
+ os.remove(os.path.join(t["run"], "llens.json"))
239
+ f = failing(run_verify(t))
240
+ assert "lens leg: llens.json present" in f
241
+ assert "tombstone: llens_sha256 agrees" in f # no artifact, so nothing can agree with the tombstone
242
+
243
+
244
+ def test_reformatting_a_memory_body_is_not_a_change(tmp_path):
245
+ t = triad(tmp_path)
246
+ cap = json.load(open(os.path.join(t["run"], "jlens_snapshot.json"), encoding="utf-8"))
247
+ write_memory(t["mem"], "jlens-" + TASK, json.dumps(cap, separators=(",", ":"), sort_keys=True))
248
+ assert failing(run_verify(t)) == []
249
+
250
+
251
+ def test_raw_npz_swapped_for_another_tasks_fails(tmp_path):
252
+ t = triad(tmp_path)
253
+ cap2, raw2 = synthetic("other-task")
254
+ raw2["jacobian_top_ids"][0, 0, 1] = 19
255
+ np.savez_compressed(os.path.join(t["run"], "jlens_raw.npz"), **raw2)
256
+ assert "lens leg: raw jacobian_top_ids digest" in failing(run_verify(t))
257
+
258
+
259
+ def test_the_artifact_holds_no_decoded_vocabulary(tmp_path):
260
+ cap, raw = synthetic()
261
+ text = json.dumps(llens.build(cap, raw, VOCAB))
262
+ assert not any(w in text for w in VOCAB.values())
263
+
264
+
265
+ def test_show_decodes_from_the_vocab_file_only():
266
+ cap, raw = synthetic()
267
+ r = llens.readable(raw, VOCAB, position=0, n_tokens=2)
268
+ assert r["final_top_tokens"] == ["t5", "t6"] and r["jacobian_top_tokens_by_layer"]["1"] == ["t5", "t6"]
269
+
270
+
271
+ def test_a_changed_vocab_file_fails(tmp_path):
272
+ t = triad(tmp_path)
273
+ v = dict(VOCAB, **{"5": "changed"})
274
+ json.dump(v, open(os.path.join(t["run"], "jlens_vocab.json"), "w", encoding="utf-8"))
275
+ assert failing(run_verify(t)) == ["lens leg: vocab digest"]
276
+
277
+
278
+ def test_a_missing_vocab_file_fails_closed(tmp_path):
279
+ t = triad(tmp_path)
280
+ os.remove(os.path.join(t["run"], "jlens_vocab.json"))
281
+ assert "lens leg: artifacts readable" in failing(run_verify(t))
tombstone/tombstone.md CHANGED
@@ -4,7 +4,10 @@ Public sample only. Real deployments append locally; do not commit private entri
4
 
5
  Format: each entry is a markdown subsection with `id`, `task`, `done`, `outcome`,
6
  `mistakes`, `lessons`, and `notes`. Triad runs stamp `task_id=...` and
7
- `jlens_digest=...` into notes so the three legs share one id.
 
 
 
8
 
9
  ### 2026-10-07T02:09:13Z | jlens-triad-e2e
10
  - id: 20261007-020913
 
4
 
5
  Format: each entry is a markdown subsection with `id`, `task`, `done`, `outcome`,
6
  `mistakes`, `lessons`, and `notes`. Triad runs stamp `task_id=...` and
7
+ `jlens_digest=...` into notes so the three legs share one id. Since 2026-10-08 they
8
+ also stamp `jlens_capture_sha256=...` and `llens_sha256=...`, which
9
+ `triad/verify_triad.py` checks against the lens leg and covenant. The entry below
10
+ predates that and is kept as written.
11
 
12
  ### 2026-10-07T02:09:13Z | jlens-triad-e2e
13
  - id: 20261007-020913
triad/fire_triad.sh CHANGED
@@ -1,14 +1,20 @@
1
  #!/usr/bin/env bash
2
- # fire_triad.sh — mandatory three-leg triad (tombstone + covenant + real JLens)
 
3
  #
4
  # Usage:
5
  # ./fire_triad.sh TITLE TASK DONE OUTCOME PROMPT [MISTAKES] [LESSONS] [NOTES]
6
  # Or env: TITLE TASK DONE OUTCOME PROMPT MISTAKES LESSONS NOTES TASK_ID
7
  #
8
  # Legs (all required; any failure => exit nonzero):
9
- # (a) tombstone append with JLens digest in notes
10
- # (b) covenant puts (task-{id} + jlens-{id} type:jlens snapshot)
11
- # (c) real Anthropic jlens.JacobianLens.apply snapshot
 
 
 
 
 
12
  #
13
  # Defaults resolve relative to REPO_ROOT (parent of triad/).
14
  set -euo pipefail
@@ -21,6 +27,9 @@ JLENS_VENV="${JLENS_VENV:-${REPO_ROOT}/.venv}"
21
  # Prefer repo-local scripts; allow override for shared venvs.
22
  JLENS_SNAPSHOT_PY="${JLENS_SNAPSHOT_PY:-$REPO_ROOT/jlens_snapshot.py}"
23
  JLENS_LENS="${JLENS_LENS:-$REPO_ROOT/gpt2_jacobian_lens.pt}"
 
 
 
24
  RUN_DIR="${RUN_DIR:-$REPO_ROOT/runs}"
25
  export AI_MEMORY_ROOT="${AI_MEMORY_ROOT:-$REPO_ROOT/data/ai_memory_root}"
26
  export COVENANT_SRC="${COVENANT_SRC:-$REPO_ROOT/vendor/covenant}"
@@ -44,6 +53,8 @@ SNAP_DIR="$RUN_DIR/$TASK_ID"
44
  mkdir -p "$SNAP_DIR"
45
  SNAP_JSON="$SNAP_DIR/jlens_snapshot.json"
46
  SNAP_DIGEST="$SNAP_DIR/jlens_digest.txt"
 
 
47
 
48
  echo "=== fire_triad task_id=$TASK_ID ==="
49
  echo "prompt=$PROMPT"
@@ -52,6 +63,8 @@ echo "REPO_ROOT=$REPO_ROOT"
52
  # Resolve python: JLENS_VENV, else python3 on PATH
53
  if [[ -x "$JLENS_VENV/bin/python" ]]; then
54
  PYTHON="$JLENS_VENV/bin/python"
 
 
55
  elif command -v python3 >/dev/null 2>&1; then
56
  PYTHON="$(command -v python3)"
57
  else
@@ -59,8 +72,8 @@ else
59
  exit 1
60
  fi
61
 
62
- # --- Leg (c) FIRST so digest can go into tombstone notes ---
63
- echo "--- leg c: JLens apply ---"
64
  if [[ ! -f "$JLENS_SNAPSHOT_PY" ]]; then
65
  echo "Missing jlens_snapshot.py at $JLENS_SNAPSHOT_PY" >&2
66
  exit 1
@@ -71,14 +84,26 @@ if [[ ! -f "$JLENS_LENS" ]]; then
71
  exit 1
72
  fi
73
 
74
- "$PYTHON" "$JLENS_SNAPSHOT_PY" \
75
  --task-id "$TASK_ID" \
76
  --prompt "$PROMPT" \
77
  --out "$SNAP_JSON" \
 
78
  --digest "$SNAP_DIGEST" \
79
- --lens "$JLENS_LENS"
 
80
  JLENS_DIGEST="$(head -1 "$SNAP_DIGEST")"
81
- JLENS_BODY="$(cat "$SNAP_JSON")"
 
 
 
 
 
 
 
 
 
 
82
 
83
  # --- Leg (a) tombstone with JLens digest in notes ---
84
  echo "--- leg a: tombstone ---"
@@ -86,7 +111,7 @@ if [[ ! -x "$TOMBSTONE_APPEND" ]]; then
86
  echo "Missing tombstone append at $TOMBSTONE_APPEND" >&2
87
  exit 1
88
  fi
89
- COMBINED_NOTES="task_id=${TASK_ID}; jlens_digest=${JLENS_DIGEST}"
90
  if [[ -n "$NOTES" ]]; then
91
  COMBINED_NOTES="${COMBINED_NOTES}; ${NOTES}"
92
  fi
@@ -105,24 +130,33 @@ done=${DONE}
105
  outcome=${OUTCOME}
106
  prompt=${PROMPT}
107
  jlens_digest=${JLENS_DIGEST}
 
 
108
  "
109
- JLENS_BODY="$JLENS_BODY" "$PUT_COVENANT" "$TASK_ID" "$TASK_BODY" \
110
- "triad task memory for ${TASK_ID}"
111
 
112
- # Proof: all three legs share task_id
 
113
  echo "=== triad proof ==="
114
  echo "task_id=$TASK_ID"
115
  echo "jlens_snapshot=$SNAP_JSON"
116
- rg -n --fixed-strings "$TASK_ID" "$REPO_ROOT/tombstone/tombstone.md" | tail -5 || {
117
  echo "PROOF FAIL: task_id not in tombstone" >&2
118
  exit 1
119
  }
120
- rg -n --fixed-strings "$TASK_ID" "$AI_MEMORY_ROOT" -g '*.md' | head -20 || {
121
  echo "PROOF FAIL: task_id not in covenant memories" >&2
122
  exit 1
123
  }
124
- rg -n --fixed-strings "$TASK_ID" "$SNAP_JSON" || {
125
  echo "PROOF FAIL: task_id not in jlens snapshot" >&2
126
  exit 1
127
  }
128
- echo "FIRE_TRIAD_OK task_id=$TASK_ID (tombstone + covenant + real jlens.apply)"
 
 
 
 
 
 
 
1
  #!/usr/bin/env bash
2
+ # fire_triad.sh — mandatory three-leg triad (tombstone + covenant + lens leg:
3
+ # L-lens over real Anthropic Jacobian Lens measurements)
4
  #
5
  # Usage:
6
  # ./fire_triad.sh TITLE TASK DONE OUTCOME PROMPT [MISTAKES] [LESSONS] [NOTES]
7
  # Or env: TITLE TASK DONE OUTCOME PROMPT MISTAKES LESSONS NOTES TASK_ID
8
  #
9
  # Legs (all required; any failure => exit nonzero):
10
+ # (a) tombstone append; notes carry task_id, jlens_capture_sha256, llens_sha256
11
+ # (b) covenant puts: task-{id} (both digests), jlens-{id} (the JLens capture),
12
+ # llens-{id} (the L-lens artifact)
13
+ # (c) lens leg: jlens_snapshot.py runs upstream jlens.JacobianLens.apply
14
+ # (anthropics/jacobian-lens) and stores what it returned; llens.py
15
+ # builds the L-lens artifact from that capture and checks it
16
+ # After all three: triad/verify_triad.py checks the legs agree (task_id and
17
+ # digests); a disagreement or an absent artifact fails the triad.
18
  #
19
  # Defaults resolve relative to REPO_ROOT (parent of triad/).
20
  set -euo pipefail
 
27
  # Prefer repo-local scripts; allow override for shared venvs.
28
  JLENS_SNAPSHOT_PY="${JLENS_SNAPSHOT_PY:-$REPO_ROOT/jlens_snapshot.py}"
29
  JLENS_LENS="${JLENS_LENS:-$REPO_ROOT/gpt2_jacobian_lens.pt}"
30
+ LLENS_PY="${LLENS_PY:-$REPO_ROOT/llens.py}"
31
+ VERIFY_PY="${VERIFY_PY:-$DIR/verify_triad.py}"
32
+ export TOMBSTONE_LOG="${TOMBSTONE_LOG:-$REPO_ROOT/tombstone/tombstone.md}"
33
  RUN_DIR="${RUN_DIR:-$REPO_ROOT/runs}"
34
  export AI_MEMORY_ROOT="${AI_MEMORY_ROOT:-$REPO_ROOT/data/ai_memory_root}"
35
  export COVENANT_SRC="${COVENANT_SRC:-$REPO_ROOT/vendor/covenant}"
 
53
  mkdir -p "$SNAP_DIR"
54
  SNAP_JSON="$SNAP_DIR/jlens_snapshot.json"
55
  SNAP_DIGEST="$SNAP_DIR/jlens_digest.txt"
56
+ SNAP_RAW="$SNAP_DIR/jlens_raw.npz"
57
+ LLENS_JSON="$SNAP_DIR/llens.json"
58
 
59
  echo "=== fire_triad task_id=$TASK_ID ==="
60
  echo "prompt=$PROMPT"
 
63
  # Resolve python: JLENS_VENV, else python3 on PATH
64
  if [[ -x "$JLENS_VENV/bin/python" ]]; then
65
  PYTHON="$JLENS_VENV/bin/python"
66
+ elif [[ -x "$JLENS_VENV/Scripts/python.exe" ]]; then
67
+ PYTHON="$JLENS_VENV/Scripts/python.exe" # a Windows venv
68
  elif command -v python3 >/dev/null 2>&1; then
69
  PYTHON="$(command -v python3)"
70
  else
 
72
  exit 1
73
  fi
74
 
75
+ # --- Leg (c) FIRST so its digests can go into tombstone notes ---
76
+ echo "--- leg c: JLens capture (upstream jlens.JacobianLens.apply) ---"
77
  if [[ ! -f "$JLENS_SNAPSHOT_PY" ]]; then
78
  echo "Missing jlens_snapshot.py at $JLENS_SNAPSHOT_PY" >&2
79
  exit 1
 
84
  exit 1
85
  fi
86
 
87
+ SNAP_OUT="$("$PYTHON" "$JLENS_SNAPSHOT_PY" \
88
  --task-id "$TASK_ID" \
89
  --prompt "$PROMPT" \
90
  --out "$SNAP_JSON" \
91
+ --raw "$SNAP_RAW" \
92
  --digest "$SNAP_DIGEST" \
93
+ --lens "$JLENS_LENS")"
94
+ printf '%s\n' "$SNAP_OUT"
95
  JLENS_DIGEST="$(head -1 "$SNAP_DIGEST")"
96
+ JLENS_CAPTURE_SHA256="$(printf '%s\n' "$SNAP_OUT" | sed -n 's/^JLENS_CAPTURE_SHA256=//p' | tr -d '\r')"
97
+
98
+ echo "--- leg c: L-lens artifact (llens.py, built on the capture) ---"
99
+ LLENS_OUT="$("$PYTHON" "$LLENS_PY" build --capture "$SNAP_JSON" --raw "$SNAP_RAW" --out "$LLENS_JSON")"
100
+ printf '%s\n' "$LLENS_OUT"
101
+ LLENS_SHA256="$(printf '%s\n' "$LLENS_OUT" | sed -n 's/^LLENS_SHA256=//p' | tr -d '\r')"
102
+ if [[ ! "$JLENS_CAPTURE_SHA256" =~ ^[0-9a-f]{64}$ || ! "$LLENS_SHA256" =~ ^[0-9a-f]{64}$ ]]; then
103
+ echo "Lens leg produced no digest (capture=${JLENS_CAPTURE_SHA256:-none} llens=${LLENS_SHA256:-none})" >&2
104
+ exit 1
105
+ fi
106
+ "$PYTHON" "$LLENS_PY" verify --capture "$SNAP_JSON" --raw "$SNAP_RAW" --llens "$LLENS_JSON"
107
 
108
  # --- Leg (a) tombstone with JLens digest in notes ---
109
  echo "--- leg a: tombstone ---"
 
111
  echo "Missing tombstone append at $TOMBSTONE_APPEND" >&2
112
  exit 1
113
  fi
114
+ COMBINED_NOTES="task_id=${TASK_ID}; jlens_capture_sha256=${JLENS_CAPTURE_SHA256}; llens_sha256=${LLENS_SHA256}; jlens_digest=${JLENS_DIGEST}"
115
  if [[ -n "$NOTES" ]]; then
116
  COMBINED_NOTES="${COMBINED_NOTES}; ${NOTES}"
117
  fi
 
130
  outcome=${OUTCOME}
131
  prompt=${PROMPT}
132
  jlens_digest=${JLENS_DIGEST}
133
+ jlens_capture_sha256=${JLENS_CAPTURE_SHA256}
134
+ llens_sha256=${LLENS_SHA256}
135
  "
136
+ JLENS_BODY_FILE="$SNAP_JSON" LLENS_BODY_FILE="$LLENS_JSON" COVENANT_SRC="$COVENANT_SRC" \
137
+ "$PUT_COVENANT" "$TASK_ID" "$TASK_BODY" "triad task memory for ${TASK_ID}"
138
 
139
+ # Proof: all three legs share task_id (rg when installed, else grep -F: same fixed-string search)
140
+ has_rg() { command -v rg >/dev/null 2>&1; }
141
  echo "=== triad proof ==="
142
  echo "task_id=$TASK_ID"
143
  echo "jlens_snapshot=$SNAP_JSON"
144
+ { if has_rg; then rg -n --fixed-strings "$TASK_ID" "$TOMBSTONE_LOG"; else grep -n -F "$TASK_ID" "$TOMBSTONE_LOG"; fi; } | tail -5 || {
145
  echo "PROOF FAIL: task_id not in tombstone" >&2
146
  exit 1
147
  }
148
+ { if has_rg; then rg -n --fixed-strings "$TASK_ID" "$AI_MEMORY_ROOT" -g '*.md'; else grep -rn -F --include='*.md' "$TASK_ID" "$AI_MEMORY_ROOT"; fi; } | head -20 || {
149
  echo "PROOF FAIL: task_id not in covenant memories" >&2
150
  exit 1
151
  }
152
+ { if has_rg; then rg -n --fixed-strings "$TASK_ID" "$SNAP_JSON"; else grep -n -F "$TASK_ID" "$SNAP_JSON"; fi; } || {
153
  echo "PROOF FAIL: task_id not in jlens snapshot" >&2
154
  exit 1
155
  }
156
+ echo "=== triad verification (task_id and digests agree across all three legs) ==="
157
+ "$PYTHON" "$VERIFY_PY" --task-id "$TASK_ID" --run-dir "$SNAP_DIR" \
158
+ --tombstone-log "$TOMBSTONE_LOG" --memory-root "$AI_MEMORY_ROOT" || {
159
+ echo "PROOF FAIL: the three legs do not agree (see FAIL lines above)" >&2
160
+ exit 1
161
+ }
162
+ echo "FIRE_TRIAD_OK task_id=$TASK_ID (tombstone + covenant + L-lens over real jlens.apply; verified)"
triad/on_speak.sh CHANGED
@@ -2,7 +2,7 @@
2
  # on_speak.sh — fire the mandatory triad for one Grok Bot spoken turn.
3
  #
4
  # Lawrence rule: every time Grok Bot speaks (SendToUser), it counts as a task.
5
- # This hook assigns a task_id and runs fire_triad.sh (tombstone ∥ covenant ∥ JLens).
6
  # No degrade mode: any leg failure exits nonzero.
7
  #
8
  # Usage:
 
2
  # on_speak.sh — fire the mandatory triad for one Grok Bot spoken turn.
3
  #
4
  # Lawrence rule: every time Grok Bot speaks (SendToUser), it counts as a task.
5
+ # This hook assigns a task_id and runs fire_triad.sh (tombstone ∥ covenant ∥ L-lens over Anthropic's Jacobian Lens).
6
  # No degrade mode: any leg failure exits nonzero.
7
  #
8
  # Usage:
triad/put_covenant_memory.sh CHANGED
@@ -9,6 +9,12 @@
9
  # JLENS_BODY — if set, also writes jlens-{TASK_ID} (type:jlens snapshot)
10
  # CLI enum only allows user|feedback|project|reference;
11
  # we use --type project and stamp type:jlens in body+description.
 
 
 
 
 
 
12
  # OBS_BODY — retired; ignored (self-observation is not a triad leg)
13
  set -euo pipefail
14
  DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -39,13 +45,32 @@ safe_name() { printf '%s' "$1" | tr -c 'A-Za-z0-9._-' '-'; }
39
  MEM_NAME="$(safe_name "task-${TASK_ID}")"
40
 
41
  echo "AI_MEMORY_ROOT=$AI_MEMORY_ROOT"
42
- python3 "$MAIN" --root "$AI_MEMORY_ROOT" put "$MEM_NAME" \
43
  --description "$DESCRIPTION" \
44
  --type "$MEMORY_TYPE" \
45
  --body "$BODY" \
46
  --agent "$AGENT_NAME"
47
 
48
- if [[ -n "${JLENS_BODY:-}" ]]; then
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  JLENS_NAME="$(safe_name "jlens-${TASK_ID}")"
50
  # Real CLI type enum has no 'jlens'; stamp type:jlens in description+body.
51
  python3 "$MAIN" --root "$AI_MEMORY_ROOT" put "$JLENS_NAME" \
 
9
  # JLENS_BODY — if set, also writes jlens-{TASK_ID} (type:jlens snapshot)
10
  # CLI enum only allows user|feedback|project|reference;
11
  # we use --type project and stamp type:jlens in body+description.
12
+ # JLENS_BODY_FILE / LLENS_BODY_FILE — the upstream JLens capture and the
13
+ # L-lens artifact, read from files (they can exceed one
14
+ # command-line argument); written as jlens-{TASK_ID} and
15
+ # llens-{TASK_ID} through triad/put_memory_file.py, which makes
16
+ # the same MemoryStore.put call main.py makes.
17
+ # PYTHON_COV — python for covenant's memory code (default python3)
18
  # OBS_BODY — retired; ignored (self-observation is not a triad leg)
19
  set -euo pipefail
20
  DIR="$(cd "$(dirname "$0")" && pwd)"
 
45
  MEM_NAME="$(safe_name "task-${TASK_ID}")"
46
 
47
  echo "AI_MEMORY_ROOT=$AI_MEMORY_ROOT"
48
+ "${PYTHON_COV:-python3}" "$MAIN" --root "$AI_MEMORY_ROOT" put "$MEM_NAME" \
49
  --description "$DESCRIPTION" \
50
  --type "$MEMORY_TYPE" \
51
  --body "$BODY" \
52
  --agent "$AGENT_NAME"
53
 
54
+ PUT_FILE="$DIR/put_memory_file.py"
55
+ WROTE_EXTRA=""
56
+ if [[ -n "${JLENS_BODY_FILE:-}" ]]; then
57
+ JLENS_NAME="$(safe_name "jlens-${TASK_ID}")"
58
+ "${PYTHON_COV:-python3}" "$PUT_FILE" --root "$AI_MEMORY_ROOT" --name "$JLENS_NAME" \
59
+ --description "type:jlens upstream Jacobian Lens capture (anthropics/jacobian-lens, Apache-2.0) for ${TASK_ID}" \
60
+ --type project --agent "$AGENT_NAME" --body-file "$JLENS_BODY_FILE" --covenant-src "$COVENANT_SRC"
61
+ WROTE_EXTRA="$WROTE_EXTRA $JLENS_NAME"
62
+ fi
63
+ if [[ -n "${LLENS_BODY_FILE:-}" ]]; then
64
+ LLENS_NAME="$(safe_name "llens-${TASK_ID}")"
65
+ "${PYTHON_COV:-python3}" "$PUT_FILE" --root "$AI_MEMORY_ROOT" --name "$LLENS_NAME" \
66
+ --description "type:llens Threefold L-lens observability/provenance artifact built on the JLens capture jlens-${TASK_ID}" \
67
+ --type project --agent "$AGENT_NAME" --body-file "$LLENS_BODY_FILE" --covenant-src "$COVENANT_SRC"
68
+ WROTE_EXTRA="$WROTE_EXTRA $LLENS_NAME"
69
+ fi
70
+
71
+ if [[ -n "$WROTE_EXTRA" ]]; then
72
+ echo "Wrote covenant memories: $MEM_NAME$WROTE_EXTRA under $AI_MEMORY_ROOT"
73
+ elif [[ -n "${JLENS_BODY:-}" ]]; then
74
  JLENS_NAME="$(safe_name "jlens-${TASK_ID}")"
75
  # Real CLI type enum has no 'jlens'; stamp type:jlens in description+body.
76
  python3 "$MAIN" --root "$AI_MEMORY_ROOT" put "$JLENS_NAME" \
triad/put_memory_file.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Put one covenant memory whose body is read from a FILE.
3
+
4
+ Covenant's CLI takes the body as a command-line argument (main.py put --body). A JLens capture or an L-lens artifact
5
+ can be larger than one argument may be (32 KB on Windows, 128 KB per argument on Linux). This helper therefore makes
6
+ the same call main.py makes, MemoryStore(root).put(name, description, type, body, agent), using the same covenant
7
+ code, with the body read from a file.
8
+
9
+ Usage: put_memory_file.py --root ROOT --name NAME --description TEXT --type TYPE --agent AGENT --body-file PATH
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import sys
17
+
18
+
19
+ def main(argv=None) -> int:
20
+ ap = argparse.ArgumentParser()
21
+ ap.add_argument("--root", required=True)
22
+ ap.add_argument("--name", required=True)
23
+ ap.add_argument("--description", required=True)
24
+ ap.add_argument("--type", default="project")
25
+ ap.add_argument("--agent", default="cli")
26
+ ap.add_argument("--body-file", required=True)
27
+ ap.add_argument("--covenant-src", default=os.environ.get("COVENANT_SRC", ""))
28
+ a = ap.parse_args(argv)
29
+ amd = os.path.join(a.covenant_src, "ai_memory_system")
30
+ if not os.path.isfile(os.path.join(amd, "memory_store.py")):
31
+ print("covenant memory_store.py not found under %r (set COVENANT_SRC)" % amd, file=sys.stderr)
32
+ return 1
33
+ sys.path.insert(0, amd)
34
+ from memory_store import MemoryStore # noqa: E402 -- covenant's own store, not a copy
35
+ with open(a.body_file, encoding="utf-8") as f:
36
+ body = f.read()
37
+ out = MemoryStore(a.root).put(a.name, a.description, a.type, body, a.agent)
38
+ print(json.dumps(out, indent=1, sort_keys=True))
39
+ return 0
40
+
41
+
42
+ if __name__ == "__main__":
43
+ raise SystemExit(main())
triad/verify_triad.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Verify one triad: all three legs exist for one task_id, and their links agree.
3
+
4
+ Fails closed. An absent artifact, an unreadable one, a task_id that differs, or a digest that differs is a FAIL,
5
+ and the exit status is 1. Checks, in order:
6
+
7
+ lens leg runs/<task_id>/jlens_snapshot.json (the JLens capture), jlens_raw.npz, jlens_vocab.json and
8
+ llens.json exist. The capture's raw arrays and vocabulary file match the digests recorded at capture
9
+ time. The L-lens artifact cites this capture, and its derived section recomputes exactly from the raw
10
+ arrays (llens.verify_llens).
11
+ tombstone an entry in the tombstone log whose notes carry task_id=<id>, jlens_capture_sha256=<the capture's
12
+ digest> and llens_sha256=<the L-lens artifact's digest>.
13
+ covenant memories task-<id>, jlens-<id> and llens-<id> exist under AI_MEMORY_ROOT. The task memory carries the
14
+ same task_id and both digests. The jlens and llens memories hold bodies whose canonical digests equal
15
+ the capture's and the artifact's.
16
+
17
+ What it cannot see. It checks that the records agree with each other and with the stored arrays. It does not re-run
18
+ the model, so it cannot tell whether the arrays are what upstream would return today. For that, re-run
19
+ jlens_snapshot.py with the recorded configuration and compare llens_capture_time.full_tensor_sha256. It also does
20
+ not check covenant's own hash chain; covenant's verify_chain does that.
21
+
22
+ Usage:
23
+ verify_triad.py --task-id ID [--run-dir DIR] [--tombstone-log PATH] [--memory-root DIR]
24
+ Defaults: --run-dir $RUN_DIR/<id> or <repo>/runs/<id>; --tombstone-log $TOMBSTONE_LOG or
25
+ <repo>/tombstone/tombstone.md; --memory-root $AI_MEMORY_ROOT.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import re
33
+ import sys
34
+
35
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
36
+ sys.path.insert(0, REPO_ROOT)
37
+ import llens # noqa: E402
38
+
39
+ HEX64 = r"[0-9a-f]{64}"
40
+
41
+
42
+ def safe_name(s: str) -> str:
43
+ """put_covenant_memory.sh's safe_name: every character outside [A-Za-z0-9._-] becomes '-'."""
44
+ return re.sub(r"[^A-Za-z0-9._-]", "-", s)
45
+
46
+
47
+ def tombstone_entries(text: str) -> list:
48
+ """Each '### ' section of the tombstone log as {field: value}."""
49
+ out = []
50
+ for block in re.split(r"(?m)^### ", text)[1:]:
51
+ fields = {"heading": block.splitlines()[0].strip()}
52
+ for line in block.splitlines()[1:]:
53
+ m = re.match(r"^- ([a-z_]+): ?(.*)$", line)
54
+ if m:
55
+ fields[m.group(1)] = m.group(2)
56
+ out.append(fields)
57
+ return out
58
+
59
+
60
+ def note_value(notes: str, key: str):
61
+ m = re.search(r"(?:^|;\s*)%s=([^;]*)" % re.escape(key), notes or "")
62
+ return m.group(1).strip() if m else None
63
+
64
+
65
+ def memory_body(root: str, name: str):
66
+ """The body of covenant memory <name> (text after the closing frontmatter fence), or None."""
67
+ path = os.path.join(root, safe_name(name) + ".md")
68
+ if not os.path.isfile(path):
69
+ return None
70
+ text = open(path, encoding="utf-8").read()
71
+ m = re.match(r"^---\n.*?\n---\n(.*)$", text, re.S)
72
+ return (m.group(1) if m else text).strip()
73
+
74
+
75
+ def verify(task_id: str, run_dir: str, tombstone_log: str, memory_root: str) -> list:
76
+ out = []
77
+
78
+ def check(ok, name, why=""):
79
+ out.append((bool(ok), name, why))
80
+ return bool(ok)
81
+
82
+ cap_p, raw_p, ll_p = (os.path.join(run_dir, f) for f in ("jlens_snapshot.json", "jlens_raw.npz", "llens.json"))
83
+ present = [check(os.path.isfile(p), "lens leg: %s present" % os.path.basename(p), "missing: %s" % p)
84
+ for p in (cap_p, raw_p, ll_p)]
85
+ cap_sha = ll_sha = None
86
+ if all(present):
87
+ try:
88
+ capture = json.load(open(cap_p, encoding="utf-8"))
89
+ art = json.load(open(ll_p, encoding="utf-8"))
90
+ raw = llens.load_raw(raw_p)
91
+ vocab = llens.load_vocab(cap_p, capture)
92
+ except Exception as e: # noqa: BLE001 -- unreadable is a failure, named
93
+ check(False, "lens leg: artifacts readable", "%s: %s" % (type(e).__name__, e))
94
+ else:
95
+ check(capture.get("task_id") == task_id, "lens leg: capture task_id",
96
+ "capture says %r" % capture.get("task_id"))
97
+ out.extend((ok, "lens leg: " + name, why) for ok, name, why in llens.verify_llens(art, capture, raw, vocab))
98
+ cap_sha, ll_sha = llens.digest(capture), llens.digest(art)
99
+
100
+ # tombstone
101
+ if check(os.path.isfile(tombstone_log), "tombstone: log present", "missing: %s" % tombstone_log):
102
+ entries = [e for e in tombstone_entries(open(tombstone_log, encoding="utf-8").read())
103
+ if note_value(e.get("notes"), "task_id") == task_id]
104
+ if check(entries, "tombstone: an entry for this task_id", "no entry's notes carry task_id=%s" % task_id):
105
+ e = entries[-1]
106
+ for key, want in (("jlens_capture_sha256", cap_sha), ("llens_sha256", ll_sha)):
107
+ got = note_value(e.get("notes"), key)
108
+ check(want is not None and got == want, "tombstone: %s agrees" % key,
109
+ "entry has %r, lens leg gives %r" % (got, want))
110
+
111
+ # covenant
112
+ if check(memory_root and os.path.isdir(memory_root), "covenant: memory root present",
113
+ "AI_MEMORY_ROOT %r is not a directory" % memory_root):
114
+ body = memory_body(memory_root, "task-" + task_id)
115
+ if check(body is not None, "covenant: task-%s present" % task_id, "no such memory"):
116
+ lines = dict(l.split("=", 1) for l in body.splitlines() if "=" in l)
117
+ check(lines.get("task_id") == task_id, "covenant: task memory task_id", "has %r" % lines.get("task_id"))
118
+ for key, want in (("jlens_capture_sha256", cap_sha), ("llens_sha256", ll_sha)):
119
+ check(want is not None and lines.get(key) == want, "covenant: task memory %s agrees" % key,
120
+ "memory has %r, lens leg gives %r" % (lines.get(key), want))
121
+ for prefix, want in (("jlens-", cap_sha), ("llens-", ll_sha)):
122
+ b = memory_body(memory_root, prefix + task_id)
123
+ if check(b is not None, "covenant: %s%s present" % (prefix, task_id), "no such memory"):
124
+ try:
125
+ got = llens.digest(json.loads(b))
126
+ except ValueError as e:
127
+ got = "unparseable (%s)" % e
128
+ check(want is not None and got == want, "covenant: %s%s body digest agrees" % (prefix, task_id),
129
+ "memory body gives %r, lens leg gives %r" % (got, want))
130
+ return out
131
+
132
+
133
+ def main(argv=None) -> int:
134
+ ap = argparse.ArgumentParser()
135
+ ap.add_argument("--task-id", required=True)
136
+ ap.add_argument("--run-dir", default="")
137
+ ap.add_argument("--tombstone-log", default=os.environ.get("TOMBSTONE_LOG")
138
+ or os.path.join(REPO_ROOT, "tombstone", "tombstone.md"))
139
+ ap.add_argument("--memory-root", default=os.environ.get("AI_MEMORY_ROOT", ""))
140
+ a = ap.parse_args(argv)
141
+ run_dir = a.run_dir or os.path.join(os.environ.get("RUN_DIR") or os.path.join(REPO_ROOT, "runs"), a.task_id)
142
+ results = verify(a.task_id, run_dir, a.tombstone_log, a.memory_root)
143
+ for ok, name, why in results:
144
+ print(" %s %s%s" % ("PASS" if ok else "FAIL", name, "" if ok else " -- " + why))
145
+ bad = sum(1 for r in results if not r[0])
146
+ print("VERIFY_TRIAD_OK task_id=%s checks=%d" % (a.task_id, len(results)) if not bad
147
+ else "VERIFY_TRIAD_FAIL task_id=%s failed=%d of %d" % (a.task_id, bad, len(results)))
148
+ return 1 if bad else 0
149
+
150
+
151
+ if __name__ == "__main__":
152
+ raise SystemExit(main())