L-lens: a real layer around Anthropic's Jacobian Lens, with a full readout and cross-leg verification
Browse filesThe 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 +165 -34
- demo_apply.py +6 -1
- jlens_snapshot.py +237 -65
- llens.py +298 -0
- requirements.txt +2 -0
- scripts/fetch_lens.sh +3 -2
- scripts/seed_demo.sh +4 -2
- tests/check_against_upstream.py +93 -0
- tests/e2e_triad.sh +72 -0
- tests/test_llens.py +281 -0
- tombstone/tombstone.md +4 -1
- triad/fire_triad.sh +51 -17
- triad/on_speak.sh +1 -1
- triad/put_covenant_memory.sh +27 -2
- triad/put_memory_file.py +43 -0
- triad/verify_triad.py +152 -0
|
@@ -1,48 +1,164 @@
|
|
| 1 |
# threefold
|
| 2 |
|
| 3 |
-
**Tombstone ∥ Covenant ∥
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
Self-observation is **not** a triad leg.
|
| 14 |
|
| 15 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
```
|
| 18 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
## CPU notes
|
| 33 |
|
| 34 |
-
- Model: `openai-community/gpt2` (~124M, 12 layers). Fits comfortably on CPU
|
| 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
|
| 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
|
| 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 (
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 78 |
|
| 79 |
## Layout
|
| 80 |
|
| 81 |
```
|
| 82 |
-
|
| 83 |
README.md
|
| 84 |
requirements.txt
|
| 85 |
-
.
|
| 86 |
-
|
| 87 |
demo_apply.py
|
| 88 |
tombstone/
|
| 89 |
append.sh
|
| 90 |
-
tombstone.md
|
| 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
|
|
|
|
|
|
|
| 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 |
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -1,119 +1,291 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
Usage:
|
| 5 |
-
jlens_snapshot.py --task-id TID --prompt "..." --out
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
Exits nonzero if apply fails
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
-
def
|
| 31 |
-
|
| 32 |
-
return
|
| 33 |
|
| 34 |
|
| 35 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
|
|
|
| 45 |
t0 = time.perf_counter()
|
| 46 |
-
|
|
|
|
| 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 |
-
|
| 54 |
-
|
| 55 |
-
|
| 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 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
"type": "jlens",
|
| 80 |
"task_id": args.task_id,
|
| 81 |
-
"
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
"
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 96 |
f.write("\n")
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 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 |
-
|
| 115 |
-
|
| 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 |
|
|
@@ -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())
|
|
@@ -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"
|
|
@@ -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}")
|
|
@@ -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 |
-
|
| 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 |
-
|
| 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
|
|
@@ -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())
|
|
@@ -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"
|
|
@@ -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))
|
|
@@ -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
|
|
@@ -1,14 +1,20 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
-
# fire_triad.sh — mandatory three-leg triad (tombstone + covenant +
|
|
|
|
| 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
|
| 10 |
-
# (b) covenant puts
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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" "$
|
| 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 "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)"
|
|
@@ -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 ∥
|
| 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:
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" \
|
|
@@ -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())
|
|
@@ -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())
|